@@ -29,6 +29,7 @@ of this software and associated documentation files (the "Software"), to deal
2929import java .io .IOException ;
3030import java .io .StringWriter ;
3131import java .io .Writer ;
32+ import java .lang .annotation .Annotation ;
3233import java .lang .reflect .Field ;
3334import java .lang .reflect .InvocationTargetException ;
3435import java .lang .reflect .Method ;
@@ -290,21 +291,44 @@ public JSONObject(Map<?, ?> m) {
290291 * Construct a JSONObject from an Object using bean getters. It reflects on
291292 * all of the public methods of the object. For each of the methods with no
292293 * parameters and a name starting with <code>"get"</code> or
293- * <code>"is"</code> followed by an uppercase letter, the method is invoked,
294- * and a key and the value returned from the getter method are put into the
295- * new JSONObject.
294+ * <code>"is"</code>, the method is invoked, and a key and the value
295+ * returned from the getter method are put into the new JSONObject.
296296 * <p>
297297 * The key is formed by removing the <code>"get"</code> or <code>"is"</code>
298298 * prefix. If the second remaining character is not upper case, then the
299299 * first character is converted to lower case.
300300 * <p>
301+ * Methods that return <code>void</code> as well as <code>static</code>
302+ * methods are ignored.
303+ * <p>
301304 * For example, if an object has a method named <code>"getName"</code>, and
302305 * if the result of calling <code>object.getName()</code> is
303306 * <code>"Larry Fine"</code>, then the JSONObject will contain
304307 * <code>"name": "Larry Fine"</code>.
305308 * <p>
306- * Methods that return <code>void</code> as well as <code>static</code>
307- * methods are ignored.
309+ * The {@link JSONPropertyName} annotation can be used on a bean getter to
310+ * override key name used in the JSONObject. For example, using the object
311+ * above with the <code>getName</code> method, if we annotated it with:
312+ * <pre>
313+ * @JSONPropertyName("FullName")
314+ * public String getName() { return this.name; }
315+ * </pre>
316+ * The resulting JSON object would contain <code>"FullName": "Larry Fine"</code>
317+ * <p>
318+ * The {@link JSONPropertyIgnore} annotation can be used to force the bean property
319+ * to not be serialized into JSON. If both {@link JSONPropertyIgnore} and
320+ * {@link JSONPropertyName} are defined on the same method, a depth comparison is
321+ * performed and the one closest to the concrete class being serialized is used.
322+ * If both annotations are at the same level, then the {@link JSONPropertyIgnore}
323+ * annotation takes precedent and the field is not serialized.
324+ * For example, the following declaration would prevent the <code>getName</code>
325+ * method from being serialized:
326+ * <pre>
327+ * @JSONPropertyName("FullName")
328+ * @JSONPropertyIgnore
329+ * public String getName() { return this.name; }
330+ * </pre>
331+ * <p>
308332 *
309333 * @param bean
310334 * An object that has getter methods that should be used to make
@@ -1409,8 +1433,8 @@ public String optString(String key, String defaultValue) {
14091433 }
14101434
14111435 /**
1412- * Populates the internal map of the JSONObject with the bean properties.
1413- * The bean can not be recursive.
1436+ * Populates the internal map of the JSONObject with the bean properties. The
1437+ * bean can not be recursive.
14141438 *
14151439 * @see JSONObject#JSONObject(Object)
14161440 *
@@ -1420,49 +1444,31 @@ public String optString(String key, String defaultValue) {
14201444 private void populateMap (Object bean ) {
14211445 Class <?> klass = bean .getClass ();
14221446
1423- // If klass is a System class then set includeSuperClass to false.
1447+ // If klass is a System class then set includeSuperClass to false.
14241448
14251449 boolean includeSuperClass = klass .getClassLoader () != null ;
14261450
1427- Method [] methods = includeSuperClass ? klass .getMethods () : klass
1428- .getDeclaredMethods ();
1451+ Method [] methods = includeSuperClass ? klass .getMethods () : klass .getDeclaredMethods ();
14291452 for (final Method method : methods ) {
14301453 final int modifiers = method .getModifiers ();
14311454 if (Modifier .isPublic (modifiers )
14321455 && !Modifier .isStatic (modifiers )
14331456 && method .getParameterTypes ().length == 0
14341457 && !method .isBridge ()
1435- && method .getReturnType () != Void .TYPE ) {
1436- final String name = method .getName ();
1437- String key ;
1438- if (name .startsWith ("get" )) {
1439- if ("getClass" .equals (name ) || "getDeclaringClass" .equals (name )) {
1440- continue ;
1441- }
1442- key = name .substring (3 );
1443- } else if (name .startsWith ("is" )) {
1444- key = name .substring (2 );
1445- } else {
1446- continue ;
1447- }
1448- if (key .length () > 0
1449- && Character .isUpperCase (key .charAt (0 ))) {
1450- if (key .length () == 1 ) {
1451- key = key .toLowerCase (Locale .ROOT );
1452- } else if (!Character .isUpperCase (key .charAt (1 ))) {
1453- key = key .substring (0 , 1 ).toLowerCase (Locale .ROOT )
1454- + key .substring (1 );
1455- }
1456-
1458+ && method .getReturnType () != Void .TYPE
1459+ && isValidMethodName (method .getName ())) {
1460+ final String key = getKeyNameFromMethod (method );
1461+ if (key != null && !key .isEmpty ()) {
14571462 try {
14581463 final Object result = method .invoke (bean );
14591464 if (result != null ) {
14601465 this .map .put (key , wrap (result ));
14611466 // we don't use the result anywhere outside of wrap
1462- // if it's a resource we should be sure to close it after calling toString
1463- if (result instanceof Closeable ) {
1467+ // if it's a resource we should be sure to close it
1468+ // after calling toString
1469+ if (result instanceof Closeable ) {
14641470 try {
1465- ((Closeable )result ).close ();
1471+ ((Closeable ) result ).close ();
14661472 } catch (IOException ignore ) {
14671473 }
14681474 }
@@ -1476,6 +1482,165 @@ private void populateMap(Object bean) {
14761482 }
14771483 }
14781484
1485+ private boolean isValidMethodName (String name ) {
1486+ return (name .startsWith ("get" ) || name .startsWith ("is" ))
1487+ && !"getClass" .equals (name )
1488+ && !"getDeclaringClass" .equals (name );
1489+ }
1490+
1491+ private String getKeyNameFromMethod (Method method ) {
1492+ final int ignoreDepth = getAnnotationDepth (method , JSONPropertyIgnore .class );
1493+ if (ignoreDepth > 0 ) {
1494+ final int forcedNameDepth = getAnnotationDepth (method , JSONPropertyName .class );
1495+ if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth ) {
1496+ // the hierarchy asked to ignore, and the nearest name override
1497+ // was higher or non-existent
1498+ return null ;
1499+ }
1500+ }
1501+ JSONPropertyName annotation = getAnnotation (method , JSONPropertyName .class );
1502+ if (annotation != null && annotation .value () != null && !annotation .value ().isEmpty ()) {
1503+ return annotation .value ();
1504+ }
1505+ String key ;
1506+ final String name = method .getName ();
1507+ if (name .startsWith ("get" )) {
1508+ key = name .substring (3 );
1509+ } else if (name .startsWith ("is" )) {
1510+ key = name .substring (2 );
1511+ } else {
1512+ return null ;
1513+ }
1514+ // if the first letter in the key is not uppercase, then skip.
1515+ // This is to maintain backwards compatibility before PR406
1516+ // (https://github.com/stleary/JSON-java/pull/406/)
1517+ if (key .isEmpty () || Character .isLowerCase (key .charAt (0 ))) {
1518+ return null ;
1519+ }
1520+ if (key .length () == 1 ) {
1521+ key = key .toLowerCase (Locale .ROOT );
1522+ } else if (!Character .isUpperCase (key .charAt (1 ))) {
1523+ key = key .substring (0 , 1 ).toLowerCase (Locale .ROOT ) + key .substring (1 );
1524+ }
1525+ return key ;
1526+ }
1527+
1528+ /**
1529+ * Searches the class hierarchy to see if the method or it's super
1530+ * implementations and interfaces has the annotation.
1531+ *
1532+ * @param <A>
1533+ * type of the annotation
1534+ *
1535+ * @param m
1536+ * method to check
1537+ * @param annotationClass
1538+ * annotation to look for
1539+ * @return the {@link Annotation} if the annotation exists on the current method
1540+ * or one of it's super class definitions
1541+ */
1542+ private static <A extends Annotation > A getAnnotation (final Method m , final Class <A > annotationClass ) {
1543+ // if we have invalid data the result is null
1544+ if (m == null || annotationClass == null ) {
1545+ return null ;
1546+ }
1547+
1548+ if (m .isAnnotationPresent (annotationClass )) {
1549+ return m .getAnnotation (annotationClass );
1550+ }
1551+
1552+ // if we've already reached the Object class, return null;
1553+ Class <?> c = m .getDeclaringClass ();
1554+ if (c .getSuperclass () == null ) {
1555+ return null ;
1556+ }
1557+
1558+ // check directly implemented interfaces for the method being checked
1559+ for (Class <?> i : c .getInterfaces ()) {
1560+ try {
1561+ Method im = i .getMethod (m .getName (), m .getParameterTypes ());
1562+ return getAnnotation (im , annotationClass );
1563+ } catch (final SecurityException ex ) {
1564+ continue ;
1565+ } catch (final NoSuchMethodException ex ) {
1566+ continue ;
1567+ }
1568+ }
1569+
1570+ try {
1571+ return getAnnotation (m .getDeclaringClass ().getSuperclass ().getMethod (m .getName (),
1572+ m .getParameterTypes ()),
1573+ annotationClass );
1574+ } catch (final SecurityException ex ) {
1575+ return null ;
1576+ } catch (final NoSuchMethodException ex ) {
1577+ return null ;
1578+ }
1579+ }
1580+
1581+ /**
1582+ * Searches the class hierarchy to see if the method or it's super
1583+ * implementations and interfaces has the annotation. Returns the depth of the
1584+ * annotation in the hierarchy.
1585+ *
1586+ * @param <A>
1587+ * type of the annotation
1588+ *
1589+ * @param m
1590+ * method to check
1591+ * @param annotationClass
1592+ * annotation to look for
1593+ * @return Depth of the annotation or -1 if the annotation is not on the method.
1594+ */
1595+ private static int getAnnotationDepth (final Method m , final Class <? extends Annotation > annotationClass ) {
1596+ // if we have invalid data the result is -1
1597+ if (m == null || annotationClass == null ) {
1598+ return -1 ;
1599+ }
1600+
1601+ if (m .isAnnotationPresent (annotationClass )) {
1602+ return 1 ;
1603+ }
1604+
1605+ // if we've already reached the Object class, return -1;
1606+ Class <?> c = m .getDeclaringClass ();
1607+ if (c .getSuperclass () == null ) {
1608+ return -1 ;
1609+ }
1610+
1611+ // check directly implemented interfaces for the method being checked
1612+ for (Class <?> i : c .getInterfaces ()) {
1613+ try {
1614+ Method im = i .getMethod (m .getName (), m .getParameterTypes ());
1615+ int d = getAnnotationDepth (im , annotationClass );
1616+ if (d > 0 ) {
1617+ // since the annotation was on the interface, add 1
1618+ return d + 1 ;
1619+ }
1620+ } catch (final SecurityException ex ) {
1621+ continue ;
1622+ } catch (final NoSuchMethodException ex ) {
1623+ continue ;
1624+ }
1625+ }
1626+
1627+ try {
1628+ int d = getAnnotationDepth (
1629+ m .getDeclaringClass ().getSuperclass ().getMethod (m .getName (),
1630+ m .getParameterTypes ()),
1631+ annotationClass );
1632+ if (d > 0 ) {
1633+ // since the annotation was on the superclass, add 1
1634+ return d + 1 ;
1635+ }
1636+ return -1 ;
1637+ } catch (final SecurityException ex ) {
1638+ return -1 ;
1639+ } catch (final NoSuchMethodException ex ) {
1640+ return -1 ;
1641+ }
1642+ }
1643+
14791644 /**
14801645 * Put a key/boolean pair in the JSONObject.
14811646 *
0 commit comments