All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.client.solrj.beans.DocumentObjectBinder Maven / Gradle / Ivy

There is a newer version: 9.5.0
Show newest version
/*
 * 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> 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.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) {
        Map mapValue = (Map) field.get(obj);

        for (Map.Entry e : mapValue.entrySet()) {
          doc.setField(e.getKey(), e.getValue(), 1.0f);
        }
      } else {
        if (field.child != null) {
          addChild(obj, field, doc);
        } else {
          doc.setField(field.name, field.get(obj), 1.0f);
        }
      }
    }
    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. 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 SolrDocument
     *
     * Returns SolrDocument.getFieldValue for regular fields,
     * and Map> for a dynamic field. The key is all matching fieldName's.
     */
    @SuppressWarnings("unchecked")
    private Object getFieldValue(SolrDocument solrDocument) {
      if (child != null) {
        List 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 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;
      }
    }

     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";
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy