/* * 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 org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.util.SuppressForbidden; import java.lang.reflect.*; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.*; import java.util.regex.Pattern; import java.util.concurrent.ConcurrentHashMap; import java.nio.ByteBuffer; /** * A class to map objects to and from solr documents. * * * @since solr 1.3 */ public class DocumentObjectBinder { private final Map<Class, List<DocField>> infocache = new ConcurrentHashMap<>(); public DocumentObjectBinder() { } public <T> List<T> getBeans(Class<T> clazz, SolrDocumentList solrDocList) { List<DocField> fields = getDocFields(clazz); List<T> result = new ArrayList<>(solrDocList.size()); for (SolrDocument sdoc : solrDocList) { result.add(getBean(clazz, fields, sdoc)); } return result; } public <T> T getBean(Class<T> clazz, SolrDocument solrDoc) { return getBean(clazz, null, solrDoc); } private <T> T getBean(Class<T> clazz, List<DocField> fields, SolrDocument solrDoc) { if (fields == null) { fields = getDocFields(clazz); } try { T obj = clazz.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<DocField> 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) { Map<String, Object> mapValue = (Map<String, Object>) field.get(obj); for (Map.Entry<String, Object> 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<DocField> getDocFields(Class clazz) { List<DocField> 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<DocField> collectInfo(Class clazz) { List<DocField> fields = new ArrayList<>(); Class superClazz = clazz; List<AccessibleObject> 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<Void>) () -> { 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<DocField> child; /* * dynamic fields may use a Map based data structure to bind a given field. * if a mapping is done using, "Map<String, List<String>> foo", <code>isContainedInMap</code> * is set to <code>TRUE</code> as well as <code>isList</code> is set to <code>TRUE</code> */ 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. Must have one and only one parameter"); } 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 <code>SolrDocument</code> * * Returns <code>SolrDocument.getFieldValue</code> for regular fields, * and <code>Map<String, List<Object>></code> for a dynamic field. The key is all matching fieldName's. */ @SuppressWarnings("unchecked") private Object getFieldValue(SolrDocument solrDocument) { if (child != null) { List<SolrDocument> children = solrDocument.getChildDocuments(); if (children == null || children.isEmpty()) return null; if (isList) { ArrayList list = new ArrayList(children.size()); for (SolrDocument c : children) { list.add(getBean(type, child, c)); } return list; } else if (isArray) { Object[] arr = (Object[]) Array.newInstance(type, children.size()); for (int i = 0; i < children.size(); i++) { arr[i] = getBean(type, child, children.get(i)); } return arr; } else { return getBean(type, child, children.get(0)); } } Object fieldValue = solrDocument.getFieldValue(name); if (fieldValue != null) { //this is not a dynamic field. so return the value return fieldValue; } if (dynamicFieldNamePatternMatcher == null) { return null; } //reading dynamic field values Map<String, Object> allValuesMap = null; List allValuesList = null; if (isContainedInMap) { allValuesMap = new HashMap<>(); } else { allValuesList = new ArrayList(); } for (String field : solrDocument.getFieldNames()) { if (dynamicFieldNamePatternMatcher.matcher(field).find()) { Object val = solrDocument.getFieldValue(field); if (val == null) { continue; } if (isContainedInMap) { if (isList) { if (!(val instanceof List)) { List al = new ArrayList(); al.add(val); val = al; } } else if (isArray) { if (!(val instanceof List)) { Object[] arr = (Object[]) Array.newInstance(type, 1); arr[0] = val; val = arr; } else { val = Array.newInstance(type, ((List) val).size()); } } allValuesMap.put(field, val); } else { if (val instanceof Collection) { allValuesList.addAll((Collection) val); } else { allValuesList.add(val); } } } } if (isContainedInMap) { return allValuesMap.isEmpty() ? null : allValuesMap; } else { return allValuesList.isEmpty() ? null : allValuesList; } } <T> void inject(T obj, SolrDocument sdoc) { Object val = getFieldValue(sdoc); if(val == null) { return; } if (isArray && !isContainedInMap) { List list; if (val.getClass().isArray()) { set(obj, val); return; } else if (val instanceof List) { list = (List) val; } else { list = new ArrayList(); list.add(val); } set(obj, list.toArray((Object[]) Array.newInstance(type, list.size()))); } else if (isList && !isContainedInMap) { if (!(val instanceof List)) { List list = new ArrayList(); list.add(val); val = list; } set(obj, val); } else if (isContainedInMap) { if (val instanceof Map) { set(obj, val); } } else { set(obj, val); } } private void set(Object obj, Object v) { if (v != null && type == ByteBuffer.class && v.getClass() == byte[].class) { v = ByteBuffer.wrap((byte[]) v); } try { if (field != null) { field.set(obj, v); } else if (setter != null) { setter.invoke(obj, v); } } catch (Exception e) { throw new BindingException("Exception while setting value : " + v + " on " + (field != null ? field : setter), e); } } public Object get(final Object obj) { if (field != null) { try { return field.get(obj); } catch (Exception e) { throw new BindingException("Exception while getting value: " + field, e); } } else if (getter == null) { throw new BindingException("Missing getter for field: " + name + " -- You can only call the 'get' for fields that have a field of 'get' method"); } try { return getter.invoke(obj, (Object[]) null); } catch (Exception e) { throw new BindingException("Exception while getting value: " + getter, e); } } } public static final String DEFAULT = "#default"; }