Search code examples
javastringreflectionobject-graph

Is there a common Java method to trim every string in an object graph?


I'm hoping to trim all Strings that are part of an object graph.

So I have an object graph like so

 RootElement
   - name (String)
   - adjective (String)
   - items ArrayOfItems
     - getItems (List<Item>)
       - get(i) (Item)
       Item
         - name (String)
         - value (double)
         - alias (String)
         - references ArrayOfReferences
           - getReferences (List<Reference>)
             - get(i) (Reference)
             Reference
               - prop1 (String)
               - prop2 (Integer)
               - prop3 (String)

There is a get and set pair for every property of every class represented in this object graph. Ideally every field of type String would end up trimmed, including enumerating any child objects contained in collections. There are no cycles contained within the object graph.

Is there any java library that implements some sort of generic object graph visitor pattern or String\Reflection utility library that does this?

An external third party library that does this would also be fine, it does not have to be part of the standard java libraries.


Solution

  • Below is the explanation of solution that I have built using Java Reflection API. I have posted the working code (with its url to github) below. This solution mainly uses:

    1. Java Reflection API
    2. Independent handling of Java Collections
    3. Recursion

    To start with, I have used Introspector to go over the readMethods of the Class omitting the methods defined for Object

    for (PropertyDescriptor propertyDescriptor : Introspector
                        .getBeanInfo(c, Object.class).getPropertyDescriptors()) {
                Method method = propertyDescriptor.getReadMethod();
    

    Cases

    1. If the current level of Property is of type String
    2. If its an Object Array of Properties
    3. If its a String array
    4. If its a type of Java Collection class
    5. Separate placement for Map with special conditions to process its keys and values

    This utility uses the Java Reflection API to traverse through an object graph with disciplined syntax of getters and setters and trims all strings encountered within an Object graph recursively.

    Code

    This entire util class with the main test class (and custom data types/pojos) is here on my github

    Usage:

    myObj = (MyObject) SpaceUtil.trimReflective(myObj);
    

    Util method:

        public static Object trimReflective(Object object) throws Exception {
            if (object == null)
                return null;
    
            Class<? extends Object> c = object.getClass();
            try {
                // Introspector usage to pick the getters conveniently thereby
                // excluding the Object getters
                for (PropertyDescriptor propertyDescriptor : Introspector
                        .getBeanInfo(c, Object.class).getPropertyDescriptors()) {
                    Method method = propertyDescriptor.getReadMethod();
                    String name = method.getName();
    
                    // If the current level of Property is of type String
                    if (method.getReturnType().equals(String.class)) {
                        String property = (String) method.invoke(object);
                        if (property != null) {
                            Method setter = c.getMethod("set" + name.substring(3),
                                    new Class<?>[] { String.class });
                            if (setter != null)
                                // Setter to trim and set the trimmed String value
                                setter.invoke(object, property.trim());
                        }
                    }
    
                    // If an Object Array of Properties - added additional check to
                    // avoid getBytes returning a byte[] and process
                    if (method.getReturnType().isArray()
                            && !method.getReturnType().isPrimitive()
                            && !method.getReturnType().equals(String[].class)
                            && !method.getReturnType().equals(byte[].class)) {
                        System.out.println(method.getReturnType());
                        // Type check for primitive arrays (would fail typecasting
                        // in case of int[], char[] etc)
                        if (method.invoke(object) instanceof Object[]) {
                            Object[] objectArray = (Object[]) method.invoke(object);
                            if (objectArray != null) {
                                for (Object obj : (Object[]) objectArray) {
                                    // Recursively revisit with the current property
                                    trimReflective(obj);
                                }
                            }
                        }
                    }
                    // If a String array
                    if (method.getReturnType().equals(String[].class)) {
                        String[] propertyArray = (String[]) method.invoke(object);
                        if (propertyArray != null) {
                            Method setter = c.getMethod("set" + name.substring(3),
                                    new Class<?>[] { String[].class });
                            if (setter != null) {
                                String[] modifiedArray = new String[propertyArray.length];
                                for (int i = 0; i < propertyArray.length; i++)
                                    if (propertyArray[i] != null)
                                        modifiedArray[i] = propertyArray[i].trim();
    
                                // Explicit wrapping
                                setter.invoke(object,
                                        new Object[] { modifiedArray });
                            }
                        }
                    }
                    // Collections start
                    if (Collection.class.isAssignableFrom(method.getReturnType())) {
                        Collection collectionProperty = (Collection) method
                                .invoke(object);
                        if (collectionProperty != null) {
                            for (int index = 0; index < collectionProperty.size(); index++) {
                                if (collectionProperty.toArray()[index] instanceof String) {
                                    String element = (String) collectionProperty
                                            .toArray()[index];
    
                                    if (element != null) {
                                        // Check if List was created with
                                        // Arrays.asList (non-resizable Array)
                                        if (collectionProperty instanceof List) {
                                            ((List) collectionProperty).set(index,
                                                    element.trim());
                                        } else {
                                            collectionProperty.remove(element);
                                            collectionProperty.add(element.trim());
                                        }
                                    }
                                } else {
                                    // Recursively revisit with the current property
                                    trimReflective(collectionProperty.toArray()[index]);
                                }
                            }
                        }
                    }
                    // Separate placement for Map with special conditions to process
                    // keys and values
                    if (method.getReturnType().equals(Map.class)) {
                        Map mapProperty = (Map) method.invoke(object);
                        if (mapProperty != null) {
                            // Keys
                            for (int index = 0; index < mapProperty.keySet().size(); index++) {
                                if (mapProperty.keySet().toArray()[index] instanceof String) {
                                    String element = (String) mapProperty.keySet()
                                            .toArray()[index];
                                    if (element != null) {
                                        mapProperty.put(element.trim(),
                                                mapProperty.get(element));
                                        mapProperty.remove(element);
                                    }
                                } else {
                                    // Recursively revisit with the current property
                                    trimReflective(mapProperty.get(index));
                                }
    
                            }
                            // Values
                            for (Map.Entry entry : (Set<Map.Entry>) mapProperty
                                    .entrySet()) {
    
                                if (entry.getValue() instanceof String) {
                                    String element = (String) entry.getValue();
                                    if (element != null) {
                                        entry.setValue(element.trim());
                                    }
                                } else {
                                    // Recursively revisit with the current property
                                    trimReflective(entry.getValue());
                                }
                            }
                        }
                    } else {// Catch a custom data type as property and send through
                            // recursion
                        Object property = (Object) method.invoke(object);
                        if (property != null) {
                            trimReflective(property);
                        }
                    }
                }
    
            } catch (Exception e) {
                throw new Exception("Strings cannot be trimmed because: ", e);
            }
    
            return object;
    
        }
    

    Test

    I also have a test class in there which creates a relatively complex object. The test class has different scenarios that cover:

    1. String properties
    2. Properties as custom datatypes which in turn have String properties
    3. Properties as custom datatypes which in turn have properties as custom datatypes which in turn have String properties
    4. List of custom data types
    5. Set of Strings
    6. Array of custom data types
    7. Array of Strings
    8. Map of String and custom data type

    Object Graph:

    enter image description here

    Test Object Code Snippet:

    public static Music buildObj() {
            Song song1 = new Song();
            Song song2 = new Song();
            Song song3 = new Song();
    
        Artist artist1 = new Artist();
        Artist artist2 = new Artist();
    
        song1.setGenre("ROCK       ");
        song1.setSonnet("X    ");
        song1.setNotes("Y    ");
        song1.setCompostions(Arrays.asList(new String[] { "SOME X DATA  ",
                "SOME OTHER DATA X ", "SOME MORE DATA X    ", " " }));
    
        Set<String> instruments = new HashSet<String>();
        instruments.add("         GUITAR    ");
        instruments.add("         SITAR    ");
        instruments.add("         DRUMS    ");
        instruments.add("         BASS    ");
    
        song1.setInstruments(instruments);
    
        song2.setGenre("METAL       ");
        song2.setSonnet("A    ");
        song2.setNotes("B    ");
        song2.setCompostions(Arrays.asList(new String[] { "SOME Y DATA  ",
                "          SOME OTHER DATA Y ",
                "           SOME MORE DATA Y    ", " " }));
    
        song3.setGenre("POP       ");
        song3.setSonnet("DONT    ");
        song3.setNotes("KNOW     ");
        song3.setCompostions(Arrays.asList(new String[] { "SOME Z DATA  ",
                "               SOME OTHER DATA Z ",
                "          SOME MORE DATA Z   ", " " }));
    
        artist1.setSongList(Arrays.asList(new Song[] { song1, song3 }));
    
        artist2.setSongList(Arrays.asList(new Song[] { song1, song2, song3 }));
        Map<String, Person> artistMap = new HashMap<String, Person>();
        Person tutor1 = new Person();
        tutor1.setName("JOHN JACKSON DOE       ");
        artistMap.put("          Name                 ", tutor1);
    
        Person coach1 = new Person();
        coach1.setName("CARTER   ");
        artistMap.put("Coach      ", coach1);
        artist2.setTutor(artistMap);
    
        music.setSongs(Arrays.asList(new Song[] { song1, song2, song3 }));
        music.setArtists(Arrays.asList(new Artist[] { artist1, artist2 }));
    
        music.setLanguages(new String[] { "    ENGLISH    ", "FRENCH    ",
                "HINDI    " });
        Person singer1 = new Person();
        singer1.setName("DAVID      ");
    
        Person singer2 = new Person();
        singer2.setName("JACOB      ");
        music.setSingers(new Person[] { singer1, singer2 });
    
        Human man = new Human();
        Person p = new Person();
        p.setName("   JACK'S RAGING BULL   ");
        SomeGuy m = new SomeGuy();
        m.setPerson(p);
        man.setMan(m);
    
        music.setHuman(man);
    
        return music;
    }
    

    Outcome:

    #######BEFORE#######
    >>[>>DAVID      ---<<, >>JACOB      ---<<]---[    ENGLISH    , FRENCH    , HINDI    ]---[>>ROCK       ---X    ---Y    ---[SOME X DATA  , SOME OTHER DATA X , SOME MORE DATA X    ,  ]---[         SITAR    ,          GUITAR    ,          BASS    ,          DRUMS    ]<<, >>METAL       ---A    ---B    ---[SOME Y DATA  ,           SOME OTHER DATA Y ,            SOME MORE DATA Y    ,  ]---<<, >>POP       ---DONT    ---KNOW     ---[SOME Z DATA  ,                SOME OTHER DATA Z ,           SOME MORE DATA Z   ,  ]---<<]---[>>---[>>ROCK       ---X    ---Y    ---[SOME X DATA  , SOME OTHER DATA X , SOME MORE DATA X    ,  ]---[         SITAR    ,          GUITAR    ,          BASS    ,          DRUMS    ]<<, >>POP       ---DONT    ---KNOW     ---[SOME Z DATA  ,                SOME OTHER DATA Z ,           SOME MORE DATA Z   ,  ]---<<]<<, >>{Coach      =>>CARTER    ---<<,           Name                 =>>JOHN JACKSON DOE       ---<<}---[>>ROCK       ---X    ---Y    ---[SOME X DATA  , SOME OTHER DATA X , SOME MORE DATA X    ,  ]---[         SITAR    ,          GUITAR    ,          BASS    ,          DRUMS    ]<<, >>METAL       ---A    ---B    ---[SOME Y DATA  ,           SOME OTHER DATA Y ,            SOME MORE DATA Y    ,  ]---<<, >>POP       ---DONT    ---KNOW     ---[SOME Z DATA  ,                SOME OTHER DATA Z ,           SOME MORE DATA Z   ,  ]---<<]<<]---=>   JACK'S RAGING BULL   <=<<
    Number of spaces : 644
    #######AFTER#######
    >>[>>DAVID---<<, >>JACOB---<<]---[ENGLISH, FRENCH, HINDI]---[>>ROCK---X---Y---[SOME X DATA, SOME OTHER DATA X, SOME MORE DATA X, ]---[GUITAR, SITAR, DRUMS, BASS]<<, >>METAL---A---B---[SOME Y DATA, SOME OTHER DATA Y, SOME MORE DATA Y, ]---<<, >>POP---DONT---KNOW---[SOME Z DATA, SOME OTHER DATA Z, SOME MORE DATA Z, ]---<<]---[>>---[>>ROCK---X---Y---[SOME X DATA, SOME OTHER DATA X, SOME MORE DATA X, ]---[GUITAR, SITAR, DRUMS, BASS]<<, >>POP---DONT---KNOW---[SOME Z DATA, SOME OTHER DATA Z, SOME MORE DATA Z, ]---<<]<<, >>{Name=>>JOHN JACKSON DOE---<<, Coach=>>CARTER---<<}---[>>ROCK---X---Y---[SOME X DATA, SOME OTHER DATA X, SOME MORE DATA X, ]---[GUITAR, SITAR, DRUMS, BASS]<<, >>METAL---A---B---[SOME Y DATA, SOME OTHER DATA Y, SOME MORE DATA Y, ]---<<, >>POP---DONT---KNOW---[SOME Z DATA, SOME OTHER DATA Z, SOME MORE DATA Z, ]---<<]<<]---=>JACK'S RAGING BULL<=<<
    Number of spaces : 111
    

    There is a non-zero count of the number of spaces in the above trimmed output because I didn't make an effort to override toString of any collections (List, Set) or Map. There are certain improvements to the code I want to make but for your case the solution should work just fine.

    Limitations (further improvements)

    1. Cannot handle undisciplined syntax of properties (invalid getters/setters)
    2. Cannot handle chained Collections: for example, List<List<Person>> - because of the exclusive support to disciplined getters/setters convention
    3. No Guava collection library support