| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more contributor license |
| * agreements. See the NOTICE file distributed with this work for additional information regarding |
| * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance with the License. You may obtain a |
| * copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the License |
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
| * or implied. See the License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package org.apache.geode.cache.lucene; |
| |
| import java.lang.reflect.Array; |
| import java.lang.reflect.Field; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| |
| import org.apache.logging.log4j.Logger; |
| import org.apache.lucene.document.Document; |
| |
| import org.apache.geode.cache.lucene.internal.repository.serializer.SerializerUtil; |
| import org.apache.geode.internal.util.JavaWorkarounds; |
| import org.apache.geode.logging.internal.log4j.api.LogService; |
| import org.apache.geode.pdx.PdxInstance; |
| |
| /** |
| * A built-in {@link LuceneSerializer} to parse user's nested object into a flat format, i.e. a |
| * single document. Each nested object will become a set of fields, with field name in format of |
| * contacts.name, contacts.homepage.title. |
| * |
| * Here is a example of usage: |
| * |
| * User needs to explicitly setLuceneSerializer with an instance of this class, and specify nested |
| * objects' indexed fields in following format: |
| * |
| * luceneService.createIndexFactory().setLuceneSerializer(new FlatFormatSerializer()) |
| * .addField("name").addField("contacts.name").addField("contacts.email", new KeywordAnalyzer()) |
| * .addField("contacts.address").addField("contacts.homepage.content") .create(INDEX_NAME, |
| * REGION_NAME); |
| * |
| * Region region = createRegion(REGION_NAME, RegionShortcut.PARTITION); |
| * |
| * When querying, use the same dot-separated index field name, such as contacts.homepage.content |
| * |
| * LuceneQuery query = luceneService.createLuceneQueryFactory().create(INDEX_NAME, REGION_NAME, |
| * "contacts.homepage.content:Hello*", "name"); |
| * |
| * results = query.findPages(); |
| */ |
| public class FlatFormatSerializer implements LuceneSerializer { |
| |
| private final ConcurrentMap<String, List<String>> tokenizedFieldCache = new ConcurrentHashMap<>(); |
| |
| private static final Logger logger = LogService.getLogger(); |
| |
| /** |
| * Recursively serialize each indexed field's value into a field of lucene document. The field |
| * name will be in the same format as its indexed, such as contacts.homepage.content |
| * |
| * @param index lucene index |
| * @param value user object to be serialized into index |
| */ |
| @Override |
| public Collection<Document> toDocuments(LuceneIndex index, Object value) { |
| String[] fields = index.getFieldNames(); |
| |
| Document doc = new Document(); |
| for (String indexedFieldName : fields) { |
| List<String> tokenizedFields = tokenizeField(indexedFieldName); |
| addFieldValue(doc, indexedFieldName, value, tokenizedFields); |
| } |
| |
| if (logger.isDebugEnabled()) { |
| logger.debug("FlatFormatSerializer.toDocuments: " + doc); |
| } |
| return Collections.singleton(doc); |
| } |
| |
| private List<String> tokenizeField(String indexedFieldName) { |
| List<String> tokenizedFields = |
| JavaWorkarounds.computeIfAbsent(tokenizedFieldCache, indexedFieldName, |
| field -> Arrays.asList(indexedFieldName.split("\\."))); |
| return tokenizedFields; |
| } |
| |
| private void addFieldValue(Document doc, String indexedFieldName, Object value, |
| List<String> tokenizedFields) { |
| String currentLevelField = tokenizedFields.get(0); |
| |
| Object fieldValue = getFieldValue(value, currentLevelField); |
| |
| if (fieldValue == null) { |
| return; |
| } |
| |
| if (fieldValue.getClass().isArray()) { |
| for (int i = 0; i < Array.getLength(fieldValue); i++) { |
| Object item = Array.get(fieldValue, i); |
| addFieldValueForNonCollectionObject(doc, indexedFieldName, item, tokenizedFields); |
| } |
| } else if (fieldValue instanceof Collection) { |
| Collection collection = (Collection) fieldValue; |
| for (Object item : collection) { |
| addFieldValueForNonCollectionObject(doc, indexedFieldName, item, tokenizedFields); |
| } |
| } else { |
| addFieldValueForNonCollectionObject(doc, indexedFieldName, fieldValue, tokenizedFields); |
| } |
| } |
| |
| private void addFieldValueForNonCollectionObject(Document doc, String indexedFieldName, |
| Object fieldValue, List<String> tokenizedFields) { |
| if (tokenizedFields.size() == 1) { |
| SerializerUtil.addField(doc, indexedFieldName, fieldValue); |
| } else { |
| addFieldValue(doc, indexedFieldName, fieldValue, |
| tokenizedFields.subList(1, tokenizedFields.size())); |
| } |
| } |
| |
| private Object getFieldValue(Object value, String fieldName) { |
| if (value instanceof PdxInstance) { |
| PdxInstance pdx = (PdxInstance) value; |
| Object fieldValue = null; |
| if (pdx.hasField(fieldName)) { |
| fieldValue = pdx.getField(fieldName); |
| } |
| return fieldValue; |
| } else { |
| Class<?> clazz = value.getClass(); |
| if (fieldName.equals(LuceneService.REGION_VALUE_FIELD) |
| && SerializerUtil.supportedPrimitiveTypes().contains(clazz)) { |
| return value; |
| } |
| |
| do { |
| try { |
| Field field = clazz.getDeclaredField(fieldName); |
| field.setAccessible(true); |
| return field.get(value); |
| } catch (Exception e) { |
| clazz = clazz.getSuperclass(); |
| } |
| } while (clazz != null && !clazz.equals(Object.class)); |
| return null; |
| } |
| } |
| } |