content: /**
   * 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 java.lang.reflect.*;
  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.
   * 
   * @version $Id: DocumentObjectBinder.java 945270 2010-05-17 17:45:18Z rmuir $
   * @since solr 1.3
   */
  public class DocumentObjectBinder {
    private final Map<Class, List<DocField>> infocache = new ConcurrentHashMap<Class, List<DocField>>();
  
    public DocumentObjectBinder() {
    }
  
    public <T> List<T> getBeans(Class<T> clazz, SolrDocumentList solrDocList) {
      List<DocField> fields = getDocFields( clazz );
      List<T> result = new ArrayList<T>(solrDocList.size());
  
      for(int j=0;j<solrDocList.size();j++) {
        SolrDocument sdoc = solrDocList.get(j);
  	  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);
      }
      T obj = null;
      try {
        obj = clazz.newInstance();
      } catch (Exception e) {
        throw new RuntimeException("Could not instantiate object of " + clazz, e);
      }
      for (int i = 0; i < fields.size(); i++) {
        DocField docField = fields.get(i);
        docField.inject(obj, solrDoc);
      }
      return obj;
    }
    
    public SolrInputDocument toSolrInputDocument( Object obj )
    {
      List<DocField> fields = getDocFields( obj.getClass() );
      if( fields.isEmpty() ) {
        throw new RuntimeException( "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 = (HashMap<String, Object>) field
  					.get(obj);
  
  			for (Map.Entry<String, Object> e : mapValue.entrySet()) {
  				doc.setField( e.getKey(), e.getValue(), 1.0f);
  			}
  		} else {
  			doc.setField(field.name, field.get(obj), 1.0f);
  		}
  	}
      return doc;
    }
    
    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;
    }
  
    private List<DocField> collectInfo(Class clazz) {
      List<DocField> fields = new ArrayList<DocField>();
      Class superClazz = clazz;
      ArrayList<AccessibleObject> members = new ArrayList<AccessibleObject>();
      while (superClazz != null && superClazz != Object.class) {
        members.addAll(Arrays.asList(superClazz.getDeclaredFields()));
        members.addAll(Arrays.asList(superClazz.getDeclaredMethods()));
        superClazz = superClazz.getSuperclass();
      }
      for (AccessibleObject member : members) {
        if (member.isAnnotationPresent(Field.class)) {
          member.setAccessible(true);
          fields.add(new DocField(member));
        }
      }
      return fields;
    }
  
    private static class DocField {
      private String name;
      private java.lang.reflect.Field field;
      private Method setter;
      private Method getter;
      private Class type;
      private boolean isArray = false, isList=false;
  
      /*
       * 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>
       */
      boolean isContainedInMap =false;
      private Pattern dynamicFieldNamePatternMatcher;
  
      public DocField(AccessibleObject member) {
        if (member instanceof java.lang.reflect.Field) {
          field = (java.lang.reflect.Field) member;
        } else {
          setter = (Method) member;
        }
        Field 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(Field.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.ENGLISH) + setterName.substring(4);
            } else {
              name = setter.getName();
            }
          }
        }
        //dynamic fields are annotated as @Field("categories_*")
        else if(annotation.value().indexOf('*') >= 0){
          //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 RuntimeException("Invalid setter method. Must have one and only one parameter");
          type = params[0];
        }
        if(type == Collection.class || type == List.class || type == ArrayList.class) {
          type = Object.class;
          isList = true;
          /*ParameterizedType parameterizedType = null;
          if(field !=null){
            if( field.getGenericType() instanceof ParameterizedType){
              parameterizedType = (ParameterizedType) field.getGenericType();
              Type[] types = parameterizedType.getActualTypeArguments();
              if (types != null && types.length > 0) type = (Class) types[0];
            }
          }*/
        } else if(type == byte[].class){
          //no op
        }else if (type.isArray()) {
          isArray = true;
          type = type.getComponentType();
        }
        //corresponding to the support for dynamicFields
        else if (type == Map.class || type == HashMap.class) {
          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];
                  }
                }
                //Of all the Parameterized types, only List is supported
                else if(types[1] instanceof ParameterizedType){
                  Type rawType = ((ParameterizedType)types[1]).getRawType();
                  if(rawType== Collection.class || rawType == List.class || rawType == ArrayList.class){
                    type = Object.class;
                    isList = true;
                  }
                }
                //Array types
                else if(types[1] instanceof GenericArrayType){
                  type = (Class) ((GenericArrayType) types[1]).getGenericComponentType();
                  isArray = true;
                }
                //Throw an Exception if types are not known
                else{
                  throw new RuntimeException("Allowed type for values of mapping a dynamicField are : " +
                      "Object, Object[] and List");
                }
              }
            }
          }
        }
      }
  
      /**
       * 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 sdoc){
        Object fieldValue = sdoc.getFieldValue(name);
        if(fieldValue != null) {
          //this is not a dynamic field. so return te value
          return fieldValue;
        }
        //reading dynamic field values
        if(dynamicFieldNamePatternMatcher != null){
          Map<String, Object> allValuesMap = null;
          ArrayList allValuesList = null;
          if(isContainedInMap){
           allValuesMap = new HashMap<String, Object>();
          } else {
            allValuesList = new ArrayList();
          }
          for(String field : sdoc.getFieldNames()){
            if(dynamicFieldNamePatternMatcher.matcher(field).find()){
              Object val = sdoc.getFieldValue(field);
              if(val == null) continue;
              if(isContainedInMap){
                if(isList){
                  if (!(val instanceof List)) {
                    ArrayList 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;
          }
        }
        return null;
      }
      <T> void inject(T obj, SolrDocument sdoc) {
        Object val = getFieldValue(sdoc);
        if(val == null) {
          return;
        }
        if(isArray && !isContainedInMap){
          List list = null;
          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)) {
            ArrayList 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 RuntimeException("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 RuntimeException("Exception while getting value: " + field, e);
          }
        }
        else if (getter == null) {
          throw new RuntimeException( "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 RuntimeException("Exception while getting value: " + getter, e);
        }
      }
    }
  }
  