You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2020/03/11 23:56:03 UTC
[juneau] branch master updated: JUNEAU-185
This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 99aec78 JUNEAU-185
99aec78 is described below
commit 99aec786eb9f2a8cfd0c65f6611783a59d7df2c3
Author: JamesBognar <ja...@apache.org>
AuthorDate: Wed Mar 11 19:55:40 2020 -0400
JUNEAU-185
ResourceDescriptions needs an append(url, label, description) method.
---
.../apache/juneau/BeanConfigAnnotationTest.java | 20 +-
.../java/org/apache/juneau/BeanMapErrorsTest.java | 75 ++++---
.../src/main/java/org/apache/juneau/BeanMap.java | 24 ++-
.../src/main/java/org/apache/juneau/BeanMeta.java | 89 +++++----
.../org/apache/juneau/utils/ReflectionMap.java | 3 +
juneau-doc/docs/ReleaseNotes/8.1.4.html | 19 ++
.../juneau/rest/annotation/RestMethodBpiTest.java | 221 ++++++++++++++++++++-
.../rest/headers/ResourceDescriptionTest.java} | 31 +--
.../juneau/rest/helper/ResourceDescription.java | 39 +++-
.../juneau/rest/helper/ResourceDescriptions.java | 12 ++
10 files changed, 423 insertions(+), 110 deletions(-)
diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/BeanConfigAnnotationTest.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/BeanConfigAnnotationTest.java
index af72ef2..804d55d 100644
--- a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/BeanConfigAnnotationTest.java
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/BeanConfigAnnotationTest.java
@@ -26,10 +26,12 @@ import org.apache.juneau.reflect.*;
import org.apache.juneau.svl.*;
import org.apache.juneau.transform.*;
import org.junit.*;
+import org.junit.runners.*;
/**
* Tests the @BeanConfig annotation.
*/
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class BeanConfigAnnotationTest {
private static void check(String expected, Object o) {
@@ -158,7 +160,7 @@ public class BeanConfigAnnotationTest {
static ClassInfo a = ClassInfo.of(A.class);
@Test
- public void basic() throws Exception {
+ public void a01_basic() throws Exception {
AnnotationList al = a.getAnnotationList();
BeanTraverseSession bc = JsonSerializer.create().applyAnnotations(al, sr).build().createSession();
@@ -209,7 +211,7 @@ public class BeanConfigAnnotationTest {
static ClassInfo b = ClassInfo.of(B.class);
@Test
- public void noValues() throws Exception {
+ public void b01_noValues() throws Exception {
AnnotationList al = b.getAnnotationList();
JsonSerializer bc = JsonSerializer.create().applyAnnotations(al, sr).build();
check("PUBLIC", bc.getBeanClassVisibility());
@@ -261,7 +263,7 @@ public class BeanConfigAnnotationTest {
static ClassInfo c = ClassInfo.of(C.class);
@Test
- public void noAnnotation() throws Exception {
+ public void c01_noAnnotation() throws Exception {
AnnotationList al = c.getAnnotationList();
JsonSerializer bc = JsonSerializer.create().applyAnnotations(al, sr).build();
check("PUBLIC", bc.getBeanClassVisibility());
@@ -327,7 +329,7 @@ public class BeanConfigAnnotationTest {
private static ClassInfo d = ClassInfo.of(D.class);
@Test
- public void beanBpiBpxCombined_noBeanConfig() throws Exception {
+ public void d01_beanBpiBpxCombined_noBeanConfig() throws Exception {
String json = SimpleJson.DEFAULT.toString(D.create());
assertEquals("{a:1,c:3}", json);
D d = SimpleJson.DEFAULT.read(json, D.class);
@@ -336,7 +338,7 @@ public class BeanConfigAnnotationTest {
}
@Test
- public void beanBpiBpxCombined_beanConfigOverride() throws Exception {
+ public void d02_beanBpiBpxCombined_beanConfigOverride() throws Exception {
AnnotationList al = d.getAnnotationList();
JsonSerializer js = JsonSerializer.create().simple().applyAnnotations(al, sr).build();
JsonParser jp = JsonParser.create().applyAnnotations(al, sr).build();
@@ -349,7 +351,7 @@ public class BeanConfigAnnotationTest {
}
@Test
- public void beanBpiBpxCombined_beanContextBuilderOverride() throws Exception {
+ public void d03_beanBpiBpxCombined_beanContextBuilderOverride() throws Exception {
Bean ba = new BeanAnnotation("D").bpi("b,c,d").bpx("c");
JsonSerializer js = JsonSerializer.create().simple().annotations(ba).build();
JsonParser jp = JsonParser.create().annotations(ba).build();
@@ -387,7 +389,7 @@ public class BeanConfigAnnotationTest {
private static ClassInfo e = ClassInfo.of(E.class);
@Test
- public void beanBpiBpxCombined_multipleBeanAnnotations_noBeanConfig() throws Exception {
+ public void e01_beanBpiBpxCombined_multipleBeanAnnotations_noBeanConfig() throws Exception {
String json = SimpleJson.DEFAULT.toString(E.create());
assertEquals("{a:1,c:3}", json);
E e = SimpleJson.DEFAULT.read(json, E.class);
@@ -396,7 +398,7 @@ public class BeanConfigAnnotationTest {
}
@Test
- public void beanBpiBpxCombined_multipleBeanAnnotations_beanConfigOverride() throws Exception {
+ public void e02_beanBpiBpxCombined_multipleBeanAnnotations_beanConfigOverride() throws Exception {
AnnotationList al = e.getAnnotationList();
JsonSerializer js = JsonSerializer.create().simple().applyAnnotations(al, sr).build();
JsonParser jp = JsonParser.create().applyAnnotations(al, sr).build();
@@ -409,7 +411,7 @@ public class BeanConfigAnnotationTest {
}
@Test
- public void beanBpiBpxCombined_multipleBeanAnnotations_beanContextBuilderOverride() throws Exception {
+ public void e03_beanBpiBpxCombined_multipleBeanAnnotations_beanContextBuilderOverride() throws Exception {
Bean ba = new BeanAnnotation("E").bpi("b,c,d").bpx("c");
JsonSerializer js = JsonSerializer.create().simple().annotations(ba).build();
JsonParser jp = JsonParser.create().annotations(ba).build();
diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/BeanMapErrorsTest.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/BeanMapErrorsTest.java
index 9842600..7245423 100644
--- a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/BeanMapErrorsTest.java
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/BeanMapErrorsTest.java
@@ -14,6 +14,8 @@ package org.apache.juneau;
import static org.junit.Assert.*;
+import java.util.stream.*;
+
import org.apache.juneau.annotation.*;
import org.junit.*;
@@ -24,39 +26,47 @@ public class BeanMapErrorsTest {
//-----------------------------------------------------------------------------------------------------------------
// @Beanp(name) on method not in @Bean(properties)
+ // Shouldn't be found in keySet()/entrySet() but should be found in containsKey()/get()
//-----------------------------------------------------------------------------------------------------------------
@Test
public void beanPropertyMethodNotInBeanProperties() {
BeanContext bc = BeanContext.DEFAULT;
- try {
- bc.getClassMeta(A1.class);
- fail();
- } catch (Exception e) {
- assertEquals("org.apache.juneau.BeanMapErrorsTest$A1: Found @Beanp(\"f2\") but name was not found in @Bean(properties)", e.getMessage());
- }
+ BeanMap<A1> bm = bc.createBeanSession().newBeanMap(A1.class);
+ assertTrue(bm.containsKey("f2"));
+ assertEquals(-1, bm.get("f2"));
+ bm.put("f2", -2);
+ assertEquals(-2, bm.get("f2"));
+ assertFalse(bm.keySet().contains("f2"));
+ assertFalse(bm.entrySet().stream().map(x -> x.getKey()).collect(Collectors.toList()).contains("f2"));
}
@Bean(bpi="f1")
public static class A1 {
public int f1;
+ private int f2 = -1;
@Beanp("f2")
public int f2() {
- return -1;
+ return f2;
};
+
+ public void setF2(int f2) {
+ this.f2 = f2;
+ }
}
@Test
public void beanPropertyMethodNotInBeanProperties_usingConfig() {
BeanContext bc = BeanContext.create().applyAnnotations(B1.class).build();
- try {
- bc.getClassMeta(B1.class);
- fail();
- } catch (Exception e) {
- assertEquals("org.apache.juneau.BeanMapErrorsTest$B1: Found @Beanp(\"f2\") but name was not found in @Bean(properties)", e.getMessage());
- }
+ BeanMap<B1> bm = bc.createBeanSession().newBeanMap(B1.class);
+ assertTrue(bm.containsKey("f2"));
+ assertEquals(-1, bm.get("f2"));
+ bm.put("f2", -2);
+ assertEquals(-2, bm.get("f2"));
+ assertFalse(bm.keySet().contains("f2"));
+ assertFalse(bm.entrySet().stream().map(x -> x.getKey()).collect(Collectors.toList()).contains("f2"));
}
@BeanConfig(
@@ -69,10 +79,16 @@ public class BeanMapErrorsTest {
)
public static class B1 {
public int f1;
+ private int f2 = -1;
+ @Beanp("f2")
public int f2() {
- return -1;
+ return f2;
};
+
+ public void setF2(int f2) {
+ this.f2 = f2;
+ }
}
//-----------------------------------------------------------------------------------------------------------------
@@ -82,31 +98,34 @@ public class BeanMapErrorsTest {
public void beanPropertyFieldNotInBeanProperties() {
BeanContext bc = BeanContext.DEFAULT;
- try {
- bc.getClassMeta(A2.class);
- fail();
- } catch (Exception e) {
- assertEquals("org.apache.juneau.BeanMapErrorsTest$A2: Found @Beanp(\"f2\") but name was not found in @Bean(properties)", e.getMessage());
- }
+ BeanMap<A2> bm = bc.createBeanSession().newBeanMap(A2.class);
+ assertTrue(bm.containsKey("f2"));
+ assertEquals(-1, bm.get("f2"));
+ bm.put("f2", -2);
+ assertEquals(-2, bm.get("f2"));
+ assertFalse(bm.keySet().contains("f2"));
+ assertFalse(bm.entrySet().stream().map(x -> x.getKey()).collect(Collectors.toList()).contains("f2"));
}
+
@Bean(bpi="f1")
public static class A2 {
public int f1;
@Beanp("f2")
- public int f2;
+ public int f2 = -1;
}
@Test
public void beanPropertyFieldNotInBeanProperties_usingBeanConfig() {
BeanContext bc = BeanContext.create().applyAnnotations(B2.class).build();
- try {
- bc.getClassMeta(B2.class);
- fail();
- } catch (Exception e) {
- assertEquals("org.apache.juneau.BeanMapErrorsTest$B2: Found @Beanp(\"f2\") but name was not found in @Bean(properties)", e.getMessage());
- }
+ BeanMap<B2> bm = bc.createBeanSession().newBeanMap(B2.class);
+ assertTrue(bm.containsKey("f2"));
+ assertEquals(-1, bm.get("f2"));
+ bm.put("f2", -2);
+ assertEquals(-2, bm.get("f2"));
+ assertFalse(bm.keySet().contains("f2"));
+ assertFalse(bm.entrySet().stream().map(x -> x.getKey()).collect(Collectors.toList()).contains("f2"));
}
@BeanConfig(
@@ -120,6 +139,6 @@ public class BeanMapErrorsTest {
public static class B2 {
public int f1;
- public int f2;
+ public int f2 = -1;
}
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
index b704bc4..28d22cf 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
@@ -155,6 +155,12 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T
if (cm.isOptional() && pMeta.get(this, pMeta.getName()) == null)
pMeta.set(this, pMeta.getName(), cm.getOptionalDefault());
}
+ // Do the same for hidden fields.
+ for (BeanPropertyMeta pMeta : this.meta.hiddenProperties.values()) {
+ ClassMeta<?> cm = pMeta.getClassMeta();
+ if (cm.isOptional() && pMeta.get(this, pMeta.getName()) == null)
+ pMeta.set(this, pMeta.getName(), cm.getOptionalDefault());
+ }
return b;
}
@@ -237,7 +243,7 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T
*/
@Override /* Map */
public Object put(String property, Object value) {
- BeanPropertyMeta p = meta.properties.get(property);
+ BeanPropertyMeta p = getPropertyMeta(property);
if (p == null) {
if (meta.ctx.isIgnoreUnknownBeanProperties())
return null;
@@ -245,7 +251,7 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T
if (property.equals(beanTypePropertyName))
return null;
- p = meta.properties.get("*");
+ p = getPropertyMeta("*");
if (p == null)
throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property);
}
@@ -254,6 +260,13 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T
return p.set(this, property, value);
}
+ @Override /* Map */
+ public boolean containsKey(Object property) {
+ if (getPropertyMeta(emptyIfNull(property)) != null)
+ return true;
+ return super.containsKey(property);
+ }
+
/**
* Add a value to a collection or array property.
*
@@ -265,7 +278,7 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T
* @param value The value to add to the collection or array.
*/
public void add(String property, Object value) {
- BeanPropertyMeta p = meta.properties.get(property);
+ BeanPropertyMeta p = getPropertyMeta(property);
if (p == null) {
if (meta.ctx.isIgnoreUnknownBeanProperties())
return;
@@ -431,10 +444,7 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T
* @return Metadata on the specified property, or <jk>null</jk> if that property does not exist.
*/
public BeanPropertyMeta getPropertyMeta(String propertyName) {
- BeanPropertyMeta bpMeta = meta.properties.get(propertyName);
- if (bpMeta == null)
- bpMeta = meta.dynaProperty;
- return bpMeta;
+ return meta.getPropertyMeta(propertyName);
}
/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
index 5fd5ebb..f24f082 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
@@ -71,6 +71,9 @@ public class BeanMeta<T> {
/** The properties on the target class. */
protected final Map<String,BeanPropertyMeta> properties;
+ /** The hidden properties on the target class. */
+ protected final Map<String,BeanPropertyMeta> hiddenProperties;
+
/** The getter properties on the target class. */
protected final Map<Method,String> getterProps;
@@ -121,6 +124,7 @@ public class BeanMeta<T> {
this.beanFilter = beanFilter;
this.dictionaryName = b.dictionaryName;
this.properties = unmodifiableMap(b.properties);
+ this.hiddenProperties = unmodifiableMap(b.hiddenProperties);
this.getterProps = unmodifiableMap(b.getterProps);
this.setterProps = unmodifiableMap(b.setterProps);
this.dynaProperty = b.dynaProperty;
@@ -139,7 +143,7 @@ public class BeanMeta<T> {
BeanContext ctx;
BeanFilter beanFilter;
String[] pNames;
- Map<String,BeanPropertyMeta> properties;
+ Map<String,BeanPropertyMeta> properties, hiddenProperties = new LinkedHashMap<>();
Map<Method,String> getterProps = new HashMap<>();
Map<Method,String> setterProps = new HashMap<>();
BeanPropertyMeta dynaProperty;
@@ -174,11 +178,13 @@ public class BeanMeta<T> {
List<Class<?>> bdClasses = new ArrayList<>();
if (beanFilter != null && beanFilter.getBeanDictionary() != null)
bdClasses.addAll(Arrays.asList(beanFilter.getBeanDictionary()));
- Bean bean = classMeta.getAnnotation(Bean.class);
- if (bean != null) {
- if (! bean.typeName().isEmpty())
- bdClasses.add(classMeta.innerClass);
- }
+
+ boolean hasTypeName = false;
+ for (Bean b : classMeta.getAnnotationsParentFirst(Bean.class))
+ if (! b.typeName().isEmpty())
+ hasTypeName = true;
+ if (hasTypeName)
+ bdClasses.add(classMeta.innerClass);
this.beanRegistry = new BeanRegistry(ctx, null, bdClasses.toArray(new Class<?>[bdClasses.size()]));
for (Bean b : classMeta.getAnnotationsParentFirst(Bean.class))
@@ -357,8 +363,8 @@ public class BeanMeta<T> {
} else /* Use 'better' introspection */ {
- for (Field f : findBeanFields(ctx, c2, stopClass, fVis, filterProps)) {
- String name = findPropertyName(f, fixedBeanProps);
+ for (Field f : findBeanFields(ctx, c2, stopClass, fVis)) {
+ String name = findPropertyName(f);
if (name != null) {
if (! normalProps.containsKey(name))
normalProps.put(name, BeanPropertyMeta.builder(beanMeta, name));
@@ -366,7 +372,7 @@ public class BeanMeta<T> {
}
}
- List<BeanMethod> bms = findBeanMethods(ctx, c2, stopClass, mVis, fixedBeanProps, filterProps, propertyNamer, fluentSetters);
+ List<BeanMethod> bms = findBeanMethods(ctx, c2, stopClass, mVis, propertyNamer, fluentSetters);
// Iterate through all the getters.
for (BeanMethod bm : bms) {
@@ -481,31 +487,44 @@ public class BeanMeta<T> {
Set<String> bfbpi = beanFilter.getBpi();
Set<String> bfbpx = beanFilter.getBpx();
- if (bpx.isEmpty() && ! bfbpx.isEmpty()) {
-
- for (String k : bfbpx)
- properties.remove(k);
-
- // Only include specified properties if BeanFilter.includeKeys is specified.
- // Note that the order must match includeKeys.
- } else if (! bfbpi.isEmpty()) {
+ if (bpi.isEmpty() && ! bfbpi.isEmpty()) {
+ // Only include specified properties if BeanFilter.includeKeys is specified.
+ // Note that the order must match includeKeys.
Map<String,BeanPropertyMeta> properties2 = new LinkedHashMap<>();
for (String k : bfbpi) {
if (properties.containsKey(k))
- properties2.put(k, properties.get(k));
+ properties2.put(k, properties.remove(k));
}
+ hiddenProperties.putAll(properties);
properties = properties2;
}
+ if (bpx.isEmpty() && ! bfbpx.isEmpty()) {
+ for (String k : bfbpx) {
+ hiddenProperties.put(k, properties.remove(k));
+ }
+ }
+ }
+
+ if (! bpi.isEmpty()) {
+ Map<String,BeanPropertyMeta> properties2 = new LinkedHashMap<>();
+ for (String k : bpi) {
+ if (properties.containsKey(k))
+ properties2.put(k, properties.remove(k));
+ }
+ hiddenProperties.putAll(properties);
+ properties = properties2;
}
for (String ep : bpx)
- properties.remove(ep);
+ hiddenProperties.put(ep, properties.remove(ep));
if (pNames != null) {
Map<String,BeanPropertyMeta> properties2 = new LinkedHashMap<>();
for (String k : pNames) {
if (properties.containsKey(k))
properties2.put(k, properties.get(k));
+ else
+ hiddenProperties.put(k, properties.get(k));
}
properties = properties2;
}
@@ -544,21 +563,15 @@ public class BeanMeta<T> {
* Returns the property name of the specified field if it's a valid property.
* Returns null if the field isn't a valid property.
*/
- private String findPropertyName(Field f, Set<String> fixedBeanProps) {
+ private String findPropertyName(Field f) {
@SuppressWarnings("deprecation")
BeanProperty px = f.getAnnotation(BeanProperty.class);
List<Beanp> lp = ctx.getAnnotations(Beanp.class, f);
List<Name> ln = ctx.getAnnotations(Name.class, f);
String name = bpName(px, lp, ln);
- if (isNotEmpty(name)) {
- if (fixedBeanProps.isEmpty() || fixedBeanProps.contains(name))
- return name;
- return null; // Could happen if filtered via BEAN_bpi/BEAN_bpx.
- }
- name = propertyNamer.getPropertyName(f.getName());
- if (fixedBeanProps.isEmpty() || fixedBeanProps.contains(name))
+ if (isNotEmpty(name))
return name;
- return null;
+ return propertyNamer.getPropertyName(f.getName());
}
}
@@ -670,7 +683,7 @@ public class BeanMeta<T> {
* @param fixedBeanProps Only include methods whose properties are in this list.
* @param pn Use this property namer to determine property names from the method names.
*/
- static final List<BeanMethod> findBeanMethods(BeanContext ctx, Class<?> c, Class<?> stopClass, Visibility v, Set<String> fixedBeanProps, Set<String> filterProps, PropertyNamer pn, boolean fluentSetters) {
+ static final List<BeanMethod> findBeanMethods(BeanContext ctx, Class<?> c, Class<?> stopClass, Visibility v, PropertyNamer pn, boolean fluentSetters) {
List<BeanMethod> l = new LinkedList<>();
for (ClassInfo c2 : findClasses(c, stopClass)) {
@@ -700,9 +713,6 @@ public class BeanMeta<T> {
MethodType methodType = UNKNOWN;
String bpName = bpName(px, lp, ln);
- if (! (isEmpty(bpName) || filterProps.isEmpty() || filterProps.contains(bpName)))
- throw new BeanRuntimeException(c, "Found @Beanp(\"{0}\") but name was not found in @Bean(properties)", bpName);
-
if (pt.size() == 0) {
if ("*".equals(bpName)) {
if (rt.isChildOf(Collection.class)) {
@@ -769,12 +779,8 @@ public class BeanMeta<T> {
throw new BeanRuntimeException(c, "Found @Beanp(\"*\") but could not determine method type on method ''{0}''.", m.getSimpleName());
if (methodType != UNKNOWN) {
- if (bpName != null && ! bpName.isEmpty()) {
+ if (bpName != null && ! bpName.isEmpty())
n = bpName;
- if (! fixedBeanProps.isEmpty())
- if (! fixedBeanProps.contains(n))
- n = null; // Could happen if filtered via BEAN_bpi/BEAN_bpx
- }
if (n != null)
l.add(new BeanMethod(n, methodType, m.inner()));
}
@@ -783,7 +789,7 @@ public class BeanMeta<T> {
return l;
}
- static final Collection<Field> findBeanFields(BeanContext ctx, Class<?> c, Class<?> stopClass, Visibility v, Set<String> filterProps) {
+ static final Collection<Field> findBeanFields(BeanContext ctx, Class<?> c, Class<?> stopClass, Visibility v) {
List<Field> l = new LinkedList<>();
for (ClassInfo c2 : findClasses(c, stopClass)) {
for (FieldInfo f : c2.getDeclaredFields()) {
@@ -795,15 +801,10 @@ public class BeanMeta<T> {
@SuppressWarnings("deprecation")
BeanProperty px = f.getAnnotation(BeanProperty.class);
List<Beanp> lp = ctx.getAnnotations(Beanp.class, f);
- List<Name> ln = ctx.getAnnotations(Name.class, f);
- String bpName = bpName(px, lp, ln);
if (! (v.isVisible(f.inner()) || px != null || lp.size() > 0))
continue;
- if (! (isEmpty(bpName) || filterProps.isEmpty() || filterProps.contains(bpName)))
- throw new BeanRuntimeException(c, "Found @Beanp(\"{0}\") but name was not found in @Bean(properties)", bpName);
-
l.add(f.inner());
}
}
@@ -872,6 +873,8 @@ public class BeanMeta<T> {
public BeanPropertyMeta getPropertyMeta(String name) {
BeanPropertyMeta bpm = properties.get(name);
if (bpm == null)
+ bpm = hiddenProperties.get(name);
+ if (bpm == null)
bpm = dynaProperty;
return bpm;
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ReflectionMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ReflectionMap.java
index 85150b4..63f76f5 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ReflectionMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ReflectionMap.java
@@ -174,6 +174,7 @@ public class ReflectionMap<V> {
* <ul>
* <li>Full class name (e.g. <js>"com.foo.MyClass"</js>).
* <li>Simple class name (e.g. <js>"MyClass"</js>).
+ * <li>All classes (e.g. <js>"*"</js>).
* <li>Full method name (e.g. <js>"com.foo.MyClass.myMethod"</js>).
* <li>Simple method name (e.g. <js>"MyClass.myMethod"</js>).
* <li>A comma-delimited list of anything on this list.
@@ -601,6 +602,8 @@ public class ReflectionMap<V> {
String cSimple = c.getSimpleName(), cFull = c.getName();
if (isEquals(simpleName, cSimple) || isEquals(fullName, cFull))
return true;
+ if ("*".equals(simpleName))
+ return true;
if (cFull.indexOf('$') != -1) {
Package p = c.getPackage();
if (p != null)
diff --git a/juneau-doc/docs/ReleaseNotes/8.1.4.html b/juneau-doc/docs/ReleaseNotes/8.1.4.html
index cbb7676..8afc505 100644
--- a/juneau-doc/docs/ReleaseNotes/8.1.4.html
+++ b/juneau-doc/docs/ReleaseNotes/8.1.4.html
@@ -110,6 +110,23 @@
String json = ws.toString(addressBean); <jc>// Will print street,city,state</jc>
</p>
<li>
+ Bean maps now have the concept of "hidden" properties (properties that aren't serialized but otherwise accessible).
+ <br>For example, the {@link oaj.html.annotation.Html#link()} can now reference hidden properties:
+ <p class='bpcode w800'>
+ <ja>@Bean</ja>(bpi=<js>"a"</js>) <jc>// Will be overridden</jc>
+ <jk>public class</jk> MyBean {
+
+ <ja>@Html</ja>(link=<js>"servlet:/{b}"</js>)
+ <jk>public</jk> String <jf>a</jf>;
+
+ <jk>public</jk> String <jf>b</jf>; <jc>// Not serialized, but referenced in link on a.</jc>
+
+ }
+ </p>
+ <br>
+ The general rule for the {@link oaj.BeanMap} class is that <c>get()</c>,<c>put()</c>, and <c>containsKey()</c>
+ will work against hidden properties, but <c>keySet()</c> and <c>entrySet()</c> will skip them.
+ <li>
Several bug fixes in the {@link HtmlSerializer} and {@link HtmlParser} classes around the handling of
collections and arrays of beans with <c><ja>@Bean</ja>(typeName)</c> annotations.
<li>
@@ -137,6 +154,8 @@
the <c><ja>@ConfigurationContext</ja>(initializers=JuneauRestInitializer.<jk>class</jk>)</c> when unit testing
using <ja>@SpringBootTest</ja>.
<li>
+ New {@link oajr.helper.ResourceDescription(String,String,String)} constructor and {@link oajr.helper.ResourceDescriptions#append(String,String,String)} method.
+ <li>
New {@link oajr.helper.Hyperlink} class.
</ul>
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestMethodBpiTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestMethodBpiTest.java
index affdbc8..6e87579 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestMethodBpiTest.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestMethodBpiTest.java
@@ -43,6 +43,21 @@ public class RestMethodBpiTest {
public Object a03() throws Exception {
return new MyBeanA().init();
}
+ @RestMethod
+ @BeanConfig(bpi="MyBeanA: a,_b")
+ public Object a04() throws Exception {
+ return new MyBeanA().init();
+ }
+ @RestMethod
+ @BeanConfig(bpi="MyBeanA: a")
+ public Object a05() throws Exception {
+ return new MyBeanA().init();
+ }
+ @RestMethod
+ @BeanConfig(bpi="MyBeanA: _b")
+ public Object a06() throws Exception {
+ return new MyBeanA().init();
+ }
}
static MockRest a = MockRest.build(A.class);
@@ -71,6 +86,31 @@ public class RestMethodBpiTest {
a.get("/a03").urlEnc().execute().assertBody("_b=foo");
}
+ @Test
+ public void a04() throws Exception {
+ a.get("/a04").json().execute().assertBody("{\"a\":1,\"_b\":\"foo\"}");
+ a.get("/a04").xml().execute().assertBodyContains("<object><a>1</a><_b>foo</_b></object>");
+ a.get("/a04").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr><tr><td>_b</td><td>foo</td></tr></table>");
+ a.get("/a04").uon().execute().assertBody("(a=1,_b=foo)");
+ a.get("/a04").urlEnc().execute().assertBody("a=1&_b=foo");
+ }
+ @Test
+ public void a05() throws Exception {
+ a.get("/a05").json().execute().assertBody("{\"a\":1}");
+ a.get("/a05").xml().execute().assertBodyContains("<object><a>1</a></object>");
+ a.get("/a05").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr></table>");
+ a.get("/a05").uon().execute().assertBody("(a=1)");
+ a.get("/a05").urlEnc().execute().assertBody("a=1");
+ }
+ @Test
+ public void a06() throws Exception {
+ a.get("/a06").json().execute().assertBody("{\"_b\":\"foo\"}");
+ a.get("/a06").xml().execute().assertBodyContains("<object><_b>foo</_b></object>");
+ a.get("/a06").html().execute().assertBodyContains("<table><tr><td>_b</td><td>foo</td></tr></table>");
+ a.get("/a06").uon().execute().assertBody("(_b=foo)");
+ a.get("/a06").urlEnc().execute().assertBody("_b=foo");
+ }
+
//=================================================================================================================
// BPX on normal bean
//=================================================================================================================
@@ -90,6 +130,21 @@ public class RestMethodBpiTest {
public Object b03() throws Exception {
return new MyBeanA().init();
}
+ @RestMethod
+ @BeanConfig(bpx="MyBeanA: a,_b")
+ public Object b04() throws Exception {
+ return new MyBeanA().init();
+ }
+ @RestMethod
+ @BeanConfig(bpx="MyBeanA: a")
+ public Object b05() throws Exception {
+ return new MyBeanA().init();
+ }
+ @RestMethod
+ @BeanConfig(bpx="MyBeanA: _b")
+ public Object b06() throws Exception {
+ return new MyBeanA().init();
+ }
}
static MockRest b = MockRest.build(B.class);
@@ -117,6 +172,30 @@ public class RestMethodBpiTest {
b.get("/b03").uon().execute().assertBody("(a=1)");
b.get("/b03").urlEnc().execute().assertBody("a=1");
}
+ @Test
+ public void b04() throws Exception {
+ b.get("/b04").json().execute().assertBody("{}");
+ b.get("/b04").xml().execute().assertBodyContains("<object/>");
+ b.get("/b04").html().execute().assertBodyContains("<table></table>");
+ b.get("/b04").uon().execute().assertBody("()");
+ b.get("/b04").urlEnc().execute().assertBody("");
+ }
+ @Test
+ public void b05() throws Exception {
+ b.get("/b05").json().execute().assertBody("{\"_b\":\"foo\"}");
+ b.get("/b05").xml().execute().assertBodyContains("<object><_b>foo</_b></object>");
+ b.get("/b05").html().execute().assertBodyContains("<table><tr><td>_b</td><td>foo</td></tr></table>");
+ b.get("/b05").uon().execute().assertBody("(_b=foo)");
+ b.get("/b05").urlEnc().execute().assertBody("_b=foo");
+ }
+ @Test
+ public void b06() throws Exception {
+ b.get("/b06").json().execute().assertBody("{\"a\":1}");
+ b.get("/b06").xml().execute().assertBodyContains("<object><a>1</a></object>");
+ b.get("/b06").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr></table>");
+ b.get("/b06").uon().execute().assertBody("(a=1)");
+ b.get("/b06").urlEnc().execute().assertBody("a=1");
+ }
//=================================================================================================================
// BPI on bean using @Bean(properties)
@@ -137,16 +216,31 @@ public class RestMethodBpiTest {
public Object c03() throws Exception {
return new MyBeanB().init();
}
+ @RestMethod
+ @BeanConfig(bpi="MyBeanB: a,_b")
+ public Object c04() throws Exception {
+ return new MyBeanB().init();
+ }
+ @RestMethod
+ @BeanConfig(bpi="MyBeanB: a")
+ public Object c05() throws Exception {
+ return new MyBeanB().init();
+ }
+ @RestMethod
+ @BeanConfig(bpi="MyBeanB: _b")
+ public Object c06() throws Exception {
+ return new MyBeanB().init();
+ }
}
static MockRest c = MockRest.build(C.class);
@Test
public void c01() throws Exception {
- c.get("/c01").json().execute().assertBody("{\"_b\":\"foo\",\"a\":1}");
- c.get("/c01").xml().execute().assertBodyContains("<object><_b>foo</_b><a>1</a></object>");
- c.get("/c01").html().execute().assertBodyContains("<table><tr><td>_b</td><td>foo</td></tr><tr><td>a</td><td>1</td></tr></table>");
- c.get("/c01").uon().execute().assertBody("(_b=foo,a=1)");
- c.get("/c01").urlEnc().execute().assertBody("_b=foo&a=1");
+ c.get("/c01").json().execute().assertBody("{\"a\":1,\"_b\":\"foo\"}");
+ c.get("/c01").xml().execute().assertBodyContains("<object><a>1</a><_b>foo</_b></object>");
+ c.get("/c01").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr><tr><td>_b</td><td>foo</td></tr></table>");
+ c.get("/c01").uon().execute().assertBody("(a=1,_b=foo)");
+ c.get("/c01").urlEnc().execute().assertBody("a=1&_b=foo");
}
@Test
public void c02() throws Exception {
@@ -164,6 +258,30 @@ public class RestMethodBpiTest {
c.get("/c03").uon().execute().assertBody("(_b=foo)");
c.get("/c03").urlEnc().execute().assertBody("_b=foo");
}
+ @Test
+ public void c04() throws Exception {
+ c.get("/c04").json().execute().assertBody("{\"a\":1,\"_b\":\"foo\"}");
+ c.get("/c04").xml().execute().assertBodyContains("<object><a>1</a><_b>foo</_b></object>");
+ c.get("/c04").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr><tr><td>_b</td><td>foo</td></tr></table>");
+ c.get("/c04").uon().execute().assertBody("(a=1,_b=foo)");
+ c.get("/c04").urlEnc().execute().assertBody("a=1&_b=foo");
+ }
+ @Test
+ public void c05() throws Exception {
+ c.get("/c05").json().execute().assertBody("{\"a\":1}");
+ c.get("/c05").xml().execute().assertBodyContains("<object><a>1</a></object>");
+ c.get("/c05").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr></table>");
+ c.get("/c05").uon().execute().assertBody("(a=1)");
+ c.get("/c05").urlEnc().execute().assertBody("a=1");
+ }
+ @Test
+ public void c06() throws Exception {
+ c.get("/c06").json().execute().assertBody("{\"_b\":\"foo\"}");
+ c.get("/c06").xml().execute().assertBodyContains("<object><_b>foo</_b></object>");
+ c.get("/c06").html().execute().assertBodyContains("<table><tr><td>_b</td><td>foo</td></tr></table>");
+ c.get("/c06").uon().execute().assertBody("(_b=foo)");
+ c.get("/c06").urlEnc().execute().assertBody("_b=foo");
+ }
//=================================================================================================================
// BPX on bean using @Bean(properties)
@@ -184,6 +302,21 @@ public class RestMethodBpiTest {
public Object d03() throws Exception {
return new MyBeanB().init();
}
+ @RestMethod
+ @BeanConfig(bpx="MyBeanB: a,_b")
+ public Object d04() throws Exception {
+ return new MyBeanB().init();
+ }
+ @RestMethod
+ @BeanConfig(bpx="MyBeanB: a")
+ public Object d05() throws Exception {
+ return new MyBeanB().init();
+ }
+ @RestMethod
+ @BeanConfig(bpx="MyBeanB: _b")
+ public Object d06() throws Exception {
+ return new MyBeanB().init();
+ }
}
static MockRest d = MockRest.build(D.class);
@@ -211,6 +344,30 @@ public class RestMethodBpiTest {
d.get("/d03").uon().execute().assertBody("(a=1)");
d.get("/d03").urlEnc().execute().assertBody("a=1");
}
+ @Test
+ public void d04() throws Exception {
+ d.get("/d04").json().execute().assertBody("{}");
+ d.get("/d04").xml().execute().assertBodyContains("<object/>");
+ d.get("/d04").html().execute().assertBodyContains("<table></table>");
+ d.get("/d04").uon().execute().assertBody("()");
+ d.get("/d04").urlEnc().execute().assertBody("");
+ }
+ @Test
+ public void d05() throws Exception {
+ d.get("/d05").json().execute().assertBody("{\"_b\":\"foo\"}");
+ d.get("/d05").xml().execute().assertBodyContains("<object><_b>foo</_b></object>");
+ d.get("/d05").html().execute().assertBodyContains("<table><tr><td>_b</td><td>foo</td></tr></table>");
+ d.get("/d05").uon().execute().assertBody("(_b=foo)");
+ d.get("/d05").urlEnc().execute().assertBody("_b=foo");
+ }
+ @Test
+ public void d06() throws Exception {
+ d.get("/d06").json().execute().assertBody("{\"a\":1}");
+ d.get("/d06").xml().execute().assertBodyContains("<object><a>1</a></object>");
+ d.get("/d06").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr></table>");
+ d.get("/d06").uon().execute().assertBody("(a=1)");
+ d.get("/d06").urlEnc().execute().assertBody("a=1");
+ }
//=================================================================================================================
// BPI meta-matching
@@ -223,6 +380,11 @@ public class RestMethodBpiTest {
public Object e01() throws Exception {
return new MyBeanA().init();
}
+ @RestMethod
+ @BeanConfig(bpi="*: a")
+ public Object e02() throws Exception {
+ return new MyBeanA().init();
+ }
}
static MockRest e = MockRest.build(E.class);
@@ -234,6 +396,14 @@ public class RestMethodBpiTest {
e.get("/e01").uon().execute().assertBody("(a=1)");
e.get("/e01").urlEnc().execute().assertBody("a=1");
}
+ @Test
+ public void e02() throws Exception {
+ e.get("/e02").json().execute().assertBody("{\"a\":1}");
+ e.get("/e02").xml().execute().assertBodyContains("<object><a>1</a></object>");
+ e.get("/e02").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr></table>");
+ e.get("/e02").uon().execute().assertBody("(a=1)");
+ e.get("/e02").urlEnc().execute().assertBody("a=1");
+ }
//=================================================================================================================
// BPI fully-qualified class name
@@ -246,6 +416,11 @@ public class RestMethodBpiTest {
public Object f01() throws Exception {
return new MyBeanA().init();
}
+ @RestMethod
+ @BeanConfig(bpi="org.apache.juneau.rest.annotation.RestMethodBpiTest$MyBeanA: a")
+ public Object f02() throws Exception {
+ return new MyBeanA().init();
+ }
}
static MockRest f = MockRest.build(F.class);
@@ -257,6 +432,14 @@ public class RestMethodBpiTest {
f.get("/f01").uon().execute().assertBody("(a=1)");
f.get("/f01").urlEnc().execute().assertBody("a=1");
}
+ @Test
+ public void f02() throws Exception {
+ f.get("/f02").json().execute().assertBody("{\"a\":1}");
+ f.get("/f02").xml().execute().assertBodyContains("<object><a>1</a></object>");
+ f.get("/f02").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr></table>");
+ f.get("/f02").uon().execute().assertBody("(a=1)");
+ f.get("/f02").urlEnc().execute().assertBody("a=1");
+ }
//=================================================================================================================
// Negative matching
@@ -275,6 +458,18 @@ public class RestMethodBpiTest {
// Should not match. We don't support meta-matches in class names.
return new MyBeanA().init();
}
+ @RestMethod
+ @BeanConfig(bpi="MyBean: a")
+ public Object g03() throws Exception {
+ // Should not match.
+ return new MyBeanA().init();
+ }
+ @RestMethod
+ @BeanConfig(bpi="MyBean*: a")
+ public Object g04() throws Exception {
+ // Should not match. We don't support meta-matches in class names.
+ return new MyBeanA().init();
+ }
}
static MockRest g = MockRest.build(G.class);
@@ -294,6 +489,22 @@ public class RestMethodBpiTest {
g.get("/g02").uon().execute().assertBody("(a=1,_b=foo)");
g.get("/g02").urlEnc().execute().assertBody("a=1&_b=foo");
}
+ @Test
+ public void g03() throws Exception {
+ g.get("/g03").json().execute().assertBody("{\"a\":1,\"_b\":\"foo\"}");
+ g.get("/g03").xml().execute().assertBodyContains("<object><a>1</a><_b>foo</_b></object>");
+ g.get("/g03").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr><tr><td>_b</td><td>foo</td></tr></table>");
+ g.get("/g03").uon().execute().assertBody("(a=1,_b=foo)");
+ g.get("/g03").urlEnc().execute().assertBody("a=1&_b=foo");
+ }
+ @Test
+ public void g04() throws Exception {
+ g.get("/g04").json().execute().assertBody("{\"a\":1,\"_b\":\"foo\"}");
+ g.get("/g04").xml().execute().assertBodyContains("<object><a>1</a><_b>foo</_b></object>");
+ g.get("/g04").html().execute().assertBodyContains("<table><tr><td>a</td><td>1</td></tr><tr><td>_b</td><td>foo</td></tr></table>");
+ g.get("/g04").uon().execute().assertBody("(a=1,_b=foo)");
+ g.get("/g04").urlEnc().execute().assertBody("a=1&_b=foo");
+ }
//=================================================================================================================
// Beans
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescriptions.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/headers/ResourceDescriptionTest.java
similarity index 67%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescriptions.java
copy to juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/headers/ResourceDescriptionTest.java
index e9a6143..bb192f6 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescriptions.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/headers/ResourceDescriptionTest.java
@@ -10,25 +10,26 @@
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
// * specific language governing permissions and limitations under the License. *
// ***************************************************************************************************************************
-package org.apache.juneau.rest.helper;
+package org.apache.juneau.rest.headers;
-import java.util.*;
+import static org.junit.Assert.*;
+
+import org.apache.juneau.marshall.*;
+import org.apache.juneau.rest.helper.*;
+import org.junit.*;
+import org.junit.runners.*;
/**
- * A list of {@link ResourceDescription} objects.
+ * Validates the handling of the Accept-Charset header.
*/
-public class ResourceDescriptions extends ArrayList<ResourceDescription> {
- private static final long serialVersionUID = 1L;
+@SuppressWarnings({})
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class ResourceDescriptionTest {
- /**
- * Adds a new {@link ResourceDescription} to this list.
- *
- * @param name The name of the child resource.
- * @param description The description of the child resource.
- * @return This object (for method chaining).
- */
- public ResourceDescriptions append(String name, String description) {
- super.add(new ResourceDescription(name, description));
- return this;
+ @Test
+ public void a01_basic() throws Exception {
+ ResourceDescription rd = new ResourceDescription("a","b?c=d&e=f","g");
+ assertEquals("<table><tr><td>name</td><td><a href=\"/b?c=d&e=f\">a</a></td></tr><tr><td>description</td><td>g</td></tr></table>", Html.DEFAULT.toString(rd));
+ assertEquals("{name:'a',description:'g'}", SimpleJson.DEFAULT.toString(rd));
}
}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescription.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescription.java
index b7f3e21..2801dab 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescription.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescription.java
@@ -36,16 +36,30 @@ import org.apache.juneau.jsonschema.annotation.Schema;
@Response(schema=@Schema(ignore=true))
public final class ResourceDescription implements Comparable<ResourceDescription> {
- private String name, description;
+ private String name, uri, description;
/**
- * Constructor.
+ * Constructor for when the name and uri are the same.
*
* @param name The name of the child resource.
* @param description The description of the child resource.
*/
public ResourceDescription(String name, String description) {
this.name = name;
+ this.uri = name;
+ this.description = description;
+ }
+
+ /**
+ * Constructor for when the name and uri are different.
+ *
+ * @param name The name of the child resource.
+ * @param uri The uri of the child resource.
+ * @param description The description of the child resource.
+ */
+ public ResourceDescription(String name, String uri, String description) {
+ this.name = name;
+ this.uri = uri;
this.description = description;
}
@@ -57,12 +71,21 @@ public final class ResourceDescription implements Comparable<ResourceDescription
*
* @return The name.
*/
- @Html(link="servlet:/{name}")
+ @Html(link="servlet:/{uri}")
public String getName() {
return name;
}
/**
+ * Returns the uri on this label.
+ *
+ * @return The name.
+ */
+ public String getUri() {
+ return uri == null ? name : uri;
+ }
+
+ /**
* Sets the name field on this label to a new value.
*
* @param name The new name.
@@ -93,6 +116,16 @@ public final class ResourceDescription implements Comparable<ResourceDescription
return this;
}
+ /**
+ * Sets the uri field on this label to a new value.
+ *
+ * @param uri The new uri.
+ * @return This object (for method chaining).
+ */
+ public ResourceDescription uri(String uri) {
+ this.uri = uri;
+ return this;
+ }
@Override /* Comparable */
public int compareTo(ResourceDescription o) {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescriptions.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescriptions.java
index e9a6143..9cf9fcf 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescriptions.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ResourceDescriptions.java
@@ -31,4 +31,16 @@ public class ResourceDescriptions extends ArrayList<ResourceDescription> {
super.add(new ResourceDescription(name, description));
return this;
}
+ /**
+ * Adds a new {@link ResourceDescription} to this list when the uri is different from the name.
+ *
+ * @param name The name of the child resource.
+ * @param uri The URI of the child resource.
+ * @param description The description of the child resource.
+ * @return This object (for method chaining).
+ */
+ public ResourceDescriptions append(String name, String uri, String description) {
+ super.add(new ResourceDescription(name, uri, description));
+ return this;
+ }
}