org.apache.solr.client.solrj.beans.DocumentObjectBinder Maven / Gradle / Ivy
/*
* 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.solr.client.solrj.beans;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.util.SuppressForbidden;
/**
* A class to map objects to and from solr documents.
*
* @since solr 1.3
*/
public class DocumentObjectBinder {
private final Map, List> infocache = new ConcurrentHashMap<>();
public DocumentObjectBinder() {}
public List getBeans(Class clazz, SolrDocumentList solrDocList) {
List fields = getDocFields(clazz);
List result = new ArrayList<>(solrDocList.size());
for (SolrDocument sdoc : solrDocList) {
result.add(getBean(clazz, fields, sdoc));
}
return result;
}
public T getBean(Class clazz, SolrDocument solrDoc) {
return getBean(clazz, null, solrDoc);
}
private T getBean(Class clazz, List fields, SolrDocument solrDoc) {
if (fields == null) {
fields = getDocFields(clazz);
}
try {
T obj = clazz.getConstructor().newInstance();
for (DocField docField : fields) {
docField.inject(obj, solrDoc);
}
return obj;
} catch (Exception e) {
throw new BindingException("Could not instantiate object of " + clazz, e);
}
}
public SolrInputDocument toSolrInputDocument(Object obj) {
List fields = getDocFields(obj.getClass());
if (fields.isEmpty()) {
throw new BindingException("class: " + obj.getClass() + " does not define any fields.");
}
SolrInputDocument doc = new SolrInputDocument();
for (DocField field : fields) {
if (field.dynamicFieldNamePatternMatcher != null
&& field.get(obj) != null
&& field.isContainedInMap) {
@SuppressWarnings({"unchecked"})
Map mapValue = (Map) field.get(obj);
for (Map.Entry e : mapValue.entrySet()) {
doc.setField(e.getKey(), e.getValue());
}
} else {
if (field.child != null) {
addChild(obj, field, doc);
} else {
doc.setField(field.name, field.get(obj));
}
}
}
return doc;
}
private void addChild(Object obj, DocField field, SolrInputDocument doc) {
Object val = field.get(obj);
if (val == null) return;
if (val instanceof Collection) {
Collection> collection = (Collection>) val;
for (Object o : collection) {
SolrInputDocument child = toSolrInputDocument(o);
doc.addChildDocument(child);
}
} else if (val.getClass().isArray()) {
Object[] objs = (Object[]) val;
for (Object o : objs) doc.addChildDocument(toSolrInputDocument(o));
} else {
doc.addChildDocument(toSolrInputDocument(val));
}
}
private List getDocFields(Class> clazz) {
List fields = infocache.get(clazz);
if (fields == null) {
synchronized (infocache) {
infocache.put(clazz, fields = collectInfo(clazz));
}
}
return fields;
}
@SuppressForbidden(reason = "Needs access to possibly private @Field annotated fields/methods")
private List collectInfo(Class> clazz) {
List fields = new ArrayList<>();
Class> superClazz = clazz;
List members = new ArrayList<>();
while (superClazz != null && superClazz != Object.class) {
members.addAll(Arrays.asList(superClazz.getDeclaredFields()));
members.addAll(Arrays.asList(superClazz.getDeclaredMethods()));
superClazz = superClazz.getSuperclass();
}
boolean childFieldFound = false;
for (AccessibleObject member : members) {
if (member.isAnnotationPresent(Field.class)) {
AccessController.doPrivileged(
(PrivilegedAction)
() -> {
member.setAccessible(true);
return null;
});
DocField df = new DocField(member);
if (df.child != null) {
if (childFieldFound)
throw new BindingException(
clazz.getName() + " cannot have more than one Field with child=true");
childFieldFound = true;
}
fields.add(df);
}
}
return fields;
}
private class DocField {
private Field annotation;
private String name;
private java.lang.reflect.Field field;
private Method setter;
private Method getter;
private Class> type;
private boolean isArray;
private boolean isList;
private List child;
/*
* dynamic fields may use a Map based data structure to bind a given field.
* if a mapping is done using, "Map> foo", isContainedInMap
* is set to TRUE
as well as isList
is set to TRUE
*/
private boolean isContainedInMap;
private Pattern dynamicFieldNamePatternMatcher;
public DocField(AccessibleObject member) {
if (member instanceof java.lang.reflect.Field) {
field = (java.lang.reflect.Field) member;
} else {
setter = (Method) member;
}
annotation = member.getAnnotation(Field.class);
storeName(annotation);
storeType();
// Look for a matching getter
if (setter != null) {
String gname = setter.getName();
if (gname.startsWith("set")) {
gname = "get" + gname.substring(3);
try {
getter = setter.getDeclaringClass().getMethod(gname, (Class[]) null);
} catch (Exception ex) {
// no getter -- don't worry about it...
if (type == Boolean.class) {
gname = "is" + setter.getName().substring(3);
try {
getter = setter.getDeclaringClass().getMethod(gname, (Class[]) null);
} catch (Exception ex2) {
// no getter -- don't worry about it...
}
}
}
}
}
}
private void storeName(Field annotation) {
if (annotation.value().equals(DEFAULT)) {
if (field != null) {
name = field.getName();
} else {
String setterName = setter.getName();
if (setterName.startsWith("set") && setterName.length() > 3) {
name = setterName.substring(3, 4).toLowerCase(Locale.ROOT) + setterName.substring(4);
} else {
name = setter.getName();
}
}
} else if (annotation.value().indexOf('*') >= 0) {
// dynamic fields are annotated as @Field("categories_*")
// if the field was annotated as a dynamic field, convert the name into a pattern
// the wildcard (*) is supposed to be either a prefix or a suffix, hence the use of
// replaceFirst
name = annotation.value().replaceFirst("\\*", "\\.*");
dynamicFieldNamePatternMatcher = Pattern.compile("^" + name + "$");
} else {
name = annotation.value();
}
}
private void storeType() {
if (field != null) {
type = field.getType();
} else {
Class>[] params = setter.getParameterTypes();
if (params.length != 1) {
throw new BindingException(
"Invalid setter method ("
+ setter
+ "). A setter must have one and only one parameter but we found "
+ params.length
+ " parameters.");
}
type = params[0];
}
if (type == Collection.class || type == List.class || type == ArrayList.class) {
isList = true;
if (annotation.child()) {
populateChild(field.getGenericType());
} else {
type = Object.class;
}
} else if (type == byte[].class) {
// no op
} else if (type.isArray()) {
isArray = true;
if (annotation.child()) {
populateChild(type.getComponentType());
} else {
type = type.getComponentType();
}
} else if (type == Map.class
|| type == HashMap.class) { // corresponding to the support for dynamicFields
if (annotation.child())
throw new BindingException("Map should is not a valid type for a child document");
isContainedInMap = true;
// assigned a default type
type = Object.class;
if (field != null) {
if (field.getGenericType() instanceof ParameterizedType) {
// check what are the generic values
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
Type[] types = parameterizedType.getActualTypeArguments();
if (types != null && types.length == 2 && types[0] == String.class) {
// the key should always be String
// Raw and primitive types
if (types[1] instanceof Class) {
// the value could be multivalued then it is a List, Collection, ArrayList
if (types[1] == Collection.class
|| types[1] == List.class
|| types[1] == ArrayList.class) {
type = Object.class;
isList = true;
} else {
// else assume it is a primitive and put in the source type itself
type = (Class) types[1];
}
} else if (types[1]
instanceof
ParameterizedType) { // Of all the Parameterized types, only List is supported
Type rawType = ((ParameterizedType) types[1]).getRawType();
if (rawType == Collection.class
|| rawType == List.class
|| rawType == ArrayList.class) {
type = Object.class;
isList = true;
}
} else if (types[1] instanceof GenericArrayType) { // Array types
type = (Class) ((GenericArrayType) types[1]).getGenericComponentType();
isArray = true;
} else { // Throw an Exception if types are not known
throw new BindingException(
"Allowed type for values of mapping a dynamicField are : "
+ "Object, Object[] and List");
}
}
}
}
} else {
if (annotation.child()) {
populateChild(type);
}
}
}
private void populateChild(Type typ) {
if (typ == null) {
throw new RuntimeException(
"no type information available for" + (field == null ? setter : field));
}
if (typ.getClass() == Class.class) { // of type class
type = (Class) typ;
} else if (typ instanceof ParameterizedType) {
try {
type = Class.forName(((ParameterizedType) typ).getActualTypeArguments()[0].getTypeName());
} catch (ClassNotFoundException e) {
throw new BindingException(
"Invalid type information available for" + (field == null ? setter : field));
}
} else {
throw new BindingException(
"Invalid type information available for" + (field == null ? setter : field));
}
child = getDocFields(type);
}
/**
* Called by the {@link #inject} method to read the value(s) for a field This method supports
* reading of all "matching" fieldName's in the {@link SolrDocument} Returns {@link
* SolrDocument#getFieldValue} for regular fields, and {@code Map>} for a
* dynamic field. The key is all matching fieldName's.
*/
private Object getFieldValue(SolrDocument solrDocument) {
if (child != null) {
List children = solrDocument.getChildDocuments();
if (children == null || children.isEmpty()) return null;
if (isList) {
ArrayList
© 2015 - 2025 Weber Informatics LLC | Privacy Policy