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&amp;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;
+	}
 }