org.apache.solr.client.solrj.beans.DocumentObjectBinder.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.solr.client.solrj.beans.DocumentObjectBinder.java

Source

/*
 * 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.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<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 (" + 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 <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";
}