@@ -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 ;
@@ -305,13 +306,47 @@ public JSONObject(Map<?, ?> m) {
305306 * prefix. If the second remaining character is not upper case, then the
306307 * first character is converted to lower case.
307308 * <p>
309+ * Methods that are <code>static</code>, return <code>void</code>,
310+ * have parameters, or are "bridge" methods, are ignored.
311+ * <p>
308312 * For example, if an object has a method named <code>"getName"</code>, and
309313 * if the result of calling <code>object.getName()</code> is
310314 * <code>"Larry Fine"</code>, then the JSONObject will contain
311315 * <code>"name": "Larry Fine"</code>.
312316 * <p>
313- * Methods that return <code>void</code> as well as <code>static</code>
314- * methods are ignored.
317+ * The {@link JSONPropertyName} annotation can be used on a bean getter to
318+ * override key name used in the JSONObject. For example, using the object
319+ * above with the <code>getName</code> method, if we annotated it with:
320+ * <pre>
321+ * @JSONPropertyName("FullName")
322+ * public String getName() { return this.name; }
323+ * </pre>
324+ * The resulting JSON object would contain <code>"FullName": "Larry Fine"</code>
325+ * <p>
326+ * Similarly, the {@link JSONPropertyName} annotation can be used on non-
327+ * <code>get</code> and <code>is</code> methods. We can also override key
328+ * name used in the JSONObject as seen below even though the field would normally
329+ * be ignored:
330+ * <pre>
331+ * @JSONPropertyName("FullName")
332+ * public String fullName() { return this.name; }
333+ * </pre>
334+ * The resulting JSON object would contain <code>"FullName": "Larry Fine"</code>
335+ * <p>
336+ * The {@link JSONPropertyIgnore} annotation can be used to force the bean property
337+ * to not be serialized into JSON. If both {@link JSONPropertyIgnore} and
338+ * {@link JSONPropertyName} are defined on the same method, a depth comparison is
339+ * performed and the one closest to the concrete class being serialized is used.
340+ * If both annotations are at the same level, then the {@link JSONPropertyIgnore}
341+ * annotation takes precedent and the field is not serialized.
342+ * For example, the following declaration would prevent the <code>getName</code>
343+ * method from being serialized:
344+ * <pre>
345+ * @JSONPropertyName("FullName")
346+ * @JSONPropertyIgnore
347+ * public String getName() { return this.name; }
348+ * </pre>
349+ * <p>
315350 *
316351 * @param bean
317352 * An object that has getter methods that should be used to make
@@ -1420,8 +1455,8 @@ public String optString(String key, String defaultValue) {
14201455 }
14211456
14221457 /**
1423- * Populates the internal map of the JSONObject with the bean properties.
1424- * The bean can not be recursive.
1458+ * Populates the internal map of the JSONObject with the bean properties. The
1459+ * bean can not be recursive.
14251460 *
14261461 * @see JSONObject#JSONObject(Object)
14271462 *
@@ -1431,49 +1466,31 @@ public String optString(String key, String defaultValue) {
14311466 private void populateMap (Object bean ) {
14321467 Class <?> klass = bean .getClass ();
14331468
1434- // If klass is a System class then set includeSuperClass to false.
1469+ // If klass is a System class then set includeSuperClass to false.
14351470
14361471 boolean includeSuperClass = klass .getClassLoader () != null ;
14371472
1438- Method [] methods = includeSuperClass ? klass .getMethods () : klass
1439- .getDeclaredMethods ();
1473+ Method [] methods = includeSuperClass ? klass .getMethods () : klass .getDeclaredMethods ();
14401474 for (final Method method : methods ) {
14411475 final int modifiers = method .getModifiers ();
14421476 if (Modifier .isPublic (modifiers )
14431477 && !Modifier .isStatic (modifiers )
14441478 && method .getParameterTypes ().length == 0
14451479 && !method .isBridge ()
1446- && method .getReturnType () != Void .TYPE ) {
1447- final String name = method .getName ();
1448- String key ;
1449- if (name .startsWith ("get" )) {
1450- if ("getClass" .equals (name ) || "getDeclaringClass" .equals (name )) {
1451- continue ;
1452- }
1453- key = name .substring (3 );
1454- } else if (name .startsWith ("is" )) {
1455- key = name .substring (2 );
1456- } else {
1457- continue ;
1458- }
1459- if (key .length () > 0
1460- && Character .isUpperCase (key .charAt (0 ))) {
1461- if (key .length () == 1 ) {
1462- key = key .toLowerCase (Locale .ROOT );
1463- } else if (!Character .isUpperCase (key .charAt (1 ))) {
1464- key = key .substring (0 , 1 ).toLowerCase (Locale .ROOT )
1465- + key .substring (1 );
1466- }
1467-
1480+ && method .getReturnType () != Void .TYPE
1481+ && isValidMethodName (method .getName ())) {
1482+ final String key = getKeyNameFromMethod (method );
1483+ if (key != null && !key .isEmpty ()) {
14681484 try {
14691485 final Object result = method .invoke (bean );
14701486 if (result != null ) {
14711487 this .map .put (key , wrap (result ));
14721488 // we don't use the result anywhere outside of wrap
1473- // if it's a resource we should be sure to close it after calling toString
1474- if (result instanceof Closeable ) {
1489+ // if it's a resource we should be sure to close it
1490+ // after calling toString
1491+ if (result instanceof Closeable ) {
14751492 try {
1476- ((Closeable )result ).close ();
1493+ ((Closeable ) result ).close ();
14771494 } catch (IOException ignore ) {
14781495 }
14791496 }
@@ -1487,6 +1504,162 @@ private void populateMap(Object bean) {
14871504 }
14881505 }
14891506
1507+ private boolean isValidMethodName (String name ) {
1508+ return !"getClass" .equals (name ) && !"getDeclaringClass" .equals (name );
1509+ }
1510+
1511+ private String getKeyNameFromMethod (Method method ) {
1512+ final int ignoreDepth = getAnnotationDepth (method , JSONPropertyIgnore .class );
1513+ if (ignoreDepth > 0 ) {
1514+ final int forcedNameDepth = getAnnotationDepth (method , JSONPropertyName .class );
1515+ if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth ) {
1516+ // the hierarchy asked to ignore, and the nearest name override
1517+ // was higher or non-existent
1518+ return null ;
1519+ }
1520+ }
1521+ JSONPropertyName annotation = getAnnotation (method , JSONPropertyName .class );
1522+ if (annotation != null && annotation .value () != null && !annotation .value ().isEmpty ()) {
1523+ return annotation .value ();
1524+ }
1525+ String key ;
1526+ final String name = method .getName ();
1527+ if (name .startsWith ("get" ) && name .length () > 3 ) {
1528+ key = name .substring (3 );
1529+ } else if (name .startsWith ("is" ) && name .length () > 2 ) {
1530+ key = name .substring (2 );
1531+ } else {
1532+ return null ;
1533+ }
1534+ // if the first letter in the key is not uppercase, then skip.
1535+ // This is to maintain backwards compatibility before PR406
1536+ // (https://github.com/stleary/JSON-java/pull/406/)
1537+ if (Character .isLowerCase (key .charAt (0 ))) {
1538+ return null ;
1539+ }
1540+ if (key .length () == 1 ) {
1541+ key = key .toLowerCase (Locale .ROOT );
1542+ } else if (!Character .isUpperCase (key .charAt (1 ))) {
1543+ key = key .substring (0 , 1 ).toLowerCase (Locale .ROOT ) + key .substring (1 );
1544+ }
1545+ return key ;
1546+ }
1547+
1548+ /**
1549+ * Searches the class hierarchy to see if the method or it's super
1550+ * implementations and interfaces has the annotation.
1551+ *
1552+ * @param <A>
1553+ * type of the annotation
1554+ *
1555+ * @param m
1556+ * method to check
1557+ * @param annotationClass
1558+ * annotation to look for
1559+ * @return the {@link Annotation} if the annotation exists on the current method
1560+ * or one of it's super class definitions
1561+ */
1562+ private static <A extends Annotation > A getAnnotation (final Method m , final Class <A > annotationClass ) {
1563+ // if we have invalid data the result is null
1564+ if (m == null || annotationClass == null ) {
1565+ return null ;
1566+ }
1567+
1568+ if (m .isAnnotationPresent (annotationClass )) {
1569+ return m .getAnnotation (annotationClass );
1570+ }
1571+
1572+ // if we've already reached the Object class, return null;
1573+ Class <?> c = m .getDeclaringClass ();
1574+ if (c .getSuperclass () == null ) {
1575+ return null ;
1576+ }
1577+
1578+ // check directly implemented interfaces for the method being checked
1579+ for (Class <?> i : c .getInterfaces ()) {
1580+ try {
1581+ Method im = i .getMethod (m .getName (), m .getParameterTypes ());
1582+ return getAnnotation (im , annotationClass );
1583+ } catch (final SecurityException ex ) {
1584+ continue ;
1585+ } catch (final NoSuchMethodException ex ) {
1586+ continue ;
1587+ }
1588+ }
1589+
1590+ try {
1591+ return getAnnotation (
1592+ c .getSuperclass ().getMethod (m .getName (), m .getParameterTypes ()),
1593+ annotationClass );
1594+ } catch (final SecurityException ex ) {
1595+ return null ;
1596+ } catch (final NoSuchMethodException ex ) {
1597+ return null ;
1598+ }
1599+ }
1600+
1601+ /**
1602+ * Searches the class hierarchy to see if the method or it's super
1603+ * implementations and interfaces has the annotation. Returns the depth of the
1604+ * annotation in the hierarchy.
1605+ *
1606+ * @param <A>
1607+ * type of the annotation
1608+ *
1609+ * @param m
1610+ * method to check
1611+ * @param annotationClass
1612+ * annotation to look for
1613+ * @return Depth of the annotation or -1 if the annotation is not on the method.
1614+ */
1615+ private static int getAnnotationDepth (final Method m , final Class <? extends Annotation > annotationClass ) {
1616+ // if we have invalid data the result is -1
1617+ if (m == null || annotationClass == null ) {
1618+ return -1 ;
1619+ }
1620+
1621+ if (m .isAnnotationPresent (annotationClass )) {
1622+ return 1 ;
1623+ }
1624+
1625+ // if we've already reached the Object class, return -1;
1626+ Class <?> c = m .getDeclaringClass ();
1627+ if (c .getSuperclass () == null ) {
1628+ return -1 ;
1629+ }
1630+
1631+ // check directly implemented interfaces for the method being checked
1632+ for (Class <?> i : c .getInterfaces ()) {
1633+ try {
1634+ Method im = i .getMethod (m .getName (), m .getParameterTypes ());
1635+ int d = getAnnotationDepth (im , annotationClass );
1636+ if (d > 0 ) {
1637+ // since the annotation was on the interface, add 1
1638+ return d + 1 ;
1639+ }
1640+ } catch (final SecurityException ex ) {
1641+ continue ;
1642+ } catch (final NoSuchMethodException ex ) {
1643+ continue ;
1644+ }
1645+ }
1646+
1647+ try {
1648+ int d = getAnnotationDepth (
1649+ c .getSuperclass ().getMethod (m .getName (), m .getParameterTypes ()),
1650+ annotationClass );
1651+ if (d > 0 ) {
1652+ // since the annotation was on the superclass, add 1
1653+ return d + 1 ;
1654+ }
1655+ return -1 ;
1656+ } catch (final SecurityException ex ) {
1657+ return -1 ;
1658+ } catch (final NoSuchMethodException ex ) {
1659+ return -1 ;
1660+ }
1661+ }
1662+
14901663 /**
14911664 * Put a key/boolean pair in the JSONObject.
14921665 *
0 commit comments