Is there a common Java method to trim every string in an object graph?
No, there's no built-in traversal for something like this, and remember that Java String
s 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.
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:
- Java Reflection API
- Independent handling of Java Collections
- 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
- If the current level of
Property
is of typeString
- If its an
Object
Array of Properties - If its a
String
array - If its a type of Java
Collection
class - 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:
String
properties- Properties as custom datatypes which in turn have
String
properties - Properties as custom datatypes which in turn have properties as custom datatypes which in turn have
String
properties List
of custom data typesSet
ofStrings
Array
of custom data typesArray
ofStrings
Map
ofString
and custom data type
Object Graph:
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)
- Cannot handle undisciplined syntax of properties (invalid getters/setters)
- Cannot handle chained Collections: for example,
List<List<Person>>
- because of the exclusive support to disciplined getters/setters convention - No Guava collection library support
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();