Solr
  1. Solr
  2. SOLR-1945

Allow @Field annotations in nested classes using DocumentObjectBinder

    Details

    • Type: Improvement Improvement
    • Status: Open
    • Priority: Minor Minor
    • Resolution: Unresolved
    • Affects Version/s: None
    • Fix Version/s: None
    • Component/s: None
    • Labels:
      None

      Description

      see http://search.lucidimagination.com/search/document/d909d909420aeb4e/does_solrj_support_nested_annotated_beans

      Would be nice to be able to pass an object graph to solrj with @field annotations rather than just a top level class

        Activity

        Hide
        Mark Miller added a comment -

        Started playing around with this on the plane yesterday - here is a pretty rough first draft.

        You annotate members that should be drilled into with @FieldObject (need to think of the right name here)

        eg:

          public static class ItemHolder {
            @Field
            String superfield;
            @FieldObject
            Item item;
            @FieldObject
            Item2 item2;
        
            public Item getItem() {
              return item;
            }
        
            public void setItem(Item item) {
              this.item = item;
            }
            
          }
          
          public static class Item {
            @Field
            String id;
        
            @Field("cat")
            String[] categories;
        
            @Field
            List<String> features;
        
            @Field
            Date timestamp;
        
            @Field("highway_mileage")
            int mwyMileage;
        
            boolean inStock = false;
        
            @Field("supplier_*")
            Map<String, List<String>> supplier;
            
            @Field("sup_simple_*")
            Map<String, String> supplier_simple;
        
            private String[] allSuppliers;
        
            @Field("supplier_*")
            public void setAllSuppliers(String[] allSuppliers){
              this.allSuppliers = allSuppliers;  
            }
        
            public String[] getAllSuppliers(){
              return this.allSuppliers;
            }
        
            @Field
            public void setInStock(Boolean b) {
              inStock = b;
            }
            
            // required if you want to fill SolrDocuments with the same annotaion...
            public boolean isInStock()
            {
              return inStock;
            }
          }
          
          public static class Item2 {
            @Field
            String sensai;
            
            @FieldObject
            Item3 item3;
            
          }
          
          public static class Item3 {
            @Field
            String server;
        
            @Field("type")
            String[] types;
          }
          
        
        Show
        Mark Miller added a comment - Started playing around with this on the plane yesterday - here is a pretty rough first draft. You annotate members that should be drilled into with @FieldObject (need to think of the right name here) eg: public static class ItemHolder { @Field String superfield; @FieldObject Item item; @FieldObject Item2 item2; public Item getItem() { return item; } public void setItem(Item item) { this .item = item; } } public static class Item { @Field String id; @Field( "cat" ) String [] categories; @Field List< String > features; @Field Date timestamp; @Field( "highway_mileage" ) int mwyMileage; boolean inStock = false ; @Field( "supplier_*" ) Map< String , List< String >> supplier; @Field( "sup_simple_*" ) Map< String , String > supplier_simple; private String [] allSuppliers; @Field( "supplier_*" ) public void setAllSuppliers( String [] allSuppliers){ this .allSuppliers = allSuppliers; } public String [] getAllSuppliers(){ return this .allSuppliers; } @Field public void setInStock( Boolean b) { inStock = b; } // required if you want to fill SolrDocuments with the same annotaion... public boolean isInStock() { return inStock; } } public static class Item2 { @Field String sensai; @FieldObject Item3 item3; } public static class Item3 { @Field String server; @Field( "type" ) String [] types; }
        Hide
        Paul Sheldon added a comment -

        I think you are on the right track here with @Field and @FieldObject ideas.

        I'm coming from a compass/lucene environment, getting ready to ditch compass and implement solr. compass has a mature set of annotations for things such as @SearchableProperty, @SearchableComponent, @SearchableDynamicProperty, and so on. It would be great if solr supported concepts like these as well! we have quite a large object tree for marshalling an entity-managed pojo from the db to the lucene index. it has a lot of one-to-many and many-to-many relationships (i.e. collections) that are 'flattened' out during the marshalling process. this is currently handled automagically by compass using the simple compass annotations. in my planning and design for the conversion to solr, it looks like i'm going to have to code all that 'flattening' logic myself, fetching and consolidating data from the db to a pojo, and then marshalling the pojo to solr.

        How far do you think your implementation will go? will it just marshall one-to-one object graph relationships, or will it also go so far as to handle the one-to-many and many-to-many (collections) relationships too?

        Show
        Paul Sheldon added a comment - I think you are on the right track here with @Field and @FieldObject ideas. I'm coming from a compass/lucene environment, getting ready to ditch compass and implement solr. compass has a mature set of annotations for things such as @SearchableProperty, @SearchableComponent, @SearchableDynamicProperty, and so on. It would be great if solr supported concepts like these as well! we have quite a large object tree for marshalling an entity-managed pojo from the db to the lucene index. it has a lot of one-to-many and many-to-many relationships (i.e. collections) that are 'flattened' out during the marshalling process. this is currently handled automagically by compass using the simple compass annotations. in my planning and design for the conversion to solr, it looks like i'm going to have to code all that 'flattening' logic myself, fetching and consolidating data from the db to a pojo, and then marshalling the pojo to solr. How far do you think your implementation will go? will it just marshall one-to-one object graph relationships, or will it also go so far as to handle the one-to-many and many-to-many (collections) relationships too?
        Hide
        Robert Muir added a comment -

        Bulk move 3.2 -> 3.3

        Show
        Robert Muir added a comment - Bulk move 3.2 -> 3.3
        Hide
        Monica Storfjord added a comment -

        What is the status on this proposal? It would be a great feature and very beneficial to my current project! Do you have a full solution on this and witch version do you think this feature will be released in?

        • Monica
        Show
        Monica Storfjord added a comment - What is the status on this proposal? It would be a great feature and very beneficial to my current project! Do you have a full solution on this and witch version do you think this feature will be released in? Monica
        Hide
        Mark Miller added a comment -

        hmmm...I don't remember. I'll take a look again.

        Show
        Mark Miller added a comment - hmmm...I don't remember. I'll take a look again.
        Hide
        Mark Miller added a comment -

        Yeah, I guess it depends on what you mean by full solution.

        What I have works for what it does - which is let you specify if you should dig down deeper into an object to look for more @Field annotations.

        Not really sure how we would flatten one-to-many or many-to-many relationships...other than allowing digging into objects within collections to look for more @Field annotations as well.

        Show
        Mark Miller added a comment - Yeah, I guess it depends on what you mean by full solution. What I have works for what it does - which is let you specify if you should dig down deeper into an object to look for more @Field annotations. Not really sure how we would flatten one-to-many or many-to-many relationships...other than allowing digging into objects within collections to look for more @Field annotations as well.
        Hide
        Robert Muir added a comment -

        3.4 -> 3.5

        Show
        Robert Muir added a comment - 3.4 -> 3.5
        Hide
        Hoss Man added a comment -

        bulk fixing the version info for 4.0-ALPHA and 4.0 all affected issues have "hoss20120711-bulk-40-change" in comment

        Show
        Hoss Man added a comment - bulk fixing the version info for 4.0-ALPHA and 4.0 all affected issues have "hoss20120711-bulk-40-change" in comment
        Hide
        Robert Muir added a comment -

        rmuir20120906-bulk-40-change

        Show
        Robert Muir added a comment - rmuir20120906-bulk-40-change
        Hide
        Robert Muir added a comment -

        moving all 4.0 issues not touched in a month to 4.1

        Show
        Robert Muir added a comment - moving all 4.0 issues not touched in a month to 4.1
        Hide
        Mark S added a comment -

        I thought I would just share workaround code until this issue is resolved.

        The SolrMapperForSimplePojoList class should do the following:

        • Simple List<?> -> Solr @Field Map -> Solr Document
        • Solr Document -> Solr @Field Map -> Simple List<?>

        ->

        	List<ShoppingItem> shoppingItems = ...
        

        <- ->

        public static class ShoppingList {
        
                @Field("id")
                private String id;
        
                @Field("shopping_items*")
                protected Map<String, Object> shoppingItemMap = new LinkedHashMap<String, Object>(16);
        		
        		// ...
        }
        
        public static class ShoppingItem {
        
        	private Float quantity;
        
        	private String product;
        
        	private String description;
        }
        

        <- ->

        SolrInputDocument[
            id=1,
            shopping_items[0].quantity_f=1.0,
            shopping_items[0].product_s=25W SOLDERING IRON STARTER KIT WITH DMM,
            shopping_items[0].description_s=This kit contains everything needed for basic electronics work.,
            shopping_items[1].quantity_f=2.0,
            shopping_items[1].product_s=Standard Microphone Insert,
            shopping_items[1].description_s=Replacement electret mic inserts for tape recorders, etc,
            shopping_items[2].quantity_f=1.0,
            shopping_items[2].product_s=11 DIGIT FLUORO DISPLAY,
            shopping_items[2].description_s=Vacuum fluorescent display,
            shopping_items[3].quantity_f=1.0,
            shopping_items[3].product_s=LEAD FREE SOLDER 0.71MM 200G ROLL,
            shopping_items[3].description_s=99.3% tin, 0.7% copper lead-free.
        ]
        

        <-

        Note*: Also includes workaround ordering issue outlined here: https://issues.apache.org/jira/browse/SOLR-4422

        The Workaround: SolrMapperForSimplePojoList

        package test;
        
        import java.lang.reflect.Method;
        import java.util.ArrayList;
        import java.util.Comparator;
        import java.util.LinkedHashMap;
        import java.util.List;
        import java.util.Map;
        import java.util.SortedSet;
        import java.util.TreeSet;
        
        /**
         * This will only work for a POJO that contains simple fields, such as String, Integer etc.
         * It will not work for complex field types such as a Collection, List, Map etc.
         *
         * This class exists because in SOLRJ @Field annotations cannot exist inside nested classes (Simple Types Only).
         *
         *
         * @see https://issues.apache.org/jira/browse/SOLR-1945
         * @author user
         *
         */
        public class SolrMapperForSimplePojoList {
        
            private SolrMapperForSimplePojoList(){
        
            }
        
            public static <T> Map<String, Object> convertPojoListToSolrjDynamicFieldMap(
                    List<T> pojoList, List<String> pojoFieldNames,
                    List<String> solrjFieldNameTemplates  ) {
        
                try{
        
                    Map<String, Object> solrjDynamicFieldMap = new LinkedHashMap<String, Object>();
        
                    for (int pojoIndex = 0; pojoIndex < pojoList.size(); pojoIndex++) {
        
                        T pojo = pojoList.get(pojoIndex);
        
                        for (int pojoFieldNameIndex = 0; pojoFieldNameIndex < pojoFieldNames.size(); pojoFieldNameIndex++) {
        
                            String pojoFieldName = pojoFieldNames.get(pojoFieldNameIndex);
                            Object pojoFieldObject = getFieldValue(pojo, pojoFieldName);
        
                            solrjDynamicFieldMap.put(getSolrjFieldName(pojoIndex, solrjFieldNameTemplates.get(pojoFieldNameIndex) ), pojoFieldObject);
                        }
        
                    }
        
                    return solrjDynamicFieldMap;
                }catch(Exception e){
                    throw new RuntimeException(e);
                }
            }
        
            private static String getSolrjFieldName(Integer index, String solrjFieldNameTemplate ){
                StringBuffer solrjDynamicFieldName = new StringBuffer();
        
                solrjDynamicFieldName.append(solrjFieldNameTemplate.substring(0, solrjFieldNameTemplate.indexOf("[") + 1) );
                solrjDynamicFieldName.append(index);
                solrjDynamicFieldName.append(solrjFieldNameTemplate.substring(solrjFieldNameTemplate.indexOf("]")) );
        
                return solrjDynamicFieldName.toString();
            }
        
            private static Object getFieldValue(Object pojo, String pojoFieldName){
        
        
                try {
        
        //          Field pojoField = pojo.getClass().getField(pojoFieldName);
        //          pojoField.setAccessible(true);
        //          Object pojoFieldObject = pojoField.get(pojo);
        
                    Method method = pojo.getClass().getMethod("get" + pojoFieldName.substring(0, 1).toUpperCase() + pojoFieldName.substring(1));
                    Object pojoFieldObject = method.invoke(pojo, (Object[]) null);
                    return pojoFieldObject;
                } catch (Exception e) {
                    // Ignore
                }
                return null;
        
            }
        
        
            /**
             * Unfortunately, the DocumentObjectBinder creates a HashMap for Map fields annotated with @Field, so order is lost.
             *
             * @see https://issues.apache.org/jira/browse/SOLR-4422
             *
             *
             * @see org.apache.solr.client.solrj.beans.DocumentObjectBinder
             *
             * @param solrjDynamicFieldMap
             * @param solrjFieldNameTemplates
             * @param pojoClass
             * @param pojoFieldNames
             * @return
             */
            public static <T> List<T> convertSolrjDynamicFieldMapToPojoList(
                    Map<String, Object> solrjDynamicFieldMap, List<String> solrjFieldNameTemplates,
                    Class<T> pojoClass, List<String> pojoFieldNames ){
        
                try{
        
                    solrjDynamicFieldMap = getMapSortedByIndexValue(solrjDynamicFieldMap);
        
                    List<T> pojoList = new ArrayList<T>();
        
                    int pojoIndex = 0;
                    T pojo = null;
        
        
                    for (Map.Entry<String, Object> solrjDynamicFieldMapEntry : solrjDynamicFieldMap.entrySet()) {
                        String solrjDynamicFieldMapEntryKey = solrjDynamicFieldMapEntry.getKey();
                        Object solrjDynamicFieldMapEntryValue = solrjDynamicFieldMapEntry.getValue();
        
        
                        Integer pojoIndexFound = extractIndexValue(solrjDynamicFieldMapEntryKey);
        
                        if (pojoIndexFound == null){
                            // A non expected value. (Non-Collection value)
                            // Value accidentally picked up by @Field regex, such as "shopping_items_grouping_count_i"
                            continue;
                        }
        
        
                        if (pojoList.size() == 0){
                            pojo = pojoClass.newInstance();
                            pojoList.add(pojo);
                        }
        
                        if ( pojoIndexFound > pojoIndex ){
                            pojo = pojoClass.newInstance();
                            pojoList.add(pojo);
                            pojoIndex++;
                        }
        
        
                        for (int solrjFieldNameIndex = 0; solrjFieldNameIndex < solrjFieldNameTemplates.size(); solrjFieldNameIndex++) {
        
                            String solrFieldName = getSolrjFieldName(pojoIndex, solrjFieldNameTemplates.get(solrjFieldNameIndex) );
        
                            if (solrjDynamicFieldMapEntryKey.equals(solrFieldName)){
                                String pojoFieldName = pojoFieldNames.get(solrjFieldNameIndex);
                                setFieldValue(pojo, pojoFieldName, solrjDynamicFieldMapEntryValue);
                            }
        
                        }
        
        
                    }
        
                    return pojoList;
                }catch(Exception e){
                    throw new RuntimeException(e);
                }
            }
        
            private static Integer extractIndexValue( String value ){
                int pojoIndexStart = value.indexOf("[");
                int pojoIndexEnd = value.indexOf("]");
        
                if ( pojoIndexStart == -1 || pojoIndexEnd == -1 || pojoIndexStart >= pojoIndexEnd ){
                    return null;
                }
        
                String indexStringValue =  value.substring(pojoIndexStart + 1, pojoIndexEnd );
        
                if (indexStringValue == null || indexStringValue.trim().length() == 0){
                    return null;
                }
        
                try{
                    return Integer.parseInt(indexStringValue);
                }catch(NumberFormatException e ){
                    return null;
                }
            }
        
            private static void setFieldValue(Object pojo, String pojoFieldName, Object value){
        
                try {
        
        //          Field pojoField = pojo.getClass().getField(pojoFieldName);
        //          pojoField.setAccessible(true);
        //          pojoField.set(pojo, solrjDynamicFieldMapEntryValue);
        
                    Method method = pojo.getClass().getMethod("set" + pojoFieldName.substring(0, 1).toUpperCase() + pojoFieldName.substring(1), value.getClass() );
                    method.invoke(pojo, value);
                } catch (Exception e) {
                    // Ignore
                }
        
            }
        
            private static Map<String, Object> getMapSortedByIndexValue( Map<String, Object> unsortedMap ){
        
                 final int BEFORE = -1;
        //         final int SAME = 0;  // This will result in one value replacing the other..
                 final int AFTER = 1;
        
                 Comparator<String> comparator = new Comparator<String>() {
                     public int compare(String s1, String s2) {
        
        
        //            	  BEFORE: A negative number if s1 comes before s2;
        //            	  AFTER:  A positive number if s1 comes after s2;
        //            	  SAME:   0.  Only if you want to replace a value.
        
                         Integer s1IndexValue = extractIndexValue(s1);
                         Integer s2IndexValue = extractIndexValue(s2);
        
                         final int DEFAULT = BEFORE;
        
                         if (s1IndexValue == null && s2IndexValue == null) {
                             return DEFAULT;
                         } else if (s1IndexValue != null && s2IndexValue == null) {
                             return AFTER;
                         } else if (s1IndexValue == null && s2IndexValue != null) {
                             return BEFORE;
                         } else {
        
                             if  (s1IndexValue > s2IndexValue){
                                 return AFTER;
                             }else if (s1IndexValue < s2IndexValue){
                                 return BEFORE;
                             }else{
                                 return DEFAULT;
                             }
                         }
                     }
                 };
                 SortedSet<String> sortedSet = new TreeSet<String>(comparator);
                 sortedSet.addAll(unsortedMap.keySet());
        
                 Map<String, Object> sortedMap = new LinkedHashMap<String, Object>();
                 for (String key : sortedSet) {
                     sortedMap.put(key, unsortedMap.get(key));
                 }
        
                 return sortedMap;
        
            }
        
        }
        
        

        The Test class: SolrMapperForSimplePojoListTest

        package test;
        
        import java.util.ArrayList;
        import java.util.Arrays;
        import java.util.Collections;
        import java.util.LinkedHashMap;
        import java.util.List;
        import java.util.Map;
        import java.util.Random;
        
        import org.apache.solr.client.solrj.SolrQuery;
        import org.apache.solr.client.solrj.beans.DocumentObjectBinder;
        import org.apache.solr.client.solrj.beans.Field;
        import org.apache.solr.client.solrj.impl.HttpSolrServer;
        import org.apache.solr.client.solrj.response.QueryResponse;
        import org.apache.solr.client.solrj.response.UpdateResponse;
        import org.apache.solr.client.solrj.util.ClientUtils;
        import org.apache.solr.common.SolrDocument;
        import org.apache.solr.common.SolrInputDocument;
        import org.apache.solr.common.SolrInputField;
        import org.junit.After;
        import org.junit.Assert;
        import org.junit.Before;
        import org.junit.Test;
        
        import test.HttpSolrServerTestSupport;
        
        public class SolrMapperForSimplePojoListTest {
        
            private static SolrTestClient solrTestClient = new SolrHttpTestClient();
        //    private static SolrTestClient solrTestClient = new SolrMockTestClient();
        
            private List<ShoppingItem> getDefaultShoppingItems() {
                return Arrays.asList(
                        new ShoppingItem(1F, "25W SOLDERING IRON STARTER KIT WITH DMM", "This kit contains everything needed for basic electronics work."),
                        new ShoppingItem(2F, "Standard Microphone Insert", "Replacement electret mic inserts for tape recorders, etc"),
                        new ShoppingItem(1F, "11 DIGIT FLUORO DISPLAY", "Vacuum fluorescent display"),
                        new ShoppingItem(1F, "LEAD FREE SOLDER 0.71MM 200G ROLL", "99.3% tin, 0.7% copper lead-free.")
                        );
            }
        
        
            private ShoppingList getDefaultShoppingList() {
        
                ShoppingList shoppingList = new ShoppingList();
                shoppingList.setId("1");
                List<ShoppingItem> shoppingItems = getDefaultShoppingItems();
                ShoppingItemMapper.setShoppingListItems(shoppingList, shoppingItems);
        
                return shoppingList;
        
            }
        
            private SolrInputDocument getDefaultShoppingListSolrInputDocument() {
        
                SolrInputDocument solrInputDocument = new SolrInputDocument();
        
                solrInputDocument.addField("id", "1");
                solrInputDocument.addField("shopping_items[0].quantity_f", 1.0F);
                solrInputDocument.addField("shopping_items[0].product_s", "25W SOLDERING IRON STARTER KIT WITH DMM");
                solrInputDocument.addField("shopping_items[0].description_s", "This kit contains everything needed for basic electronics work.");
                solrInputDocument.addField("shopping_items[1].quantity_f", 2.0F);
                solrInputDocument.addField("shopping_items[1].product_s", "Standard Microphone Insert");
                solrInputDocument.addField("shopping_items[1].description_s", "Replacement electret mic inserts for tape recorders ,etc");
                solrInputDocument.addField("shopping_items[2].quantity_f", 1.0F);
                solrInputDocument.addField("shopping_items[2].product_s", "11 DIGIT FLUORO DISPLAY");
                solrInputDocument.addField("shopping_items[2].description_s", "Vacuum fluorescent display");
                solrInputDocument.addField("shopping_items[3].quantity_f", 1.0F);
                solrInputDocument.addField("shopping_items[3].product_s", "LEAD FREE SOLDER 0.71MM 200G ROLL");
                solrInputDocument.addField("shopping_items[3].description_s", "99.3% tin, 0.7% copper lead-free.");
        
                return solrInputDocument;
        
            }
        
            @Before
            public void setUp() throws Exception {
                solrTestClient.deleteById("1");
            }
        
        
            @After
            public void tearDown() throws Exception {
                solrTestClient.deleteById("1");
            }
        
            @Test
            public void testSaveAndRetrieve() throws Exception {
        
                ShoppingList shoppingListIn = getDefaultShoppingList();
                ShoppingList shoppingListOut = solrTestClient.saveAndRetrieve(ShoppingList.class, shoppingListIn, true);
        
                Assert.assertEquals(shoppingListIn.getId(), shoppingListOut.getId());
        
                Assert.assertEquals(
                        ShoppingItemMapper.getShoppingListItems(shoppingListIn).toString(),
                        ShoppingItemMapper.getShoppingListItems(shoppingListOut).toString());
        
                // https://issues.apache.org/jira/browse/SOLR-4422
                Assert.assertFalse(
                        String.valueOf(shoppingListIn.getShoppingItemMap()).equals(
                        String.valueOf(shoppingListOut.getShoppingItemMap())));
        
                System.out.println("shoppingListIn[" + String.valueOf(shoppingListIn.getShoppingItemMap()) + "]");
                System.out.println("shoppingListOut[" +String.valueOf(shoppingListOut.getShoppingItemMap()) + "]");
        
                Assert.assertFalse(
                        String.valueOf(String.valueOf(shoppingListIn.getShoppingItemMap())).equals(
                        String.valueOf(String.valueOf(shoppingListOut.getShoppingItemMap()))));
        
        
                Assert.assertFalse(shoppingListIn.toString().equals(shoppingListOut.toString()));
            }
        
            @Test
            public void testDetailedMapperOrdering() throws Exception {
        
                SolrInputDocument solrInputDocument = getDefaultShoppingListSolrInputDocument();
                StringBuffer solrInputDocumentString = new StringBuffer();
        
                {
                    for (Map.Entry<String, SolrInputField> solrInputDocumentMapEntry : solrInputDocument.entrySet()) {
        
                        if ("id".equals(solrInputDocumentMapEntry.getKey())) {
                            continue;
                        }
        
                        solrInputDocumentString.append(solrInputDocumentMapEntry.getKey())
                            .append("=\"").append(solrInputDocumentMapEntry.getValue().getValue()).append("\", ");
                    }
                }
        
        
                // RELOAD
                ShoppingList shoppingListOut = solrTestClient.saveAndRetrieve(ShoppingList.class, solrInputDocument, true);
        
                List<ShoppingItem> shoppingItemsOut = ShoppingItemMapper.getShoppingListItems(shoppingListOut);
                StringBuffer shoppingItemsOutString = new StringBuffer();
        
                {
                    for (int i = 0; i < shoppingItemsOut.size(); i++) {
        
                        ShoppingItem shoppingItem = shoppingItemsOut.get(i);
        
                        // This order is defined in ShoppingItemMapper
                        // Note:  Arrays.asList("quantity", "product", "description")
        
                        shoppingItemsOutString.append("shopping_items[" + i + "].quantity_f")
                            .append("=\"").append(shoppingItem.getQuantity()).append("\", ");
        
                        shoppingItemsOutString.append("shopping_items[" + i + "].product_s")
                            .append("=\"").append(shoppingItem.getProduct()).append("\", ");
        
                        shoppingItemsOutString.append("shopping_items[" + i + "].description_s")
                            .append("=\"").append(shoppingItem.getDescription()).append("\", ");
                    }
                }
        
        
                Assert.assertEquals(solrInputDocumentString.toString(), shoppingItemsOutString.toString());
        
            }
        
        
        //    private static void assertTrue(Boolean value) {
        //        if (value == null || !value) {
        //            throw new AssertionError();
        //        }
        //    }
        //
        //    private static void assertEquals(Object expected, Object actual) {
        //        if (expected == null && actual == null) {
        //            return;
        //        }
        //
        //        if (expected == null && actual != null) {
        //            throw new AssertionError();
        //        }
        //
        //        if (!expected.equals(actual)) {
        //            throw new AssertionError();
        //        }
        //    }
        
        
            public static class ShoppingList {
        
                @Field("id")
                private String id;
        
                @Field("shopping_items*")
                protected Map<String, Object> shoppingItemMap = new LinkedHashMap<String, Object>(16);
        
                public String getId() {
                    return id;
                }
        
                public void setId(String id) {
                    this.id = id;
                }
        
                public Map<String, Object> getShoppingItemMap() {
                    return shoppingItemMap;
                }
        
                public void setShoppingItemMap(Map<String, Object> shoppingItemMap) {
                    this.shoppingItemMap = shoppingItemMap;
                }
        
                @Override
                public String toString() {
                    return "ShoppingList [id=" + id + ", shoppingItemMap=" + shoppingItemMap + "]";
                }
        
            }
        
            public static class ShoppingItem {
        
                private Float quantity;
        
                private String product;
        
                private String description;
        
        
                public ShoppingItem(){
        
                }
        
                public ShoppingItem(Float quantity, String product, String description) {
                    super();
                    this.quantity = quantity;
                    this.product = product;
                    this.description = description;
                }
        
                public Float getQuantity() {
                    return quantity;
                }
        
                public void setQuantity(Float quantity) {
                    this.quantity = quantity;
                }
        
                public String getProduct() {
                    return product;
                }
        
                public void setProduct(String product) {
                    this.product = product;
                }
        
                public String getDescription() {
                    return description;
                }
        
                public void setDescription(String description) {
                    this.description = description;
                }
        
                @Override
                public String toString() {
                    return "ShoppingItem [quantity=" + quantity + ", product=" + product + ", description=" + description + "]";
                }
        
            }
        
            public static class ShoppingItemMapper {
        
                public static void setShoppingListItems(ShoppingList shoppingList, List<ShoppingItem> shoppingItems) {
        
                    Map<String, Object> solrjDynamicFieldMap = SolrMapperForSimplePojoList.convertPojoListToSolrjDynamicFieldMap(
                            shoppingItems, Arrays.asList("quantity", "product", "description"),
                            Arrays.asList("shopping_items[].quantity_f", "shopping_items[].product_s", "shopping_items[].description_s"));
        
                    shoppingList.setShoppingItemMap(solrjDynamicFieldMap);
                }
        
                public static List<ShoppingItem> getShoppingListItems(ShoppingList shoppingList) {
        
                    Map<String, Object> solrjDynamicFieldMap = shoppingList.getShoppingItemMap();
                    Assert.assertTrue(solrjDynamicFieldMap != null);
        
                    List<ShoppingItem> shoppingItems = SolrMapperForSimplePojoList.convertSolrjDynamicFieldMapToPojoList(
                        solrjDynamicFieldMap, Arrays.asList("shopping_items[].quantity_f", "shopping_items[].product_s", "shopping_items[].description_s") ,
                        ShoppingItem.class, Arrays.asList("quantity", "product", "description") );
        
                    return shoppingItems;
                }
            }
        
            public static interface SolrTestClient {
        
                public void deleteById(String id) throws Exception;
        
                public <T> T saveAndRetrieve(Class<T> type, T beanIn, boolean shuffleSolrInputFields) throws Exception;
        
                public <T> T saveAndRetrieve(Class<T> type, SolrInputDocument solrInputDocument, boolean shuffleSolrInputFields) throws Exception;
            }
        
            public static class SolrHttpTestClient implements SolrTestClient {
        
                private static HttpSolrServer server = HttpSolrServerTestSupport.getInstance().getServer();
        
                public void deleteById(String id) throws Exception {
        
                     UpdateResponse updateResponse = server.deleteById(id);
                     Assert.assertEquals(0, updateResponse.getStatus());
        
                     server.commit();
                }
        
                public <T> T saveAndRetrieve(Class<T> type, T beanIn, boolean shuffleSolrInputFields) throws Exception {
        
                    String docId = null;
        
                    if (ShoppingList.class.isAssignableFrom(type)) {
                        docId = ((ShoppingList) beanIn).getId();
                    }
        
                    if (docId == null) {
                        return null;
                    }
        
                    UpdateResponse updateResponse = server.addBean(beanIn);
                    Assert.assertEquals(0, updateResponse.getStatus());
                    server.commit();
        
                    return getBeanById(type, docId);
        
                }
        
                public <T> T saveAndRetrieve(Class<T> type, SolrInputDocument solrInputDocument, boolean shuffleSolrInputFields) throws Exception {
        
                    String docId = String.valueOf(solrInputDocument.get("id").getValue());
        
                    UpdateResponse updateResponse = server.add(solrInputDocument);
                    Assert.assertEquals(0, updateResponse.getStatus());
                    server.commit();
        
                    return getBeanById(type, docId);
                }
        
                private <T> T getBeanById(Class<T> type, String id) throws Exception {
        
                    SolrQuery solrQuery = new SolrQuery();
                    solrQuery.setQuery("*:*");
                    solrQuery.addFilterQuery("id:" + id);
                    QueryResponse queryResponse = server.query(solrQuery);
                    Assert.assertEquals(0, queryResponse.getStatus());
        
                    List<T> beanOutList = queryResponse.getBeans(type);
        
                    if (beanOutList == null || beanOutList.size() == 0) {
                        return null;
                    }
        
                    T beanOut = beanOutList.get(0);
        
                    return beanOut;
                }
        
            }
        
            public static class SolrMockTestClient implements SolrTestClient {
        
        
                public void deleteById(String id) throws Exception {
                    // Do nothing - in memory only
                }
        
                /**
                 * Should return the same results as using a Solr instance directly.
                 *
                 * @param solrInputDocument
                 * @return
                 */
                public <T> T saveAndRetrieve(Class<T> type, T beanIn, boolean shuffleSolrInputFields) {
        
                    DocumentObjectBinder documentObjectBinder = new DocumentObjectBinder();
        
                    // Store Pojo
        
                    SolrInputDocument solrInputDocument = documentObjectBinder.toSolrInputDocument(beanIn);
        
                    /*
                     * SolrInputDocument[
                     *     id=1,
                     *     shopping_items[0].quantity_f=1.0,
                     *     shopping_items[0].product_s=25W SOLDERING IRON STARTER KIT WITH DMM,
                     *     shopping_items[0].description_s=This kit contains everything needed for basic electronics work.,
                     *     shopping_items[1].quantity_f=2.0,
                     *     shopping_items[1].product_s=Standard Microphone Insert,
                     *     shopping_items[1].description_s=Replacement electret mic inserts for tape recorders, etc,
                     *     shopping_items[2].quantity_f=1.0,
                     *     shopping_items[2].product_s=11 DIGIT FLUORO DISPLAY,
                     *     shopping_items[2].description_s=Vacuum fluorescent display,
                     *     shopping_items[3].quantity_f=1.0,
                     *     shopping_items[3].product_s=LEAD FREE SOLDER 0.71MM 200G ROLL,
                     *     shopping_items[3].description_s=99.3% tin, 0.7% copper lead-free.]
                     *
                     *
                     */
        
                    // Retrieve Pojo
        //            SolrDocument solrDocument = ClientUtils.toSolrDocument(solrInputDocument); // (Slightly Mocked)
        //            T beanOut = documentObjectBinder.getBean(type, solrDocument);
        
                    T beanOut = saveAndRetrieve(type, solrInputDocument, shuffleSolrInputFields);
        
                    return beanOut;
                }
        
                public <T> T saveAndRetrieve(Class<T> type, SolrInputDocument solrInputDocument, boolean shuffleSolrInputFields) {
        
                    DocumentObjectBinder documentObjectBinder = new DocumentObjectBinder();
        
                    SolrDocument solrDocument;
        
                    if (shuffleSolrInputFields) {
                        // Shuffle
                        // Unfortunately, the DocumentObjectBinder creates a HashMap for Map fields annotated with @Field, so order is lost.
                        // https://issues.apache.org/jira/browse/SOLR-4422
                        // Replicate this behaviour below.
        
                        SolrInputDocument solrInputDocumentShuffled = new SolrInputDocument();
                        {
                            List<SolrInputField> solrInputFieldList = new ArrayList<SolrInputField>(solrInputDocument.values());
        
                            long seed = System.nanoTime();
                            Collections.shuffle(solrInputFieldList, new Random(seed));
        
                            for (SolrInputField solrInputField : solrInputFieldList) {
                                solrInputDocumentShuffled.addField(solrInputField.getName(), solrInputField.getValue());
                            }
                        }
        
                        solrDocument = ClientUtils.toSolrDocument(solrInputDocumentShuffled); // (Slightly Mocked)
                    } else {
                        solrDocument = ClientUtils.toSolrDocument(solrInputDocument); // (Slightly Mocked)
                    }
        
        
                    T beanOut = documentObjectBinder.getBean(type, solrDocument);
        
                    return beanOut;
        
                }
        
            }
        }
        
        
        Show
        Mark S added a comment - I thought I would just share workaround code until this issue is resolved. The SolrMapperForSimplePojoList class should do the following: Simple List<?> -> Solr @Field Map -> Solr Document Solr Document -> Solr @Field Map -> Simple List<?> -> List<ShoppingItem> shoppingItems = ... <- -> public static class ShoppingList { @Field( "id" ) private String id; @Field( "shopping_items*" ) protected Map< String , Object > shoppingItemMap = new LinkedHashMap< String , Object >(16); // ... } public static class ShoppingItem { private Float quantity; private String product; private String description; } <- -> SolrInputDocument[ id=1, shopping_items[0].quantity_f=1.0, shopping_items[0].product_s=25W SOLDERING IRON STARTER KIT WITH DMM, shopping_items[0].description_s=This kit contains everything needed for basic electronics work., shopping_items[1].quantity_f=2.0, shopping_items[1].product_s=Standard Microphone Insert, shopping_items[1].description_s=Replacement electret mic inserts for tape recorders, etc, shopping_items[2].quantity_f=1.0, shopping_items[2].product_s=11 DIGIT FLUORO DISPLAY, shopping_items[2].description_s=Vacuum fluorescent display, shopping_items[3].quantity_f=1.0, shopping_items[3].product_s=LEAD FREE SOLDER 0.71MM 200G ROLL, shopping_items[3].description_s=99.3% tin, 0.7% copper lead-free. ] <- Note*: Also includes workaround ordering issue outlined here: https://issues.apache.org/jira/browse/SOLR-4422 The Workaround: SolrMapperForSimplePojoList package test; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; /** * This will only work for a POJO that contains simple fields, such as String , Integer etc. * It will not work for complex field types such as a Collection, List, Map etc. * * This class exists because in SOLRJ @Field annotations cannot exist inside nested classes (Simple Types Only). * * * @see https: //issues.apache.org/jira/browse/SOLR-1945 * @author user * */ public class SolrMapperForSimplePojoList { private SolrMapperForSimplePojoList(){ } public static <T> Map< String , Object > convertPojoListToSolrjDynamicFieldMap( List<T> pojoList, List< String > pojoFieldNames, List< String > solrjFieldNameTemplates ) { try { Map< String , Object > solrjDynamicFieldMap = new LinkedHashMap< String , Object >(); for ( int pojoIndex = 0; pojoIndex < pojoList.size(); pojoIndex++) { T pojo = pojoList.get(pojoIndex); for ( int pojoFieldNameIndex = 0; pojoFieldNameIndex < pojoFieldNames.size(); pojoFieldNameIndex++) { String pojoFieldName = pojoFieldNames.get(pojoFieldNameIndex); Object pojoFieldObject = getFieldValue(pojo, pojoFieldName); solrjDynamicFieldMap.put(getSolrjFieldName(pojoIndex, solrjFieldNameTemplates.get(pojoFieldNameIndex) ), pojoFieldObject); } } return solrjDynamicFieldMap; } catch (Exception e){ throw new RuntimeException(e); } } private static String getSolrjFieldName( Integer index, String solrjFieldNameTemplate ){ StringBuffer solrjDynamicFieldName = new StringBuffer (); solrjDynamicFieldName.append(solrjFieldNameTemplate.substring(0, solrjFieldNameTemplate.indexOf( "[" ) + 1) ); solrjDynamicFieldName.append(index); solrjDynamicFieldName.append(solrjFieldNameTemplate.substring(solrjFieldNameTemplate.indexOf( "]" )) ); return solrjDynamicFieldName.toString(); } private static Object getFieldValue( Object pojo, String pojoFieldName){ try { // Field pojoField = pojo.getClass().getField(pojoFieldName); // pojoField.setAccessible( true ); // Object pojoFieldObject = pojoField.get(pojo); Method method = pojo.getClass().getMethod( "get" + pojoFieldName.substring(0, 1).toUpperCase() + pojoFieldName.substring(1)); Object pojoFieldObject = method.invoke(pojo, ( Object []) null ); return pojoFieldObject; } catch (Exception e) { // Ignore } return null ; } /** * Unfortunately, the DocumentObjectBinder creates a HashMap for Map fields annotated with @Field, so order is lost. * * @see https: //issues.apache.org/jira/browse/SOLR-4422 * * * @see org.apache.solr.client.solrj.beans.DocumentObjectBinder * * @param solrjDynamicFieldMap * @param solrjFieldNameTemplates * @param pojoClass * @param pojoFieldNames * @ return */ public static <T> List<T> convertSolrjDynamicFieldMapToPojoList( Map< String , Object > solrjDynamicFieldMap, List< String > solrjFieldNameTemplates, Class <T> pojoClass, List< String > pojoFieldNames ){ try { solrjDynamicFieldMap = getMapSortedByIndexValue(solrjDynamicFieldMap); List<T> pojoList = new ArrayList<T>(); int pojoIndex = 0; T pojo = null ; for (Map.Entry< String , Object > solrjDynamicFieldMapEntry : solrjDynamicFieldMap.entrySet()) { String solrjDynamicFieldMapEntryKey = solrjDynamicFieldMapEntry.getKey(); Object solrjDynamicFieldMapEntryValue = solrjDynamicFieldMapEntry.getValue(); Integer pojoIndexFound = extractIndexValue(solrjDynamicFieldMapEntryKey); if (pojoIndexFound == null ){ // A non expected value. (Non-Collection value) // Value accidentally picked up by @Field regex, such as "shopping_items_grouping_count_i" continue ; } if (pojoList.size() == 0){ pojo = pojoClass.newInstance(); pojoList.add(pojo); } if ( pojoIndexFound > pojoIndex ){ pojo = pojoClass.newInstance(); pojoList.add(pojo); pojoIndex++; } for ( int solrjFieldNameIndex = 0; solrjFieldNameIndex < solrjFieldNameTemplates.size(); solrjFieldNameIndex++) { String solrFieldName = getSolrjFieldName(pojoIndex, solrjFieldNameTemplates.get(solrjFieldNameIndex) ); if (solrjDynamicFieldMapEntryKey.equals(solrFieldName)){ String pojoFieldName = pojoFieldNames.get(solrjFieldNameIndex); setFieldValue(pojo, pojoFieldName, solrjDynamicFieldMapEntryValue); } } } return pojoList; } catch (Exception e){ throw new RuntimeException(e); } } private static Integer extractIndexValue( String value ){ int pojoIndexStart = value.indexOf( "[" ); int pojoIndexEnd = value.indexOf( "]" ); if ( pojoIndexStart == -1 || pojoIndexEnd == -1 || pojoIndexStart >= pojoIndexEnd ){ return null ; } String indexStringValue = value.substring(pojoIndexStart + 1, pojoIndexEnd ); if (indexStringValue == null || indexStringValue.trim().length() == 0){ return null ; } try { return Integer .parseInt(indexStringValue); } catch (NumberFormatException e ){ return null ; } } private static void setFieldValue( Object pojo, String pojoFieldName, Object value){ try { // Field pojoField = pojo.getClass().getField(pojoFieldName); // pojoField.setAccessible( true ); // pojoField.set(pojo, solrjDynamicFieldMapEntryValue); Method method = pojo.getClass().getMethod( "set" + pojoFieldName.substring(0, 1).toUpperCase() + pojoFieldName.substring(1), value.getClass() ); method.invoke(pojo, value); } catch (Exception e) { // Ignore } } private static Map< String , Object > getMapSortedByIndexValue( Map< String , Object > unsortedMap ){ final int BEFORE = -1; // final int SAME = 0; // This will result in one value replacing the other.. final int AFTER = 1; Comparator< String > comparator = new Comparator< String >() { public int compare( String s1, String s2) { // BEFORE: A negative number if s1 comes before s2; // AFTER: A positive number if s1 comes after s2; // SAME: 0. Only if you want to replace a value. Integer s1IndexValue = extractIndexValue(s1); Integer s2IndexValue = extractIndexValue(s2); final int DEFAULT = BEFORE; if (s1IndexValue == null && s2IndexValue == null ) { return DEFAULT; } else if (s1IndexValue != null && s2IndexValue == null ) { return AFTER; } else if (s1IndexValue == null && s2IndexValue != null ) { return BEFORE; } else { if (s1IndexValue > s2IndexValue){ return AFTER; } else if (s1IndexValue < s2IndexValue){ return BEFORE; } else { return DEFAULT; } } } }; SortedSet< String > sortedSet = new TreeSet< String >(comparator); sortedSet.addAll(unsortedMap.keySet()); Map< String , Object > sortedMap = new LinkedHashMap< String , Object >(); for ( String key : sortedSet) { sortedMap.put(key, unsortedMap.get(key)); } return sortedMap; } } The Test class: SolrMapperForSimplePojoListTest package test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Random; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.beans.DocumentObjectBinder; import org.apache.solr.client.solrj.beans.Field; import org.apache.solr.client.solrj.impl.HttpSolrServer; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.client.solrj.response.UpdateResponse; import org.apache.solr.client.solrj.util.ClientUtils; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.SolrInputField; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import test.HttpSolrServerTestSupport; public class SolrMapperForSimplePojoListTest { private static SolrTestClient solrTestClient = new SolrHttpTestClient(); // private static SolrTestClient solrTestClient = new SolrMockTestClient(); private List<ShoppingItem> getDefaultShoppingItems() { return Arrays.asList( new ShoppingItem(1F, "25W SOLDERING IRON STARTER KIT WITH DMM" , "This kit contains everything needed for basic electronics work." ), new ShoppingItem(2F, "Standard Microphone Insert" , "Replacement electret mic inserts for tape recorders, etc" ), new ShoppingItem(1F, "11 DIGIT FLUORO DISPLAY" , "Vacuum fluorescent display" ), new ShoppingItem(1F, "LEAD FREE SOLDER 0.71MM 200G ROLL" , "99.3% tin, 0.7% copper lead-free." ) ); } private ShoppingList getDefaultShoppingList() { ShoppingList shoppingList = new ShoppingList(); shoppingList.setId( "1" ); List<ShoppingItem> shoppingItems = getDefaultShoppingItems(); ShoppingItemMapper.setShoppingListItems(shoppingList, shoppingItems); return shoppingList; } private SolrInputDocument getDefaultShoppingListSolrInputDocument() { SolrInputDocument solrInputDocument = new SolrInputDocument(); solrInputDocument.addField( "id" , "1" ); solrInputDocument.addField( "shopping_items[0].quantity_f" , 1.0F); solrInputDocument.addField( "shopping_items[0].product_s" , "25W SOLDERING IRON STARTER KIT WITH DMM" ); solrInputDocument.addField( "shopping_items[0].description_s" , "This kit contains everything needed for basic electronics work." ); solrInputDocument.addField( "shopping_items[1].quantity_f" , 2.0F); solrInputDocument.addField( "shopping_items[1].product_s" , "Standard Microphone Insert" ); solrInputDocument.addField( "shopping_items[1].description_s" , "Replacement electret mic inserts for tape recorders ,etc" ); solrInputDocument.addField( "shopping_items[2].quantity_f" , 1.0F); solrInputDocument.addField( "shopping_items[2].product_s" , "11 DIGIT FLUORO DISPLAY" ); solrInputDocument.addField( "shopping_items[2].description_s" , "Vacuum fluorescent display" ); solrInputDocument.addField( "shopping_items[3].quantity_f" , 1.0F); solrInputDocument.addField( "shopping_items[3].product_s" , "LEAD FREE SOLDER 0.71MM 200G ROLL" ); solrInputDocument.addField( "shopping_items[3].description_s" , "99.3% tin, 0.7% copper lead-free." ); return solrInputDocument; } @Before public void setUp() throws Exception { solrTestClient.deleteById( "1" ); } @After public void tearDown() throws Exception { solrTestClient.deleteById( "1" ); } @Test public void testSaveAndRetrieve() throws Exception { ShoppingList shoppingListIn = getDefaultShoppingList(); ShoppingList shoppingListOut = solrTestClient.saveAndRetrieve(ShoppingList.class, shoppingListIn, true ); Assert.assertEquals(shoppingListIn.getId(), shoppingListOut.getId()); Assert.assertEquals( ShoppingItemMapper.getShoppingListItems(shoppingListIn).toString(), ShoppingItemMapper.getShoppingListItems(shoppingListOut).toString()); // https://issues.apache.org/jira/browse/SOLR-4422 Assert.assertFalse( String .valueOf(shoppingListIn.getShoppingItemMap()).equals( String .valueOf(shoppingListOut.getShoppingItemMap()))); System .out.println( "shoppingListIn[" + String .valueOf(shoppingListIn.getShoppingItemMap()) + "]" ); System .out.println( "shoppingListOut[" + String .valueOf(shoppingListOut.getShoppingItemMap()) + "]" ); Assert.assertFalse( String .valueOf( String .valueOf(shoppingListIn.getShoppingItemMap())).equals( String .valueOf( String .valueOf(shoppingListOut.getShoppingItemMap())))); Assert.assertFalse(shoppingListIn.toString().equals(shoppingListOut.toString())); } @Test public void testDetailedMapperOrdering() throws Exception { SolrInputDocument solrInputDocument = getDefaultShoppingListSolrInputDocument(); StringBuffer solrInputDocumentString = new StringBuffer (); { for (Map.Entry< String , SolrInputField> solrInputDocumentMapEntry : solrInputDocument.entrySet()) { if ( "id" .equals(solrInputDocumentMapEntry.getKey())) { continue ; } solrInputDocumentString.append(solrInputDocumentMapEntry.getKey()) .append( "=\" ").append(solrInputDocumentMapEntry.getValue().getValue()).append(" \ ", " ); } } // RELOAD ShoppingList shoppingListOut = solrTestClient.saveAndRetrieve(ShoppingList.class, solrInputDocument, true ); List<ShoppingItem> shoppingItemsOut = ShoppingItemMapper.getShoppingListItems(shoppingListOut); StringBuffer shoppingItemsOutString = new StringBuffer (); { for ( int i = 0; i < shoppingItemsOut.size(); i++) { ShoppingItem shoppingItem = shoppingItemsOut.get(i); // This order is defined in ShoppingItemMapper // Note: Arrays.asList( "quantity" , "product" , "description" ) shoppingItemsOutString.append( "shopping_items[" + i + "].quantity_f" ) .append( "=\" ").append(shoppingItem.getQuantity()).append(" \ ", " ); shoppingItemsOutString.append( "shopping_items[" + i + "].product_s" ) .append( "=\" ").append(shoppingItem.getProduct()).append(" \ ", " ); shoppingItemsOutString.append( "shopping_items[" + i + "].description_s" ) .append( "=\" ").append(shoppingItem.getDescription()).append(" \ ", " ); } } Assert.assertEquals(solrInputDocumentString.toString(), shoppingItemsOutString.toString()); } // private static void assertTrue( Boolean value) { // if (value == null || !value) { // throw new AssertionError(); // } // } // // private static void assertEquals( Object expected, Object actual) { // if (expected == null && actual == null ) { // return ; // } // // if (expected == null && actual != null ) { // throw new AssertionError(); // } // // if (!expected.equals(actual)) { // throw new AssertionError(); // } // } public static class ShoppingList { @Field( "id" ) private String id; @Field( "shopping_items*" ) protected Map< String , Object > shoppingItemMap = new LinkedHashMap< String , Object >(16); public String getId() { return id; } public void setId( String id) { this .id = id; } public Map< String , Object > getShoppingItemMap() { return shoppingItemMap; } public void setShoppingItemMap(Map< String , Object > shoppingItemMap) { this .shoppingItemMap = shoppingItemMap; } @Override public String toString() { return "ShoppingList [id=" + id + ", shoppingItemMap=" + shoppingItemMap + "]" ; } } public static class ShoppingItem { private Float quantity; private String product; private String description; public ShoppingItem(){ } public ShoppingItem( Float quantity, String product, String description) { super (); this .quantity = quantity; this .product = product; this .description = description; } public Float getQuantity() { return quantity; } public void setQuantity( Float quantity) { this .quantity = quantity; } public String getProduct() { return product; } public void setProduct( String product) { this .product = product; } public String getDescription() { return description; } public void setDescription( String description) { this .description = description; } @Override public String toString() { return "ShoppingItem [quantity=" + quantity + ", product=" + product + ", description=" + description + "]" ; } } public static class ShoppingItemMapper { public static void setShoppingListItems(ShoppingList shoppingList, List<ShoppingItem> shoppingItems) { Map< String , Object > solrjDynamicFieldMap = SolrMapperForSimplePojoList.convertPojoListToSolrjDynamicFieldMap( shoppingItems, Arrays.asList( "quantity" , "product" , "description" ), Arrays.asList( "shopping_items[].quantity_f" , "shopping_items[].product_s" , "shopping_items[].description_s" )); shoppingList.setShoppingItemMap(solrjDynamicFieldMap); } public static List<ShoppingItem> getShoppingListItems(ShoppingList shoppingList) { Map< String , Object > solrjDynamicFieldMap = shoppingList.getShoppingItemMap(); Assert.assertTrue(solrjDynamicFieldMap != null ); List<ShoppingItem> shoppingItems = SolrMapperForSimplePojoList.convertSolrjDynamicFieldMapToPojoList( solrjDynamicFieldMap, Arrays.asList( "shopping_items[].quantity_f" , "shopping_items[].product_s" , "shopping_items[].description_s" ) , ShoppingItem.class, Arrays.asList( "quantity" , "product" , "description" ) ); return shoppingItems; } } public static interface SolrTestClient { public void deleteById( String id) throws Exception; public <T> T saveAndRetrieve( Class <T> type, T beanIn, boolean shuffleSolrInputFields) throws Exception; public <T> T saveAndRetrieve( Class <T> type, SolrInputDocument solrInputDocument, boolean shuffleSolrInputFields) throws Exception; } public static class SolrHttpTestClient implements SolrTestClient { private static HttpSolrServer server = HttpSolrServerTestSupport.getInstance().getServer(); public void deleteById( String id) throws Exception { UpdateResponse updateResponse = server.deleteById(id); Assert.assertEquals(0, updateResponse.getStatus()); server.commit(); } public <T> T saveAndRetrieve( Class <T> type, T beanIn, boolean shuffleSolrInputFields) throws Exception { String docId = null ; if (ShoppingList.class.isAssignableFrom(type)) { docId = ((ShoppingList) beanIn).getId(); } if (docId == null ) { return null ; } UpdateResponse updateResponse = server.addBean(beanIn); Assert.assertEquals(0, updateResponse.getStatus()); server.commit(); return getBeanById(type, docId); } public <T> T saveAndRetrieve( Class <T> type, SolrInputDocument solrInputDocument, boolean shuffleSolrInputFields) throws Exception { String docId = String .valueOf(solrInputDocument.get( "id" ).getValue()); UpdateResponse updateResponse = server.add(solrInputDocument); Assert.assertEquals(0, updateResponse.getStatus()); server.commit(); return getBeanById(type, docId); } private <T> T getBeanById( Class <T> type, String id) throws Exception { SolrQuery solrQuery = new SolrQuery(); solrQuery.setQuery( "*:*" ); solrQuery.addFilterQuery( "id:" + id); QueryResponse queryResponse = server.query(solrQuery); Assert.assertEquals(0, queryResponse.getStatus()); List<T> beanOutList = queryResponse.getBeans(type); if (beanOutList == null || beanOutList.size() == 0) { return null ; } T beanOut = beanOutList.get(0); return beanOut; } } public static class SolrMockTestClient implements SolrTestClient { public void deleteById( String id) throws Exception { // Do nothing - in memory only } /** * Should return the same results as using a Solr instance directly. * * @param solrInputDocument * @ return */ public <T> T saveAndRetrieve( Class <T> type, T beanIn, boolean shuffleSolrInputFields) { DocumentObjectBinder documentObjectBinder = new DocumentObjectBinder(); // Store Pojo SolrInputDocument solrInputDocument = documentObjectBinder.toSolrInputDocument(beanIn); /* * SolrInputDocument[ * id=1, * shopping_items[0].quantity_f=1.0, * shopping_items[0].product_s=25W SOLDERING IRON STARTER KIT WITH DMM, * shopping_items[0].description_s=This kit contains everything needed for basic electronics work., * shopping_items[1].quantity_f=2.0, * shopping_items[1].product_s=Standard Microphone Insert, * shopping_items[1].description_s=Replacement electret mic inserts for tape recorders, etc, * shopping_items[2].quantity_f=1.0, * shopping_items[2].product_s=11 DIGIT FLUORO DISPLAY, * shopping_items[2].description_s=Vacuum fluorescent display, * shopping_items[3].quantity_f=1.0, * shopping_items[3].product_s=LEAD FREE SOLDER 0.71MM 200G ROLL, * shopping_items[3].description_s=99.3% tin, 0.7% copper lead-free.] * * */ // Retrieve Pojo // SolrDocument solrDocument = ClientUtils.toSolrDocument(solrInputDocument); // (Slightly Mocked) // T beanOut = documentObjectBinder.getBean(type, solrDocument); T beanOut = saveAndRetrieve(type, solrInputDocument, shuffleSolrInputFields); return beanOut; } public <T> T saveAndRetrieve( Class <T> type, SolrInputDocument solrInputDocument, boolean shuffleSolrInputFields) { DocumentObjectBinder documentObjectBinder = new DocumentObjectBinder(); SolrDocument solrDocument; if (shuffleSolrInputFields) { // Shuffle // Unfortunately, the DocumentObjectBinder creates a HashMap for Map fields annotated with @Field, so order is lost. // https://issues.apache.org/jira/browse/SOLR-4422 // Replicate this behaviour below. SolrInputDocument solrInputDocumentShuffled = new SolrInputDocument(); { List<SolrInputField> solrInputFieldList = new ArrayList<SolrInputField>(solrInputDocument.values()); long seed = System .nanoTime(); Collections.shuffle(solrInputFieldList, new Random(seed)); for (SolrInputField solrInputField : solrInputFieldList) { solrInputDocumentShuffled.addField(solrInputField.getName(), solrInputField.getValue()); } } solrDocument = ClientUtils.toSolrDocument(solrInputDocumentShuffled); // (Slightly Mocked) } else { solrDocument = ClientUtils.toSolrDocument(solrInputDocument); // (Slightly Mocked) } T beanOut = documentObjectBinder.getBean(type, solrDocument); return beanOut; } } }

          People

          • Assignee:
            Unassigned
            Reporter:
            Mark Miller
          • Votes:
            10 Vote for this issue
            Watchers:
            11 Start watching this issue

            Dates

            • Created:
              Updated:

              Development