@@ -29,6 +29,7 @@ of this software and associated documentation files (the "Software"), to deal
29
29
import java .io .IOException ;
30
30
import java .io .StringWriter ;
31
31
import java .io .Writer ;
32
+ import java .lang .annotation .Annotation ;
32
33
import java .lang .reflect .Field ;
33
34
import java .lang .reflect .InvocationTargetException ;
34
35
import java .lang .reflect .Method ;
@@ -290,21 +291,44 @@ public JSONObject(Map<?, ?> m) {
290
291
* Construct a JSONObject from an Object using bean getters. It reflects on
291
292
* all of the public methods of the object. For each of the methods with no
292
293
* 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.
296
296
* <p>
297
297
* The key is formed by removing the <code>"get"</code> or <code>"is"</code>
298
298
* prefix. If the second remaining character is not upper case, then the
299
299
* first character is converted to lower case.
300
300
* <p>
301
+ * Methods that return <code>void</code> as well as <code>static</code>
302
+ * methods are ignored.
303
+ * <p>
301
304
* For example, if an object has a method named <code>"getName"</code>, and
302
305
* if the result of calling <code>object.getName()</code> is
303
306
* <code>"Larry Fine"</code>, then the JSONObject will contain
304
307
* <code>"name": "Larry Fine"</code>.
305
308
* <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>
308
332
*
309
333
* @param bean
310
334
* An object that has getter methods that should be used to make
@@ -1409,8 +1433,8 @@ public String optString(String key, String defaultValue) {
1409
1433
}
1410
1434
1411
1435
/**
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.
1414
1438
*
1415
1439
* @see JSONObject#JSONObject(Object)
1416
1440
*
@@ -1420,49 +1444,31 @@ public String optString(String key, String defaultValue) {
1420
1444
private void populateMap (Object bean ) {
1421
1445
Class <?> klass = bean .getClass ();
1422
1446
1423
- // If klass is a System class then set includeSuperClass to false.
1447
+ // If klass is a System class then set includeSuperClass to false.
1424
1448
1425
1449
boolean includeSuperClass = klass .getClassLoader () != null ;
1426
1450
1427
- Method [] methods = includeSuperClass ? klass .getMethods () : klass
1428
- .getDeclaredMethods ();
1451
+ Method [] methods = includeSuperClass ? klass .getMethods () : klass .getDeclaredMethods ();
1429
1452
for (final Method method : methods ) {
1430
1453
final int modifiers = method .getModifiers ();
1431
1454
if (Modifier .isPublic (modifiers )
1432
1455
&& !Modifier .isStatic (modifiers )
1433
1456
&& method .getParameterTypes ().length == 0
1434
1457
&& !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 ()) {
1457
1462
try {
1458
1463
final Object result = method .invoke (bean );
1459
1464
if (result != null ) {
1460
1465
this .map .put (key , wrap (result ));
1461
1466
// 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 ) {
1464
1470
try {
1465
- ((Closeable )result ).close ();
1471
+ ((Closeable ) result ).close ();
1466
1472
} catch (IOException ignore ) {
1467
1473
}
1468
1474
}
@@ -1476,6 +1482,165 @@ private void populateMap(Object bean) {
1476
1482
}
1477
1483
}
1478
1484
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
+
1479
1644
/**
1480
1645
* Put a key/boolean pair in the JSONObject.
1481
1646
*
0 commit comments