From 74b9a60f98b352a15581d98018a8a5d3318308aa Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Wed, 7 Mar 2018 14:51:37 -0500 Subject: [PATCH 1/2] Adds annotation to support custom field names during Bean serialization --- JSONObject.java | 235 ++++++++++++++++++++++++++++++++++------ JSONPropertyIgnore.java | 43 ++++++++ JSONPropertyName.java | 47 ++++++++ README.md | 12 ++ 4 files changed, 302 insertions(+), 35 deletions(-) create mode 100644 JSONPropertyIgnore.java create mode 100644 JSONPropertyName.java diff --git a/JSONObject.java b/JSONObject.java index 5a6b734..ca219c9 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -29,6 +29,7 @@ import java.io.Closeable; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -290,21 +291,44 @@ public class JSONObject { * Construct a JSONObject from an Object using bean getters. It reflects on * all of the public methods of the object. For each of the methods with no * parameters and a name starting with "get" or - * "is" followed by an uppercase letter, the method is invoked, - * and a key and the value returned from the getter method are put into the - * new JSONObject. + * "is", the method is invoked, and a key and the value + * returned from the getter method are put into the new JSONObject. *

* The key is formed by removing the "get" or "is" * prefix. If the second remaining character is not upper case, then the * first character is converted to lower case. *

+ * Methods that return void as well as static + * methods are ignored. + *

* For example, if an object has a method named "getName", and * if the result of calling object.getName() is * "Larry Fine", then the JSONObject will contain * "name": "Larry Fine". *

- * Methods that return void as well as static - * methods are ignored. + * The {@link JSONPropertyName} annotation can be used on a bean getter to + * override key name used in the JSONObject. For example, using the object + * above with the getName method, if we annotated it with: + *

+     * @JSONPropertyName("FullName")
+     * public String getName() { return this.name; }
+     * 
+ * The resulting JSON object would contain "FullName": "Larry Fine" + *

+ * The {@link JSONPropertyIgnore} annotation can be used to force the bean property + * to not be serialized into JSON. If both {@link JSONPropertyIgnore} and + * {@link JSONPropertyName} are defined on the same method, a depth comparison is + * performed and the one closest to the concrete class being serialized is used. + * If both annotations are at the same level, then the {@link JSONPropertyIgnore} + * annotation takes precedent and the field is not serialized. + * For example, the following declaration would prevent the getName + * method from being serialized: + *

+     * @JSONPropertyName("FullName")
+     * @JSONPropertyIgnore 
+     * public String getName() { return this.name; }
+     * 
+ *

* * @param bean * An object that has getter methods that should be used to make @@ -1409,8 +1433,8 @@ public class JSONObject { } /** - * Populates the internal map of the JSONObject with the bean properties. - * The bean can not be recursive. + * Populates the internal map of the JSONObject with the bean properties. The + * bean can not be recursive. * * @see JSONObject#JSONObject(Object) * @@ -1420,49 +1444,31 @@ public class JSONObject { private void populateMap(Object bean) { Class klass = bean.getClass(); -// If klass is a System class then set includeSuperClass to false. + // If klass is a System class then set includeSuperClass to false. boolean includeSuperClass = klass.getClassLoader() != null; - Method[] methods = includeSuperClass ? klass.getMethods() : klass - .getDeclaredMethods(); + Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods(); for (final Method method : methods) { final int modifiers = method.getModifiers(); if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers) && method.getParameterTypes().length == 0 && !method.isBridge() - && method.getReturnType() != Void.TYPE ) { - final String name = method.getName(); - String key; - if (name.startsWith("get")) { - if ("getClass".equals(name) || "getDeclaringClass".equals(name)) { - continue; - } - key = name.substring(3); - } else if (name.startsWith("is")) { - key = name.substring(2); - } else { - continue; - } - if (key.length() > 0 - && Character.isUpperCase(key.charAt(0))) { - if (key.length() == 1) { - key = key.toLowerCase(Locale.ROOT); - } else if (!Character.isUpperCase(key.charAt(1))) { - key = key.substring(0, 1).toLowerCase(Locale.ROOT) - + key.substring(1); - } - + && method.getReturnType() != Void.TYPE + && isValidMethodName(method.getName())) { + final String key = getKeyNameFromMethod(method); + if (key != null && !key.isEmpty()) { try { final Object result = method.invoke(bean); if (result != null) { this.map.put(key, wrap(result)); // we don't use the result anywhere outside of wrap - // if it's a resource we should be sure to close it after calling toString - if(result instanceof Closeable) { + // if it's a resource we should be sure to close it + // after calling toString + if (result instanceof Closeable) { try { - ((Closeable)result).close(); + ((Closeable) result).close(); } catch (IOException ignore) { } } @@ -1476,6 +1482,165 @@ public class JSONObject { } } + private boolean isValidMethodName(String name) { + return (name.startsWith("get") || name.startsWith("is")) + && !"getClass".equals(name) + && !"getDeclaringClass".equals(name); + } + + private String getKeyNameFromMethod(Method method) { + final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class); + if (ignoreDepth > 0) { + final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class); + if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth) { + // the hierarchy asked to ignore, and the nearest name override + // was higher or non-existent + return null; + } + } + JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class); + if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) { + return annotation.value(); + } + String key; + final String name = method.getName(); + if (name.startsWith("get")) { + key = name.substring(3); + } else if (name.startsWith("is")) { + key = name.substring(2); + } else { + return null; + } + // if the first letter in the key is not uppercase, then skip. + // This is to maintain backwards compatibility before PR406 + // (https://github.com/stleary/JSON-java/pull/406/) + if(key.isEmpty() || Character.isLowerCase(key.charAt(0))) { + return null; + } + if (key.length() == 1) { + key = key.toLowerCase(Locale.ROOT); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase(Locale.ROOT) + key.substring(1); + } + return key; + } + + /** + * Searches the class hierarchy to see if the method or it's super + * implementations and interfaces has the annotation. + * + * @param + * type of the annotation + * + * @param m + * method to check + * @param annotationClass + * annotation to look for + * @return the {@link Annotation} if the annotation exists on the current method + * or one of it's super class definitions + */ + private static A getAnnotation(final Method m, final Class annotationClass) { + // if we have invalid data the result is null + if (m == null || annotationClass == null) { + return null; + } + + if (m.isAnnotationPresent(annotationClass)) { + return m.getAnnotation(annotationClass); + } + + // if we've already reached the Object class, return null; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return null; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + return getAnnotation(im, annotationClass); + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + return getAnnotation(m.getDeclaringClass().getSuperclass().getMethod(m.getName(), + m.getParameterTypes()), + annotationClass); + } catch (final SecurityException ex) { + return null; + } catch (final NoSuchMethodException ex) { + return null; + } + } + + /** + * Searches the class hierarchy to see if the method or it's super + * implementations and interfaces has the annotation. Returns the depth of the + * annotation in the hierarchy. + * + * @param + * type of the annotation + * + * @param m + * method to check + * @param annotationClass + * annotation to look for + * @return Depth of the annotation or -1 if the annotation is not on the method. + */ + private static int getAnnotationDepth(final Method m, final Class annotationClass) { + // if we have invalid data the result is -1 + if (m == null || annotationClass == null) { + return -1; + } + + if (m.isAnnotationPresent(annotationClass)) { + return 1; + } + + // if we've already reached the Object class, return -1; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return -1; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + int d = getAnnotationDepth(im, annotationClass); + if (d > 0) { + // since the annotation was on the interface, add 1 + return d + 1; + } + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + int d = getAnnotationDepth( + m.getDeclaringClass().getSuperclass().getMethod(m.getName(), + m.getParameterTypes()), + annotationClass); + if (d > 0) { + // since the annotation was on the superclass, add 1 + return d + 1; + } + return -1; + } catch (final SecurityException ex) { + return -1; + } catch (final NoSuchMethodException ex) { + return -1; + } + } + /** * Put a key/boolean pair in the JSONObject. * diff --git a/JSONPropertyIgnore.java b/JSONPropertyIgnore.java new file mode 100644 index 0000000..682de74 --- /dev/null +++ b/JSONPropertyIgnore.java @@ -0,0 +1,43 @@ +package org.json; + +/* +Copyright (c) 2018 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) +/** + * Use this annotation on a getter method to override the Bean name + * parser for Bean -> JSONObject mapping. If this annotation is + * present at any level in the class hierarchy, then the method will + * not be serialized from the bean into the JSONObject. + */ +public @interface JSONPropertyIgnore { } diff --git a/JSONPropertyName.java b/JSONPropertyName.java new file mode 100644 index 0000000..a1bcd58 --- /dev/null +++ b/JSONPropertyName.java @@ -0,0 +1,47 @@ +package org.json; + +/* +Copyright (c) 2018 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) +/** + * Use this annotation on a getter method to override the Bean name + * parser for Bean -> JSONObject mapping. A value set to empty string "" + * will have the Bean parser fall back to the default field name processing. + */ +public @interface JSONPropertyName { + /** + * @return The name of the property as to be used in the JSON Object. + */ + String value(); +} diff --git a/README.md b/README.md index a62a0ea..e66f3c6 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,18 @@ by this package. JSON Pointers both in the form of string representation and URI fragment representation. +**JSONPropertyIgnore.java**: Annotation class that can be used on Java Bean getter methods. +When used on a bean method that would normally be serialized into a `JSONObject`, it +overrides the getter-to-key-name logic and forces the property to be excluded from the +resulting `JSONObject`. + +**JSONPropertyName.java**: Annotation class that can be used on Java Bean getter methods. +When used on a bean method that would normally be serialized into a `JSONObject`, it +overrides the getter-to-key-name logic and uses the value of the annotation. The Bean +processor will look through the class hierarchy. This means you can use the annotation on +a base class or interface and the value of the annotation will be used even if the getter +is overridden in a child class. + **JSONString.java**: The `JSONString` interface requires a `toJSONString` method, allowing an object to provide its own serialization. From a509a28ed47a5a31e0981a6de3949a7b46c5aa4a Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Sun, 11 Mar 2018 16:59:34 -0400 Subject: [PATCH 2/2] Cleans up the name check a little to be more permissive on what can be tagged with the new JSONPropertyName annotation. Also updates the javadoc to reflect the new name allowances --- JSONObject.java | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/JSONObject.java b/JSONObject.java index ca219c9..58138c0 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -291,15 +291,16 @@ public class JSONObject { * Construct a JSONObject from an Object using bean getters. It reflects on * all of the public methods of the object. For each of the methods with no * parameters and a name starting with "get" or - * "is", the method is invoked, and a key and the value - * returned from the getter method are put into the new JSONObject. + * "is" followed by an uppercase letter, the method is invoked, + * and a key and the value returned from the getter method are put into the + * new JSONObject. *

* The key is formed by removing the "get" or "is" * prefix. If the second remaining character is not upper case, then the * first character is converted to lower case. *

- * Methods that return void as well as static - * methods are ignored. + * Methods that are static, return void, + * have parameters, or are "bridge" methods, are ignored. *

* For example, if an object has a method named "getName", and * if the result of calling object.getName() is @@ -315,6 +316,16 @@ public class JSONObject { * * The resulting JSON object would contain "FullName": "Larry Fine" *

+ * Similarly, the {@link JSONPropertyName} annotation can be used on non- + * get and is methods. We can also override key + * name used in the JSONObject as seen below even though the field would normally + * be ignored: + *

+     * @JSONPropertyName("FullName")
+     * public String fullName() { return this.name; }
+     * 
+ * The resulting JSON object would contain "FullName": "Larry Fine" + *

* The {@link JSONPropertyIgnore} annotation can be used to force the bean property * to not be serialized into JSON. If both {@link JSONPropertyIgnore} and * {@link JSONPropertyName} are defined on the same method, a depth comparison is @@ -1483,9 +1494,7 @@ public class JSONObject { } private boolean isValidMethodName(String name) { - return (name.startsWith("get") || name.startsWith("is")) - && !"getClass".equals(name) - && !"getDeclaringClass".equals(name); + return !"getClass".equals(name) && !"getDeclaringClass".equals(name); } private String getKeyNameFromMethod(Method method) { @@ -1504,9 +1513,9 @@ public class JSONObject { } String key; final String name = method.getName(); - if (name.startsWith("get")) { + if (name.startsWith("get") && name.length() > 3) { key = name.substring(3); - } else if (name.startsWith("is")) { + } else if (name.startsWith("is") && name.length() > 2) { key = name.substring(2); } else { return null; @@ -1514,7 +1523,7 @@ public class JSONObject { // if the first letter in the key is not uppercase, then skip. // This is to maintain backwards compatibility before PR406 // (https://github.com/stleary/JSON-java/pull/406/) - if(key.isEmpty() || Character.isLowerCase(key.charAt(0))) { + if (Character.isLowerCase(key.charAt(0))) { return null; } if (key.length() == 1) { @@ -1568,8 +1577,8 @@ public class JSONObject { } try { - return getAnnotation(m.getDeclaringClass().getSuperclass().getMethod(m.getName(), - m.getParameterTypes()), + return getAnnotation( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), annotationClass); } catch (final SecurityException ex) { return null; @@ -1626,8 +1635,7 @@ public class JSONObject { try { int d = getAnnotationDepth( - m.getDeclaringClass().getSuperclass().getMethod(m.getName(), - m.getParameterTypes()), + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), annotationClass); if (d > 0) { // since the annotation was on the superclass, add 1