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

12,988

Solution 1

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

Solution 2

No, there's no built-in traversal for something like this, and remember that Java Strings are immutable, so you can't actually trim in place--you have to trim and replace. Some objects may not permit modification of their String variables.

Solution 3

Building off @SwissArmyKnife I converted his simple String trimming function into an interface with a default method. So any object where you would like to use object.trim(), you just have to add "implements Trimmable".

Simple String trim interface: Trimmable.class

/**
 * Utility interface that trims all String fields of the implementing class.
 */
public interface Trimmable {

    /**
     * Trim all Strings
     */
    default void trim(){
        for (Field field : this.getClass().getDeclaredFields()) {
            try {
                field.setAccessible(true);
                Object value = field.get(this);
                if (value != null){
                    if (value instanceof String){
                        String trimmed = (String) value;
                        field.set(this, trimmed.trim());
                    }
                }
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
    }
}

An object that we would like to be trimmable: Person.class (implements Trimmable interface)

public class Person implements Trimmable {
     private String firstName;
     private String lastName;
     private int age;

     // getters/setters omitted
}

Now you can use person.trim()

Person person = new Person();
person.setFirstName("    John   ");
person.setLastName("  Doe");
person.setAge(30);
person.trim();

Solution 4

I made a simple method for trimming String values with Reflection API.

public Object trimStringValues(Object model){
  for(Field field : model.getClass().getDeclaredFields()){
        try{
        field.setAccessible(true);
        Object value = field.get(model);
        String fieldName = field.getName();
         if(value != null){
            if(value instanceof String){
                String trimmed = (String) value;
                field.set(model, trimmed.trim());
                }

            }
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }
}

I haven't bumped in to any problems with this one yet. I know its an old thread, but it might help somone whos is looking for something simple.

Share:
12,988
Peter Smith
Author by

Peter Smith

I'm a developer who writes code in C#.NET, Java, Python, JQuery, and dabble in other languages. I'm currently trying to improve my development skills to deliver more robust applications faster through automated testing and better enterprise architectural pratices.

Updated on June 21, 2022

Comments

  • Peter Smith
    Peter Smith almost 2 years

    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.