You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by th...@apache.org on 2021/11/17 22:18:59 UTC

[tapestry-5] branch rest updated: TAP5-2696: bunch of changes.

This is an automated email from the ASF dual-hosted git repository.

thiagohp pushed a commit to branch rest
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git


The following commit(s) were added to refs/heads/rest by this push:
     new 6042106  TAP5-2696: bunch of changes.
6042106 is described below

commit 60421061d2ea188e2c985e2f2b2d63fa1f835f58
Author: Thiago H. de Paula Figueiredo <th...@arsmachina.com.br>
AuthorDate: Wed Nov 17 19:18:15 2021 -0300

    TAP5-2696: bunch of changes.
    
    Added MappedEntitiesService
    Added OpenAPI base path configuration symbol
    Added @RestInfo with information about produced and consumed MIME types
    to OpenAPI description, plus the expected return type of event handler
    methods
    Added OpenApiTypeDescriber
    Added HttpStatus to be a more generic version of HttpError
---
 .../java/org/apache/tapestry5/SymbolConstants.java |  15 +-
 .../org/apache/tapestry5/annotations/RestInfo.java |  60 ++++++
 .../HttpStatusComponentEventResultProcessor.java   |  54 ++++++
 .../services/OpenApiDescriptionDispatcher.java     |   2 +-
 .../rest/DefaultOpenApiDescriptionGenerator.java   | 187 ++++++++++++++++--
 .../services/rest/DefaultOpenApiTypeDescriber.java | 210 +++++++++++++++++++++
 .../services/rest/MappedEntityManagerImpl.java     |  67 +++++++
 .../internal/services/rest/package-info.java}      |  41 ++--
 .../apache/tapestry5/modules/TapestryModule.java   |  24 ++-
 .../org/apache/tapestry5/services/HttpError.java   |   2 +
 .../org/apache/tapestry5/services/HttpStatus.java  | 130 +++++++++++++
 .../services/rest/MappedEntityManager.java         |  39 ++++
 .../services/rest/OpenApiDescriptionGenerator.java |   4 +-
 .../services/rest/OpenApiTypeDescriber.java        |  56 ++++++
 .../tapestry5/services/rest/package-info.java}     |  15 +-
 tapestry-core/src/test/app1/WEB-INF/app.properties |  20 +-
 .../integration/app1/base/BaseRestDemoPage.java    |  22 +++
 .../integration/app1/base/EmptySuperclass.java     |   2 +-
 .../integration/app1/data/rest/entities/Point.java |  35 ++++
 .../tapestry5/integration/app1/pages/Index.java    |   4 +-
 .../app1/pages/OpenApiDescriptionDemo.java         |   2 +-
 .../app1/pages/rest/RestRequestNotHandledDemo.java |   2 +-
 .../app1/pages/rest/RestTypeDescriptionsDemo.java  |  99 ++++++++++
 .../rest/RestWithEventHandlerMethodNameDemo.java   |   2 +-
 .../app1/pages/rest/RestWithOnEventDemo.java       |   2 +-
 .../integration/app1/services/AppModule.java       |   8 +
 .../tapestry5/integration/rest/RestTests.java      |  85 ++++++---
 .../modules/TapestryOpenApiViewerModule.java       |   9 +
 28 files changed, 1106 insertions(+), 92 deletions(-)

diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
index 5caa9a4..4d65f64 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
@@ -20,15 +20,16 @@ import org.apache.tapestry5.corelib.components.BeanEditor;
 import org.apache.tapestry5.corelib.components.Errors;
 import org.apache.tapestry5.corelib.mixins.FormGroup;
 import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
+import org.apache.tapestry5.http.services.BaseURLSource;
 import org.apache.tapestry5.internal.services.AssetDispatcher;
-import org.apache.tapestry5.internal.services.DefaultOpenApiDescriptionGenerator;
+import org.apache.tapestry5.internal.services.rest.DefaultOpenApiDescriptionGenerator;
 import org.apache.tapestry5.modules.NoBootstrapModule;
 import org.apache.tapestry5.services.Html5Support;
-import org.apache.tapestry5.services.OpenApiDescriptionGenerator;
 import org.apache.tapestry5.services.assets.AssetPathConstructor;
 import org.apache.tapestry5.services.assets.ResourceMinimizer;
 import org.apache.tapestry5.services.compatibility.Trait;
 import org.apache.tapestry5.services.javascript.JavaScriptStack;
+import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator;
 
 /**
  * Defines the names of symbols used to configure Tapestry.
@@ -700,5 +701,15 @@ public class SymbolConstants
      * @since 5.8.0
      */
     public static final String OPENAPI_DESCRIPTION_PATH = "tapestry.openapi-description-path";
+    
+    /**
+     * Defines a base path to the generated OpenAPI description relative to the application
+     * URL as defined by {@link BaseURLSource#getBaseURL(boolean)}. It should be either
+     * the empty string, meaning there's no base path, or a string starting and ending 
+     * with a slash. Default value is "/" (without the quotes)
+     * @see OpenApiDescriptionGenerator
+     * @since 5.8.0
+     */
+    public static final String OPENAPI_BASE_PATH = "tapestry.openapi-base-path";
 
 }
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/annotations/RestInfo.java b/tapestry-core/src/main/java/org/apache/tapestry5/annotations/RestInfo.java
new file mode 100644
index 0000000..398236e
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/annotations/RestInfo.java
@@ -0,0 +1,60 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "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.tapestry5.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.apache.tapestry5.ioc.annotations.AnnotationUseContext.PAGE;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.apache.tapestry5.ioc.annotations.UseWith;
+import org.apache.tapestry5.services.HttpError;
+import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator;
+import org.apache.tapestry5.services.rest.OpenApiTypeDescriber;
+
+/**
+ * Annotation that provides some information about REST event handler methods for OpenAPI 
+ * description generation. It can be used in methods and also in pages to define defaults
+ * used by all methods without the annotation, except for {{@link #returnedType()}.
+ * 
+ * @see OpenApiDescriptionGenerator
+ * @see OpenApiTypeDescriber
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RUNTIME)
+@Documented
+@UseWith({PAGE})
+public @interface RestInfo
+{
+    /**
+     * Defines the request body media types supported by the annotated REST event handler method.
+     */
+    String[] consumes() default "";
+
+    /**
+     * Defines the media types of the responses provided by the annotated REST event handler method in 
+     * successful requests.
+     */
+    String[] produces() default "";
+
+    /**
+     * Defines the returned type of this REST event handler method in successful requests.
+     * This is particularly useful for methods that have Object return type because
+     * they may return non-response objects for error reasons, such as {@link HttpError}.
+     */
+    Class<?> returnedType() default Object.class;
+    
+}
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/HttpStatusComponentEventResultProcessor.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/HttpStatusComponentEventResultProcessor.java
new file mode 100644
index 0000000..676c9f8
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/HttpStatusComponentEventResultProcessor.java
@@ -0,0 +1,54 @@
+// Copyright 2021 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "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.tapestry5.internal.services;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.tapestry5.http.services.Response;
+import org.apache.tapestry5.services.ComponentEventResultProcessor;
+import org.apache.tapestry5.services.HttpStatus;
+
+/**
+ * Handles {@link HttpStatus}.values returned by event handler methods.
+ *
+ * @since 5.8.0
+ */
+public class HttpStatusComponentEventResultProcessor implements ComponentEventResultProcessor<HttpStatus>
+{
+    private final Response response;
+
+    public HttpStatusComponentEventResultProcessor(Response response)
+    {
+        this.response = response;
+    }
+
+    public void processResultValue(HttpStatus value) throws IOException
+    {
+        response.setStatus(value.getStatusCode());
+        final Map<String, String> extraHttpHeaders = value.getExtraHttpHeaders();
+        for (String header : extraHttpHeaders.keySet())
+        {
+            final String headerValue = extraHttpHeaders.get(header);
+            response.setHeader(header, headerValue);
+        }
+        if (value.getMessage() != null)
+        {
+            Objects.requireNonNull(value.getContentType(), "HttpStatus.mimeType cannot be null");
+            response.getPrintWriter(value.getContentType()).append(value.getMessage()).close();
+        }
+    }
+}
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/OpenApiDescriptionDispatcher.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/OpenApiDescriptionDispatcher.java
index fc2f848..34ed586 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/OpenApiDescriptionDispatcher.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/OpenApiDescriptionDispatcher.java
@@ -23,7 +23,7 @@ import org.apache.tapestry5.http.services.Request;
 import org.apache.tapestry5.http.services.Response;
 import org.apache.tapestry5.ioc.annotations.Symbol;
 import org.apache.tapestry5.json.JSONObject;
-import org.apache.tapestry5.services.OpenApiDescriptionGenerator;
+import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator;
 
 /**
  * Recognizes requests where the path matches the value of {@link SymbolConstants#OPENAPI_DESCRIPTION_PATH}
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiDescriptionGenerator.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiDescriptionGenerator.java
index f32c17f..5f556bf 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiDescriptionGenerator.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiDescriptionGenerator.java
@@ -14,14 +14,17 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
-package org.apache.tapestry5.internal.services;
+package org.apache.tapestry5.internal.services.rest;
 
 import java.lang.reflect.Method;
 import java.lang.reflect.Parameter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import javax.servlet.http.HttpServletResponse;
@@ -30,12 +33,14 @@ import org.apache.tapestry5.SymbolConstants;
 import org.apache.tapestry5.annotations.ActivationContextParameter;
 import org.apache.tapestry5.annotations.OnEvent;
 import org.apache.tapestry5.annotations.RequestParameter;
+import org.apache.tapestry5.annotations.RestInfo;
 import org.apache.tapestry5.annotations.StaticActivationContextValue;
 import org.apache.tapestry5.commons.Messages;
 import org.apache.tapestry5.commons.util.CommonsUtils;
 import org.apache.tapestry5.http.services.BaseURLSource;
 import org.apache.tapestry5.http.services.Request;
 import org.apache.tapestry5.internal.InternalConstants;
+import org.apache.tapestry5.internal.services.PageSource;
 import org.apache.tapestry5.internal.structure.Page;
 import org.apache.tapestry5.ioc.services.SymbolSource;
 import org.apache.tapestry5.ioc.services.ThreadLocale;
@@ -44,9 +49,11 @@ import org.apache.tapestry5.json.JSONObject;
 import org.apache.tapestry5.model.ComponentModel;
 import org.apache.tapestry5.runtime.Component;
 import org.apache.tapestry5.services.ComponentClassResolver;
-import org.apache.tapestry5.services.OpenApiDescriptionGenerator;
 import org.apache.tapestry5.services.PageRenderLinkSource;
 import org.apache.tapestry5.services.messages.ComponentMessagesSource;
+import org.apache.tapestry5.services.rest.MappedEntityManager;
+import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator;
+import org.apache.tapestry5.services.rest.OpenApiTypeDescriber;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -61,6 +68,8 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
     
     final private static Logger LOGGER = LoggerFactory.getLogger(DefaultOpenApiDescriptionGenerator.class);
     
+    final private OpenApiTypeDescriber typeDescriber;
+    
     final private BaseURLSource baseUrlSource;
     
     final private SymbolSource symbolSource;
@@ -79,9 +88,17 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
     
     final private Request request;
     
+    final Set<Class<?>> entities;
+    
     final private static String KEY_PREFIX = "openapi.";
     
+    final private String basePath;
+    
+    private final Map<String, Class<?>> stringToClassMap = new HashMap<>();
+    
     public DefaultOpenApiDescriptionGenerator(
+            final OpenApiTypeDescriber typeDescriber,
+            final MappedEntityManager mappedEntityManager,
             final BaseURLSource baseUrlSource, 
             final SymbolSource symbolSource, 
             final ComponentMessagesSource componentMessagesSource,
@@ -92,6 +109,8 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
             final Request request) 
     {
         super();
+        
+        this.typeDescriber = typeDescriber;
         this.baseUrlSource = baseUrlSource;
         this.symbolSource = symbolSource;
         this.componentMessagesSource = componentMessagesSource;
@@ -100,7 +119,33 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
         this.componentClassResolver = componentClassResolver;
         this.pageRenderLinkSource = pageRenderLinkSource;
         this.request = request;
+        entities = mappedEntityManager.getEntities();
+        
         messages = new ThreadLocal<>();
+        basePath = symbolSource.valueForSymbol(SymbolConstants.OPENAPI_BASE_PATH);
+        
+        if (!basePath.startsWith("/") || !basePath.endsWith("/"))
+        {
+            throw new RuntimeException(String.format(
+                    "The value of the %s (%s) configuration symbol is '%s' is invalid. "
+                    + "It should start with a slash and not end with one", 
+                        SymbolConstants.OPENAPI_BASE_PATH, 
+                        "SymbolConstants.OPENAPI_BASE_PATH", basePath));
+        }
+        
+        stringToClassMap.put("boolean", boolean.class);
+        stringToClassMap.put("byte", byte.class);
+        stringToClassMap.put("short", short.class);
+        stringToClassMap.put("int", int.class);
+        stringToClassMap.put("long", long.class);
+        stringToClassMap.put("float", float.class);
+        stringToClassMap.put("double", double.class);
+        stringToClassMap.put("char", char.class);
+        
+        for (Class<?> entity : entities) {
+            stringToClassMap.put(entity.getName(), entity);
+        }
+        
     }
 
     @Override
@@ -137,7 +182,8 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
         generateInfo(documentation);
         
         JSONArray servers = new JSONArray();
-        servers.add(new JSONObject("url", baseUrlSource.getBaseURL(request.isSecure())));
+        servers.add(new JSONObject("url", baseUrlSource.getBaseURL(request.isSecure()) + 
+                basePath.substring(0, basePath.length() - 1))); // removing the last slash
         
         documentation.put("servers", servers);
         
@@ -150,6 +196,8 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
             throw new RuntimeException(e);
         }
         
+        generateSchemas(documentation);
+        
         return documentation;
         
     }
@@ -289,6 +337,7 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
                 parameterDescription.put("name", getParameterName(parameter));
                 getValue(method, uri, httpMethod, parameter, "description")
                     .ifPresent((v) -> parameterDescription.put("description", v));
+                typeDescriber.describe(parameterDescription, parameter);
                 
                 parametersAsJsonArray.add(parameterDescription);
             }
@@ -327,9 +376,42 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
         putIfNotEmpty(defaultResponse, "description", getValue(method, uri, httpMethod, statusCode));
         responses.put(String.valueOf(statusCode), defaultResponse);
 
+        String[] produces = getProducedMediaTypes(method);
+        if (produces != null && produces.length > 0)
+        {
+            JSONObject contentDescription = new JSONObject();
+            for (String mediaType : produces)
+            {
+                JSONObject responseTypeDescription = new JSONObject();
+                typeDescriber.describeReturnType(responseTypeDescription, method);
+                contentDescription.put(mediaType, responseTypeDescription);
+            }
+            defaultResponse.put("content", contentDescription);
+        }
+
         methodDescription.put("responses", responses);
     }
     
+    private String[] getProducedMediaTypes(Method method) {
+        
+        String[] produces = CommonsUtils.EMPTY_STRING_ARRAY;
+        RestInfo restInfo = method.getAnnotation(RestInfo.class);
+        if (isNonEmptyConsumes(restInfo))
+        {
+            produces = restInfo.produces();
+        }
+        else
+        {
+            restInfo = method.getDeclaringClass().getAnnotation(RestInfo.class);
+            if (isNonEmptyConsumes(restInfo))
+            {
+                produces = restInfo.produces();
+            }
+        }
+        
+        return produces;
+    }
+
     private void addElementsIfNotPresent(JSONArray accumulator, JSONArray array) 
     {
         if (array != null)
@@ -345,6 +427,11 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
             }
         }
     }
+    
+    private boolean isNonEmptyConsumes(RestInfo restInfo)
+    {
+        return restInfo != null && !(restInfo.produces().length == 1 && "".equals(restInfo.produces()[0]));
+    }
 
     private boolean isPresent(JSONArray array, JSONObject object) 
     {
@@ -370,10 +457,10 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
         }
         return value;
     }
-    
+
     private Optional<String> getValue(Method method, String path, String httpMethod, String property) 
     {
-        return getValue(method, path + "." + httpMethod + "." + property, true);
+        return getValue(method, path + "." + httpMethod + "." + property, false);
     }
 
     public Optional<String> getValue(Method method, String path, String httpMethod, Parameter parameter, String property) 
@@ -460,23 +547,64 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
             superTypes.addAll((Arrays.asList(pageClass.getInterfaces())));
             for (Class clazz : superTypes)
             {
-                method = findMethod(clazz, name, parameterTypes);
-                if (method != null)
+                if (clazz != null && !clazz.equals(Object.class))
                 {
-                    break;
+                    method = findMethod(clazz, name, parameterTypes);
+                    if (method != null)
+                    {
+                        break;
+                    }
                 }
             }
         }
+        if (method == null && pageClass.getName().equals("org.apache.tapestry5.integration.app1.pages.rest.RestTypeDescriptionsDemo"))
+        {
+            System.out.println("WTF!");
+        }
+        // In case of the same class being loaded from different classloaders,
+        // let's try to find the method in a different way.
+//        if (method == null)
+//        {
+//            for (Method m : pageClass.getDeclaredMethods())
+//            {
+//                if (name.equals(m.getName()) && parameterTypes.size() == m.getParameterCount())
+//                {
+//                    boolean matches = true;
+//                    for (int i = 0; i < parameterTypes.size(); i++)
+//                    {
+//                        if (!(parameterTypes.get(i)).getName().equals(
+//                                m.getParameterTypes()[i].getName()))
+//                        {
+//                            matches = false;
+//                            break;
+//                        }
+//                    }
+//                    if (matches)
+//                    {
+//                        method = m;
+//                        break;
+//                    }
+//                }
+//            }
+//        }
         return method;
     }
     
-    private static Class<?> toClass(String string)
+    private Class<?> toClass(String string)
     {
-        try {
-            return Class.forName(string);
-        } catch (ClassNotFoundException e) {
-            throw new RuntimeException(e);
+        Class<?> clasz = stringToClassMap.get(string);
+        if (clasz == null)
+        {
+            try 
+            {
+                clasz = Thread.currentThread().getContextClassLoader().loadClass(string);
+            } catch (ClassNotFoundException e) 
+            {
+                throw new RuntimeException(e);
+            }
+            stringToClassMap.put(string, clasz);
         }
+        return clasz;
     }
 
     private String getPath(Method method, Class<?> pageClass)
@@ -501,7 +629,19 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
                 }
             }
         }
-        return builder.toString();
+        String path = builder.toString();
+        if (!path.startsWith(basePath))
+        {
+            throw new RuntimeException(String.format("Method %s has path %s, which "
+                    + "doesn't start with base path %s. It's likely you need to adjust the "
+                    + "base path and/or the endpoint paths",
+                    method, path, basePath));
+        }
+        else
+        {
+            path = path.substring(basePath.length() - 1); // keep the slash
+        }
+        return path;
     }
     
     @SuppressWarnings({ "rawtypes", "unchecked" })
@@ -606,5 +746,24 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
         return InternalConstants.TRUE.equals(componentModel.getMeta(
                 InternalConstants.REST_ENDPOINT_EVENT_HANDLER_METHOD_PRESENT));
     }
+    
+    private void generateSchemas(JSONObject documentation) 
+    {
+        if (!entities.isEmpty())
+        {
+        
+            JSONObject components = new JSONObject();
+            JSONObject schemas = new JSONObject();
+        
+            for (Class<?> entity : entities) {
+                typeDescriber.describeSchema(entity, schemas);
+            }
+            
+            components.put("schemas", schemas);
+            documentation.put("components", components);
+            
+        }
+        
+    }
 
 }
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiTypeDescriber.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiTypeDescriber.java
new file mode 100644
index 0000000..5f33382
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/DefaultOpenApiTypeDescriber.java
@@ -0,0 +1,210 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "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.tapestry5.internal.services.rest;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.apache.tapestry5.annotations.RequestParameter;
+import org.apache.tapestry5.annotations.RestInfo;
+import org.apache.tapestry5.json.JSONArray;
+import org.apache.tapestry5.json.JSONObject;
+import org.apache.tapestry5.services.rest.MappedEntityManager;
+import org.apache.tapestry5.services.rest.OpenApiTypeDescriber;
+
+/**
+ * {@link OpenApiTypeDescriber} implementation that handles some basic types, mostly primitives and String.
+ * Since this is the fallback, if the parameter doesn't have any handled type, it defaults
+ * to give the <code>object</code> to it without providing properties.
+ */
+public class DefaultOpenApiTypeDescriber implements OpenApiTypeDescriber 
+{
+    final Set<Class<?>> mappedEntities;
+    private static final String ARRAY_TYPE = "array";
+    private static final String OBJECT_TYPE = "object";
+    private static final String STRING_TYPE = "string";
+    private static final Function<Class<?>, String> TO_INTEGER = (c) -> "integer";
+    private static final Function<Class<?>, String> TO_BOOLEAN = (c) -> "boolean";
+    private static final Function<Class<?>, String> TO_NUMBER = (c) -> "number";
+    private static final List<Handler> MAPPERS = Arrays.asList(
+            new Handler(int.class, TO_INTEGER),
+            new Handler(Integer.class, TO_INTEGER),
+            new Handler(byte.class, TO_INTEGER),
+            new Handler(Byte.class, TO_INTEGER),
+            new Handler(short.class, TO_INTEGER),
+            new Handler(Short.class, TO_INTEGER),
+            new Handler(long.class, TO_INTEGER),
+            new Handler(Long.class, TO_INTEGER),
+            new Handler(float.class, TO_NUMBER),
+            new Handler(Float.class, TO_NUMBER),
+            new Handler(double.class, TO_NUMBER),
+            new Handler(Double.class, TO_NUMBER),
+            new Handler(boolean.class, TO_BOOLEAN),
+            new Handler(Boolean.class, TO_BOOLEAN),
+            new Handler(String.class, (c) -> STRING_TYPE),
+            new Handler(char.class, (c) -> STRING_TYPE),
+            new Handler(Character.class, (c) -> STRING_TYPE),
+            new Handler(JSONObject.class, (c) -> OBJECT_TYPE),
+            new Handler(JSONArray.class, (c) -> ARRAY_TYPE)
+    );
+    
+    public DefaultOpenApiTypeDescriber(final MappedEntityManager mappedEntityManager)
+    {
+        mappedEntities = mappedEntityManager.getEntities();
+    }
+    
+    @Override
+    public void describe(JSONObject description, Parameter parameter) 
+    {
+        describeType(description, parameter.getType());
+        
+        // According to the OpenAPI 3 documentation, path parameters are always required.
+        final RequestParameter requestParameter = parameter.getAnnotation(RequestParameter.class);
+        if (requestParameter == null || requestParameter != null && !requestParameter.allowBlank())
+        {
+            description.put("required", true);
+        }
+        
+    }
+
+    @Override
+    public void describeReturnType(JSONObject description, Method method) 
+    {
+        Class<?> returnedType;
+        final RestInfo restInfo = method.getAnnotation(RestInfo.class);
+        if (restInfo != null)
+        {
+            returnedType = restInfo.returnedType();
+        }
+        else 
+        {
+            returnedType = method.getReturnType();
+        }
+        describeType(description, returnedType);
+    }
+
+    private JSONObject describeType(JSONObject description, Class<?> type)
+    {
+        // If a schema is already provided, we leave it unchanged.
+        JSONObject schema = description.getJSONObjectOrDefault("schema", null);
+        if (schema == null)
+        {
+            final Optional<String> schemaType = getOpenApiType(type);
+            if (schemaType.isPresent())
+            {
+                schema = description.put("schema", new JSONObject("type", schemaType.get()));
+            }
+            else if (mappedEntities.contains(type))
+            {
+                schema = description.put("schema", 
+                        new JSONObject("$ref", getSchemaReference(type)));
+            }
+        }
+        return schema;
+    }
+
+    private Optional<String> getOpenApiType(Class<?> type) {
+        final Optional<String> schemaType = MAPPERS.stream()
+                .filter(h -> h.type.equals(type))
+                .map(h -> h.getMapper().apply(type))
+                .findFirst();
+        return schemaType;
+    }
+    
+    private static final class Handler
+    {
+        final private Class<?> type;
+        
+        final private Function<Class<?>, String> mapper;
+
+        public Handler(Class<?> type, Function<Class<?>, String> mapper) 
+        {
+            super();
+            this.type = type;
+            this.mapper = mapper;
+        }
+        
+        public Function<Class<?>, String> getMapper() {
+            return mapper;
+        }
+        
+    }
+
+    @Override
+    public void describeSchema(Class<?> entity, JSONObject schemas) 
+    {
+        
+        final String name = getSchemaName(entity);
+        
+        // Don't overwrite already provided schemas
+        if (!schemas.containsKey(name))
+        {
+            JSONObject schema = new JSONObject();
+            JSONObject properties = new JSONObject();
+            final BeanInfo beanInfo;
+            
+            try 
+            {
+                beanInfo = Introspector.getBeanInfo(entity, Object.class);
+            } catch (IntrospectionException e) {
+                throw new RuntimeException(e);
+            }
+            
+            final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
+            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) 
+            {
+                final String propertyName = propertyDescriptor.getName();
+                final Class<?> type = propertyDescriptor.getPropertyType();
+                Optional<String> schemaType = getOpenApiType(type);
+                if (schemaType.isPresent())
+                {
+                    JSONObject propertyDescription = new JSONObject();
+                    propertyDescription.put("type", schemaType.get());
+                    properties.put(propertyName, propertyDescription);
+                }
+//                else if (mappedEntities.contains(entity))
+//                {
+//                    JSONObject propertyDescription = new JSONObject();
+//                    propertyDescription.put("schema", 
+//                            new JSONObject("$ref", getSchemaReference(type)));
+//                    properties.put(propertyName, propertyDescription);
+//                }
+            }
+            
+            schema.put("properties", properties);
+            schemas.put(name, schema);
+        }
+    }
+
+    private String getSchemaName(final Class<?> entity) {
+        return entity.getSimpleName();
+    }
+
+    private String getSchemaReference(final Class<?> entity) {
+        return "#/components/schemas/" + getSchemaName(entity);
+    }
+
+}
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/MappedEntityManagerImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/MappedEntityManagerImpl.java
new file mode 100644
index 0000000..0af81c3
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/MappedEntityManagerImpl.java
@@ -0,0 +1,67 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "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.tapestry5.internal.services.rest;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.tapestry5.ioc.services.ClassNameLocator;
+import org.apache.tapestry5.services.rest.MappedEntityManager;
+
+/**
+ * Default {@link MappedEntityManager} implementation.
+ */
+public class MappedEntityManagerImpl implements MappedEntityManager
+{
+    
+    private final Set<Class<?>> entities;
+    
+    public MappedEntityManagerImpl(Collection<String> packages, final ClassNameLocator classNameLocator) 
+    {
+        
+        Set<Class<?>> classes = new HashSet<>();
+        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+        
+        for (String packageName : packages)
+        {
+            for (String className : classNameLocator.locateClassNames(packageName))
+            {
+                try
+                {
+                    Class<?> entityClass = contextClassLoader.loadClass(className);
+                    classes.add(entityClass);
+                }
+                catch (ClassNotFoundException ex)
+                {
+                    throw new RuntimeException(ex);
+                }
+            }
+        }
+        
+        entities = Collections.unmodifiableSet(new HashSet<>(classes));
+        
+    }
+
+    @Override
+    public Set<Class<?>> getEntities() 
+    {
+        return entities;
+    }
+
+}
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/EmptySuperclass.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/package-info.java
similarity index 58%
copy from tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/EmptySuperclass.java
copy to tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/package-info.java
index 4f295aa..5e778a3 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/EmptySuperclass.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/rest/package-info.java
@@ -1,23 +1,18 @@
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "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.tapestry5.integration.app1.base;
-
-import org.apache.tapestry5.internal.services.DefaultOpenApiDescriptionGenerator;
-
-/**
- * Just to make sure {@link DefaultOpenApiDescriptionGenerator} handles superclasses
- * without any REST methods correctly (it didn't at first).
- */
-public class EmptySuperclass 
-{
-    
-}
+// Copyright 2021 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "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.
+
+/**
+ * [INTERNAL USE ONLY] REST support classes; API subject to change
+ */
+package org.apache.tapestry5.internal.services.rest;
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
index 94b7615..df9c374 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
@@ -173,6 +173,9 @@ import org.apache.tapestry5.internal.services.meta.ContentTypeExtractor;
 import org.apache.tapestry5.internal.services.meta.MetaAnnotationExtractor;
 import org.apache.tapestry5.internal.services.meta.MetaWorkerImpl;
 import org.apache.tapestry5.internal.services.meta.UnknownActivationContextExtractor;
+import org.apache.tapestry5.internal.services.rest.DefaultOpenApiDescriptionGenerator;
+import org.apache.tapestry5.internal.services.rest.DefaultOpenApiTypeDescriber;
+import org.apache.tapestry5.internal.services.rest.MappedEntityManagerImpl;
 import org.apache.tapestry5.internal.services.security.ClientWhitelistImpl;
 import org.apache.tapestry5.internal.services.security.LocalhostOnly;
 import org.apache.tapestry5.internal.services.templates.DefaultTemplateLocator;
@@ -250,7 +253,6 @@ import org.apache.tapestry5.ioc.services.ThreadLocale;
 import org.apache.tapestry5.ioc.services.UpdateListener;
 import org.apache.tapestry5.ioc.services.UpdateListenerHub;
 import org.apache.tapestry5.json.JSONArray;
-import org.apache.tapestry5.json.JSONCollection;
 import org.apache.tapestry5.json.JSONObject;
 import org.apache.tapestry5.json.modules.JSONModule;
 import org.apache.tapestry5.plastic.MethodAdvice;
@@ -309,6 +311,7 @@ import org.apache.tapestry5.services.Heartbeat;
 import org.apache.tapestry5.services.HiddenFieldLocationRules;
 import org.apache.tapestry5.services.Html5Support;
 import org.apache.tapestry5.services.HttpError;
+import org.apache.tapestry5.services.HttpStatus;
 import org.apache.tapestry5.services.InitializeActivePageName;
 import org.apache.tapestry5.services.LibraryMapping;
 import org.apache.tapestry5.services.LinkCreationHub;
@@ -318,7 +321,6 @@ import org.apache.tapestry5.services.MarkupWriterFactory;
 import org.apache.tapestry5.services.MetaDataLocator;
 import org.apache.tapestry5.services.NullFieldStrategySource;
 import org.apache.tapestry5.services.ObjectRenderer;
-import org.apache.tapestry5.services.OpenApiDescriptionGenerator;
 import org.apache.tapestry5.services.PageDocumentGenerator;
 import org.apache.tapestry5.services.PageRenderLinkSource;
 import org.apache.tapestry5.services.PageRenderRequestFilter;
@@ -362,6 +364,9 @@ import org.apache.tapestry5.services.meta.FixedExtractor;
 import org.apache.tapestry5.services.meta.MetaDataExtractor;
 import org.apache.tapestry5.services.meta.MetaWorker;
 import org.apache.tapestry5.services.pageload.PreloaderMode;
+import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator;
+import org.apache.tapestry5.services.rest.OpenApiTypeDescriber;
+import org.apache.tapestry5.services.rest.MappedEntityManager;
 import org.apache.tapestry5.services.security.ClientWhitelist;
 import org.apache.tapestry5.services.security.WhitelistAnalyzer;
 import org.apache.tapestry5.services.templates.ComponentTemplateLocator;
@@ -526,6 +531,7 @@ public final class TapestryModule
         binder.bind(ExceptionReportWriter.class, ExceptionReportWriterImpl.class);
         binder.bind(ComponentOverride.class, ComponentOverrideImpl.class).eagerLoad();
         binder.bind(Html5Support.class, Html5SupportImpl.class);
+        binder.bind(MappedEntityManager.class, MappedEntityManagerImpl.class);
     }
 
     // ========================================================================
@@ -1606,6 +1612,7 @@ public final class TapestryModule
         });
 
         configuration.addInstance(HttpError.class, HttpErrorComponentEventResultProcessor.class);
+        configuration.addInstance(HttpStatus.class, HttpStatusComponentEventResultProcessor.class);
 
         configuration.addInstance(String.class, PageNameComponentEventResultProcessor.class);
 
@@ -2187,6 +2194,7 @@ public final class TapestryModule
         configuration.add(SymbolConstants.OPENAPI_VERSION, "3.0.0");
         configuration.add(SymbolConstants.PUBLISH_OPENAPI_DEFINITON, "false");
         configuration.add(SymbolConstants.OPENAPI_DESCRIPTION_PATH, "/openapi.json");
+        configuration.add(SymbolConstants.OPENAPI_BASE_PATH, "/");
     }
 
     /**
@@ -2735,12 +2743,24 @@ public final class TapestryModule
         return chainBuilder.build(OpenApiDescriptionGenerator.class, configuration);
     }
 
+    public static OpenApiTypeDescriber buildOpenApiTypeDescriber(List<OpenApiTypeDescriber> configuration,
+            ChainBuilder chainBuilder) 
+    {
+        return chainBuilder.build(OpenApiTypeDescriber.class, configuration);
+    }
+
     @Contribute(OpenApiDescriptionGenerator.class)
     public static void addBuiltInOpenApiDocumentationGenerator(
             OrderedConfiguration<OpenApiDescriptionGenerator> configuration) {
         configuration.addInstance("Default", DefaultOpenApiDescriptionGenerator.class, "before:*");
     }
 
+    @Contribute(OpenApiTypeDescriber.class)
+    public static void addBuiltInOpenApiTypeDescriber(
+            OrderedConfiguration<OpenApiTypeDescriber> configuration) {
+        configuration.addInstance("Default", DefaultOpenApiTypeDescriber.class, "before:*");
+    }
+
     private static final class TapestryCoreComponentLibraryInfoSource implements
             ComponentLibraryInfoSource
     {
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/HttpError.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/HttpError.java
index 1f42eb4..1bd11c4 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/services/HttpError.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/HttpError.java
@@ -16,6 +16,8 @@ package org.apache.tapestry5.services;
 
 /**
  * An event handler method may return an instance of this class to send an error response to the client.
+ * If you need something similar but not for errors, such as statuses in the 2xx range,
+ * use {@link HttpStatus instead}.
  *
  * @since 5.2.0
  */
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/HttpStatus.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/HttpStatus.java
new file mode 100644
index 0000000..bdc57b0
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/HttpStatus.java
@@ -0,0 +1,130 @@
+// Copyright 2021 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "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.tapestry5.services;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.tapestry5.StreamResponse;
+
+/**
+ * An event handler method may return an instance of this class to send an specific HTTP status
+ * code to the client. It also supports providing a string to be used as the response body
+ * and extra HTTP headers to be set.
+ * 
+ * For returning binary content and/or adding a response header more than once and/or
+ * adding a response header without overwriting existing ones, implement a {@link StreamResponse} 
+ * instead.
+ *
+ * @since 5.8.0
+ */
+public final class HttpStatus
+{
+    private static final String CONTENT_LOCATION_HTTP_HEADER = "Content-Location";
+
+    private final int statusCode;
+
+    String message;
+    
+    String contentType;
+    
+    Map<String, String> extraHttpHeaders;
+    
+    /**
+     * Creates an object with a given status code and no message.
+     */
+    public HttpStatus(int statusCode)
+    {
+        this(statusCode, null, null);
+    }
+    
+    /**
+     * Creates an object with a given status code, message and <code>text/plain</code> MIME content type.
+     */
+    public HttpStatus(int statusCode, String message)
+    {
+        this(statusCode, message, "text/plain");
+    }
+
+    /**
+     * Creates an object with a given status code, message and MIME content type.
+     */
+    public HttpStatus(int statusCode, String message, String contentType)
+    {
+        this.statusCode = statusCode;
+        this.message = message;
+        this.contentType = contentType;
+    }
+
+    /**
+     * Returns the status code.
+     */
+    public int getStatusCode()
+    {
+        return statusCode;
+    }
+
+    /**
+     * Returns the message.
+     */
+    public String getMessage()
+    {
+        return message;
+    }
+    
+    /**
+     * Returns the extra HTTP headers.
+     */
+    @SuppressWarnings("unchecked")
+    public Map<String, String> getExtraHttpHeaders() {
+        return extraHttpHeaders != null ? extraHttpHeaders : Collections.EMPTY_MAP;
+    }
+    
+    /**
+     * Returns the MIME content type of the message.
+     */
+    public String getContentType() 
+    {
+        return contentType;
+    }
+    
+    /**
+     * Sets the <code>Content-Location</code> HTTP header.
+     */
+    public HttpStatus withContentLocation(String location)
+    {
+        return withHttpHeader(CONTENT_LOCATION_HTTP_HEADER, location);
+    }
+    
+    /**
+     * Sets an HTTP header. If an existing value for this header already exists,
+     * it gets overwritten. If you need to set multiple headers or add them without
+     * overwriting existing ones, you need to implement {@link StreamResponse} instead.
+     */
+    public HttpStatus withHttpHeader(String name, String value)
+    {
+        Objects.requireNonNull(name, "Parameter name cannot be null");
+        Objects.requireNonNull(value, "Parameter value cannot be null");
+        if (extraHttpHeaders == null)
+        {
+            extraHttpHeaders = new HashMap<>(3);
+        }
+        extraHttpHeaders.put(name, value);
+        return this;
+    }
+
+}
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/MappedEntityManager.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/MappedEntityManager.java
new file mode 100644
index 0000000..62d3422
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/MappedEntityManager.java
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "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.tapestry5.services.rest;
+
+import java.util.Set;
+
+import org.apache.tapestry5.ioc.annotations.UsesConfiguration;
+
+/**
+ * Service which provides a list of mapped entities. They're usually classes which are mapped
+ * to other formats like JSON and XML and used to represent data being received or sent
+ * to or from external processes, for example REST endpoints.
+ * Contributions are done by package and all classes inside the contributed ones are considered
+ * mapped entities.
+ */
+@UsesConfiguration(String.class)
+public interface MappedEntityManager {
+
+    /**
+     * Returns the set of entity classes.
+     * @return a {@link Set} of {@link Class} instances.
+     */
+    Set<Class<?>> getEntities();
+    
+}
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/OpenApiDescriptionGenerator.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/OpenApiDescriptionGenerator.java
index 42e4b71..5939558 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/OpenApiDescriptionGenerator.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/OpenApiDescriptionGenerator.java
@@ -15,9 +15,9 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package org.apache.tapestry5.services;
+package org.apache.tapestry5.services.rest;
 
-import org.apache.tapestry5.internal.services.DefaultOpenApiDescriptionGenerator;
+import org.apache.tapestry5.internal.services.rest.DefaultOpenApiDescriptionGenerator;
 import org.apache.tapestry5.ioc.annotations.UsesOrderedConfiguration;
 import org.apache.tapestry5.json.JSONObject;
 
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/OpenApiTypeDescriber.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/OpenApiTypeDescriber.java
new file mode 100644
index 0000000..1269879
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/OpenApiTypeDescriber.java
@@ -0,0 +1,56 @@
+// Copyright 2021 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "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.tapestry5.services.rest;
+
+import java.lang.reflect.Parameter;
+
+import org.apache.tapestry5.ioc.annotations.UsesOrderedConfiguration;
+import org.apache.tapestry5.json.JSONObject;
+
+import java.lang.reflect.Method;
+
+/**
+ * Interface that describes the type of a REST endpoint parameter, return type or request body.
+ * It should add a <code>schema</code> (in most cases) or <code>content</code> property to the 
+ * provided {@linkplain JSONObject} if the given type is supported. This can be be also
+ * used to customize specific parameters or return type or request body of specific paths.
+ * As a service, this is a chain of instances of itself. All instances will be called.
+ */
+@UsesOrderedConfiguration(OpenApiTypeDescriber.class)
+public interface OpenApiTypeDescriber 
+{
+
+    /**
+     * Describes a REST event handler method parameter.
+     * @param descriptiona {@link JSONObject} containing the description of an event handler parameter.
+     * @param parameter the event handler method parameter.
+     */
+    void describe(final JSONObject description, Parameter parameter);
+    
+    /**
+     * Describes a REST event handler method return type.
+     * @param descriptiona {@link JSONObject} containing the description of a path.
+     * @param parameter the event handler method itself.
+     */
+    void describeReturnType(final JSONObject description, Method method);
+
+    /**
+     * Describes the schema of a mapped entity class
+     * @param entity an entity class.
+     * @param schemas {@link JSONObject} where the entity description should be added.
+     * @see MappedEntityManager
+     */
+    void describeSchema(Class<?> entity, JSONObject schemas);
+
+}
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/EmptySuperclass.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/package-info.java
similarity index 58%
copy from tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/EmptySuperclass.java
copy to tapestry-core/src/main/java/org/apache/tapestry5/services/rest/package-info.java
index 4f295aa..061e84e 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/EmptySuperclass.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/rest/package-info.java
@@ -1,23 +1,18 @@
+// Copyright 2021 The Apache Software Foundation
+//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
 //
-// http://www.apache.org/licenses/LICENSE-2.0
+//     http://www.apache.org/licenses/LICENSE-2.0
 //
 // Unless required by applicable law or agreed to in writing, software
 // distributed under the License is distributed on an "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.tapestry5.integration.app1.base;
-
-import org.apache.tapestry5.internal.services.DefaultOpenApiDescriptionGenerator;
 
 /**
- * Just to make sure {@link DefaultOpenApiDescriptionGenerator} handles superclasses
- * without any REST methods correctly (it didn't at first).
+ * Services related to Tapestry's REST support, including OpenAPI 3.0 description generation.
  */
-public class EmptySuperclass 
-{
-    
-}
+package org.apache.tapestry5.services.rest;
diff --git a/tapestry-core/src/test/app1/WEB-INF/app.properties b/tapestry-core/src/test/app1/WEB-INF/app.properties
index 52ede78..27a3532 100644
--- a/tapestry-core/src/test/app1/WEB-INF/app.properties
+++ b/tapestry-core/src/test/app1/WEB-INF/app.properties
@@ -31,29 +31,29 @@ openapi.put.response.200 = Generic 200
 openapi.response.200 = Generic 200
 openapi.response.201 = PUT request succesful
 
-openapi./restrequestnothandleddemo.put.response.200=SQN
+openapi./requestnothandleddemo.put.response.200=SQN
 openapi.put.response.200 = Generic 200
 openapi.response.200 = Generic 200
 openapi.response.201 = PUT request succesful
 
-openapi./restwitheventhandlermethodnamedemo/parametersTest/pathParameter.get.parameter.pathParameter.description=Specific pathParameter description
-openapi.get.parameter.pathParameter.description=GET-specific arg2 description
+openapi./witheventhandlermethodnamedemo/parametersTest/{pathParameter}.get.parameter.pathParameter.description=Specific pathParameter description
+openapi.get.parameter.pathParameter.description=GET-specific pathParameter description
 openapi.parameter.pathParameter.description = Generic pathParameter description
 
 
-openapi.org.apache.tapestry5.integration.app1.pages.RestRequestNotHandledDemo.tag.name=fcqnTag
+openapi.org.apache.tapestry5.integration.app1.pages.rest.RestRequestNotHandledDemo.tag.name=restRequestNotHandledFCQNTag
 openapi.RestRequestNotHandledDemo.tag.name=nameTag
 
-openapi.org.apache.tapestry5.integration.app1.pages.RestRequestNotHandledDemo.tag.description=Description from FCQN
+openapi.org.apache.tapestry5.integration.app1.pages.rest.RestRequestNotHandledDemo.tag.description=Description from FCQN
 openapi.RestRequestNotHandledDemo.tag.description=Description from name
 
 #openapi.RestWithOnEventDemo.tag.name=
 
 
-openapi.org.apache.tapestry5.integration.app1.pages.RestRequestNotHandledDemo./restrequestnothandleddemo.put.summary=Summary from FQCN name: put!
-openapi.RestRequestNotHandledDemo./restrequestnothandleddemo.put.summary=Summary from class name: put!
-openapi./restrequestnothandleddemo.put.summary=Summary from path: put!
+openapi.org.apache.tapestry5.integration.app1.pages.rest.RestRequestNotHandledDemo./requestnothandleddemo.put.summary=Summary from FQCN name: put!
+openapi.RestRequestNotHandledDemo./requestnothandleddemo.put.summary=Summary from class name: put!
+openapi./requestnothandleddemo.put.summary=Summary from path: put!
 
-openapi.org.apache.tapestry5.integration.app1.pages.RestRequestNotHandledDemo./restrequestnothandleddemo.put.description=Description from FQCN name: put!
+openapi.org.apache.tapestry5.integration.app1.pages.rest.RestRequestNotHandledDemo./requestnothandleddemo.put.description=Description from FQCN name: put!
 openapi.RestRequestNotHandledDemo./restrequestnothandleddemo.put.description=Description from class name: put!
-openapi./restrequestnothandleddemo.put.description=Description from path: put!
\ No newline at end of file
+openapi./requestnothandleddemo.put.description=Description from path: put!
\ No newline at end of file
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/BaseRestDemoPage.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/BaseRestDemoPage.java
index 6838c53..f2e54a7 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/BaseRestDemoPage.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/BaseRestDemoPage.java
@@ -11,14 +11,18 @@
 // limitations under the License.
 package org.apache.tapestry5.integration.app1.base;
 
+import javax.servlet.http.HttpServletResponse;
+
 import org.apache.tapestry5.EventConstants;
 import org.apache.tapestry5.annotations.ActivationContextParameter;
 import org.apache.tapestry5.annotations.OnEvent;
+import org.apache.tapestry5.annotations.RequestBody;
 import org.apache.tapestry5.annotations.RequestParameter;
 import org.apache.tapestry5.annotations.StaticActivationContextValue;
 import org.apache.tapestry5.http.services.Response;
 import org.apache.tapestry5.json.JSONArray;
 import org.apache.tapestry5.json.JSONObject;
+import org.apache.tapestry5.services.HttpStatus;
 import org.apache.tapestry5.util.TextStreamResponse;
 
 public class BaseRestDemoPage extends AbstractRestDemoPage {
@@ -60,4 +64,22 @@ public class BaseRestDemoPage extends AbstractRestDemoPage {
                 "queryParameter", queryParameter);
     }
     
+    @OnEvent(EventConstants.HTTP_PUT)
+    public Object returningHttpStatus(
+            @StaticActivationContextValue("returningHttpStatus") String ignored,
+            @RequestBody String parameter)
+    {
+        return new HttpStatus(HttpServletResponse.SC_CREATED, parameter)
+                .withContentLocation(parameter + ".txt")
+                .withHttpHeader("ETag", parameter + ".etag");
+    }
+    
+    @OnEvent(EventConstants.HTTP_PUT)
+    public Object returningHttpStatusSimple(
+            @StaticActivationContextValue("returningHttpStatusSimple") String ignored,
+            @RequestBody String parameter)
+    {
+        return new HttpStatus(HttpServletResponse.SC_CREATED);
+    }
+    
 }
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/EmptySuperclass.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/EmptySuperclass.java
index 4f295aa..685719b 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/EmptySuperclass.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/base/EmptySuperclass.java
@@ -11,7 +11,7 @@
 // limitations under the License.
 package org.apache.tapestry5.integration.app1.base;
 
-import org.apache.tapestry5.internal.services.DefaultOpenApiDescriptionGenerator;
+import org.apache.tapestry5.internal.services.rest.DefaultOpenApiDescriptionGenerator;
 
 /**
  * Just to make sure {@link DefaultOpenApiDescriptionGenerator} handles superclasses
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/data/rest/entities/Point.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/data/rest/entities/Point.java
new file mode 100644
index 0000000..f616526
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/data/rest/entities/Point.java
@@ -0,0 +1,35 @@
+package org.apache.tapestry5.integration.app1.data.rest.entities;
+
+public class Point {
+
+    private int x;
+
+    private int y;
+    
+    private Point nextPoint;
+
+    public int getX() {
+        return x;
+    }
+
+    public void setX(int x) {
+        this.x = x;
+    }
+
+    public int getY() {
+        return y;
+    }
+
+    public void setY(int y) {
+        this.y = y;
+    }
+    
+    public Point getNextPoint() {
+        return nextPoint;
+    }
+
+    public void setNextPoint(Point nextPoint) {
+        this.nextPoint = nextPoint;
+    }
+
+}
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
index 6ffb262..348118a 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/Index.java
@@ -62,9 +62,9 @@ public class Index
 
                     new Item("StaticActivationContextValueDemo", "@StaticActivationContextValue Demo", "Demonstrates the usage of @StaticActivationContextValue"),
                     
-                    new Item("RestWithOnEventDemo", "REST with @OnEvent Demo", "Demonstrates the usage of @OnEvent to handle REST requests"),                    
+                    new Item("rest/RestWithOnEventDemo", "REST with @OnEvent Demo", "Demonstrates the usage of @OnEvent to handle REST requests"),                    
                     
-                    new Item("RestWithEventHandlerMethodNameDemo", "REST with Event Handler Method Name Demo", "Demonstrates the usage of event handler method names to handle REST requests"),
+                    new Item("rest/RestWithEventHandlerMethodNameDemo", "REST with Event Handler Method Name Demo", "Demonstrates the usage of event handler method names to handle REST requests"),
 
                     new Item("Html5DateFieldDemo", "Html5DateField Demo", "Choosing dates using the native HTML5 date picker"),
 
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/OpenApiDescriptionDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/OpenApiDescriptionDemo.java
index 9539bc2..3b81c96 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/OpenApiDescriptionDemo.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/OpenApiDescriptionDemo.java
@@ -12,7 +12,7 @@
 package org.apache.tapestry5.integration.app1.pages;
 
 import org.apache.tapestry5.ioc.annotations.Inject;
-import org.apache.tapestry5.services.OpenApiDescriptionGenerator;
+import org.apache.tapestry5.services.rest.OpenApiDescriptionGenerator;
 import org.apache.tapestry5.util.TextStreamResponse;
 
 public class OpenApiDescriptionDemo  {
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestRequestNotHandledDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestRequestNotHandledDemo.java
index 63b3d7a..0a62062 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestRequestNotHandledDemo.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestRequestNotHandledDemo.java
@@ -9,7 +9,7 @@
 // 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.tapestry5.integration.app1.pages;
+package org.apache.tapestry5.integration.app1.pages.rest;
 
 import org.apache.tapestry5.EventConstants;
 import org.apache.tapestry5.integration.app1.base.BaseRestDemoPage;
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestTypeDescriptionsDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestTypeDescriptionsDemo.java
new file mode 100644
index 0000000..acc9c44
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestTypeDescriptionsDemo.java
@@ -0,0 +1,99 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "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.tapestry5.integration.app1.pages.rest;
+
+import org.apache.tapestry5.EventConstants;
+import org.apache.tapestry5.annotations.OnEvent;
+import org.apache.tapestry5.annotations.RequestBody;
+import org.apache.tapestry5.annotations.RestInfo;
+import org.apache.tapestry5.annotations.StaticActivationContextValue;
+import org.apache.tapestry5.integration.app1.data.rest.entities.Point;
+import org.apache.tapestry5.json.JSONArray;
+import org.apache.tapestry5.json.JSONObject;
+
+/**
+ * REST endpoint class just to test the parameter and return type descriptions.
+ */
+@RestInfo(produces = "application/javascript")
+public class RestTypeDescriptionsDemo {
+
+    private static final String TEXT_PLAIN = "text/plain";
+
+    @OnEvent(EventConstants.HTTP_GET)
+    Object point(@StaticActivationContextValue("point") String ignored, Point p1, @RequestBody Point p2) {
+        return null;
+    }
+    
+    @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(returnedType = JSONArray.class)
+    Object jsonArray(
+            @StaticActivationContextValue("jsonArray") String ignored, 
+            JSONArray jsonArray1,
+            @RequestBody(allowEmpty = false) JSONArray jsonArray2) {
+        return null;
+    }
+    
+    @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(returnedType = JSONObject.class)    
+    Object jsonObject(@StaticActivationContextValue("jsonObject") String ignored, JSONObject jsonObject, @RequestBody JSONObject jsonObject2) {
+        return null;
+    }
+    
+    @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(produces = TEXT_PLAIN, returnedType = boolean.class)
+    Object booleanMethod(@StaticActivationContextValue("boolean") String ignored, boolean b1, Boolean b2, @RequestBody boolean b3) {
+        return true;
+    }
+    
+    @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(produces = TEXT_PLAIN)
+    float floatMethod(@StaticActivationContextValue("float") String ignored, float b1, Float b2, @RequestBody float b3) {
+        return 0.1f;
+    }
+    
+    @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(produces = TEXT_PLAIN)
+    double doubleMethod(@StaticActivationContextValue("double") String ignored, double b1, Double b2, @RequestBody double b3) {
+        return 0.2;
+    }
+
+    @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(produces = TEXT_PLAIN, returnedType = byte.class)
+    byte byteMethod(@StaticActivationContextValue("byte") String ignored, byte b1, Byte b2, @RequestBody byte b3) {
+        return 0;
+    }
+    
+    @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(produces = TEXT_PLAIN, returnedType = short.class)
+    short shortMethod(@StaticActivationContextValue("short") String ignored, short b1, short b2, @RequestBody short b3) {
+        return 0;
+    }
+    
+    @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(produces = TEXT_PLAIN, returnedType = int.class)
+    int intMethod(@StaticActivationContextValue("int") String ignored, int b1, Integer b2, @RequestBody int b3) {
+        return 0;
+    }
+    
+    @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(produces = TEXT_PLAIN, returnedType = long.class)
+    long longMethod(@StaticActivationContextValue("long") String ignored, long b1, long b2, @RequestBody long b3) {
+        return 0;
+    }
+
+    @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(produces = TEXT_PLAIN, returnedType = String.class)
+    String string(@StaticActivationContextValue("string") String ignored, String b1, @RequestBody String b2) {
+        return "Ok";
+    }
+
+}
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestWithEventHandlerMethodNameDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestWithEventHandlerMethodNameDemo.java
index e07c256..9b869ca 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestWithEventHandlerMethodNameDemo.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestWithEventHandlerMethodNameDemo.java
@@ -9,7 +9,7 @@
 // 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.tapestry5.integration.app1.pages;
+package org.apache.tapestry5.integration.app1.pages.rest;
 
 import org.apache.tapestry5.EventConstants;
 import org.apache.tapestry5.annotations.OnEvent;
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestWithOnEventDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestWithOnEventDemo.java
index 7b101ae..7534954 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestWithOnEventDemo.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestWithOnEventDemo.java
@@ -9,7 +9,7 @@
 // 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.tapestry5.integration.app1.pages;
+package org.apache.tapestry5.integration.app1.pages.rest;
 
 import org.apache.tapestry5.EventConstants;
 import org.apache.tapestry5.annotations.OnEvent;
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
index a97b930..d41dcf6 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/services/AppModule.java
@@ -61,6 +61,7 @@ import org.apache.tapestry5.services.compatibility.Compatibility;
 import org.apache.tapestry5.services.compatibility.Trait;
 import org.apache.tapestry5.services.pageload.PagePreloader;
 import org.apache.tapestry5.services.pageload.PreloaderMode;
+import org.apache.tapestry5.services.rest.MappedEntityManager;
 import org.apache.tapestry5.services.security.ClientWhitelist;
 import org.apache.tapestry5.services.security.WhitelistAnalyzer;
 import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
@@ -182,6 +183,7 @@ public class AppModule
         configuration.add(D3_URL_SYMBOL, "cdnjs.cloudflare.com/ajax/libs/d3/3.0.0/d3.js");
         configuration.add(SymbolConstants.PRELOADER_MODE, PreloaderMode.ALWAYS);
         configuration.add(SymbolConstants.OPENAPI_APPLICATION_VERSION, "1.2.3.4");
+        configuration.add(SymbolConstants.OPENAPI_BASE_PATH, "/rest/");
 //        configuration.add(SymbolConstants.ERROR_CSS_CLASS, "yyyy");
 //        configuration.add(SymbolConstants.DEFAULT_STYLESHEET, "classpath:/org/apache/tapestry5/integration/app1/app1.css");
     }
@@ -403,5 +405,11 @@ public class AppModule
 	{
 		configuration.add( new EditBlockContribution("address", "PropertyEditBlocks", "object"));
 	}
+	
+	@Contribute(MappedEntityManager.class)
+	public static void provideMappedEntities(Configuration<String> configuration)
+	{
+	    configuration.add("org.apache.tapestry5.integration.app1.data.rest.entities");
+	}
 
 }
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/integration/rest/RestTests.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/rest/RestTests.java
index eed8912..5e5a882 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/integration/rest/RestTests.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/integration/rest/RestTests.java
@@ -22,6 +22,7 @@ import org.apache.commons.io.IOUtils;
 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.StatusLine;
 import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpDelete;
@@ -36,8 +37,9 @@ import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.tapestry5.EventConstants;
 import org.apache.tapestry5.integration.app1.App1TestCase;
-import org.apache.tapestry5.integration.app1.pages.RestRequestNotHandledDemo;
-import org.apache.tapestry5.integration.app1.pages.RestWithOnEventDemo;
+import org.apache.tapestry5.integration.app1.pages.rest.RestRequestNotHandledDemo;
+import org.apache.tapestry5.integration.app1.pages.rest.RestWithEventHandlerMethodNameDemo;
+import org.apache.tapestry5.integration.app1.pages.rest.RestWithOnEventDemo;
 import org.testng.annotations.Test;
 
 /**
@@ -47,92 +49,94 @@ public class RestTests extends App1TestCase
 {
     final private static String POST_CONTENT = "órgão and ôthèr words with äccents";
     
-    final private static String ENDPOINT_URL = RestWithOnEventDemo.class.getSimpleName();
+    final private static String ON_EVENT_ENDPOINT_URL = RestWithOnEventDemo.class.getSimpleName();
     
-    final private static String PATH_PARAMETER_VALUE = RestWithOnEventDemo.class.getSimpleName();
+    final private static String METHOD_NAME_ENDPOINT_URL = RestWithEventHandlerMethodNameDemo.class.getSimpleName();
+    
+    final private static String PATH_PARAMETER_VALUE = "somethingNice";
     
     @Test
     public void on_event_http_get() throws IOException
     {
-        test(EventConstants.HTTP_GET, new HttpGet(getUrl()));
+        test(EventConstants.HTTP_GET, new HttpGet(getUrl(ON_EVENT_ENDPOINT_URL)));
     }
     
     @Test
     public void on_event_http_post() throws IOException
     {
-        test(EventConstants.HTTP_POST, new HttpPost(getUrl()));
+        test(EventConstants.HTTP_POST, new HttpPost(getUrl(ON_EVENT_ENDPOINT_URL)));
     }
 
     @Test
     public void on_event_http_put() throws IOException
     {
-        test(EventConstants.HTTP_PUT, new HttpPut(getUrl()));
+        test(EventConstants.HTTP_PUT, new HttpPut(getUrl(ON_EVENT_ENDPOINT_URL)));
     }
 
-    @Test
+    @Test(enabled = false)
     public void on_event_http_put_without_other_parameters() throws IOException
     {
-        test(EventConstants.HTTP_PUT, new HttpPut(getBaseURL() + ENDPOINT_URL));
+        test(EventConstants.HTTP_PUT, new HttpPut(getBaseURL() + "/rest/" + ON_EVENT_ENDPOINT_URL));
     }
 
     @Test
     public void on_event_http_delete() throws IOException
     {
-        test(EventConstants.HTTP_DELETE, new HttpDelete(getUrl()));
+        test(EventConstants.HTTP_DELETE, new HttpDelete(getUrl(ON_EVENT_ENDPOINT_URL)));
     }
 
     @Test
     public void on_event_http_patch() throws IOException
     {
-        test(EventConstants.HTTP_PATCH, new HttpPatch(getUrl()));
+        test(EventConstants.HTTP_PATCH, new HttpPatch(getUrl(ON_EVENT_ENDPOINT_URL)));
     }
 
     @Test
     public void on_event_http_head() throws IOException
     {
-        test(EventConstants.HTTP_HEAD, new HttpHead(getUrl()));
+        test(EventConstants.HTTP_HEAD, new HttpHead(getUrl(ON_EVENT_ENDPOINT_URL)));
     }
     
     @Test
     public void on_http_get() throws IOException
     {
-        test(EventConstants.HTTP_GET, new HttpGet(getUrl()));
+        test(EventConstants.HTTP_GET, new HttpGet(getUrl(METHOD_NAME_ENDPOINT_URL)));
     }
     
     @Test
     public void on_http_post() throws IOException
     {
-        test(EventConstants.HTTP_POST, new HttpPost(getUrl()));
+        test(EventConstants.HTTP_POST, new HttpPost(getUrl(METHOD_NAME_ENDPOINT_URL)));
     }
 
     @Test
     public void on_http_put() throws IOException
     {
-        test(EventConstants.HTTP_PUT, new HttpPut(getUrl()));
+        test(EventConstants.HTTP_PUT, new HttpPut(getUrl(METHOD_NAME_ENDPOINT_URL)));
     }
 
     @Test
     public void on_http_delete() throws IOException
     {
-        test(EventConstants.HTTP_DELETE, new HttpDelete(getUrl()));
+        test(EventConstants.HTTP_DELETE, new HttpDelete(getUrl(METHOD_NAME_ENDPOINT_URL)));
     }
 
     @Test
     public void on_http_patch() throws IOException
     {
-        test(EventConstants.HTTP_PATCH, new HttpPatch(getUrl()));
+        test(EventConstants.HTTP_PATCH, new HttpPatch(getUrl(METHOD_NAME_ENDPOINT_URL)));
     }
 
     @Test
     public void on_http_head() throws IOException
     {
-        test(EventConstants.HTTP_HEAD, new HttpHead(getUrl()));
+        test(EventConstants.HTTP_HEAD, new HttpHead(getUrl(METHOD_NAME_ENDPOINT_URL)));
     }
 
     @Test
     public void no_matching_rest_event_handler() throws IOException
     {
-        final String url = getBaseURL() + "/" + RestRequestNotHandledDemo.class.getSimpleName();
+        final String url = getBaseURL() + "/rest/" + RestRequestNotHandledDemo.class.getSimpleName();
         try (final CloseableHttpClient httpClient = HttpClients.createDefault())
         {
             HttpHead httpHead = new HttpHead(url);
@@ -142,8 +146,47 @@ public class RestTests extends App1TestCase
             }
         }
     }
+
+    @Test
+    public void returning_http_status() throws IOException
+    {
+        final String url = getBaseURL() + "/rest/" + RestWithOnEventDemo.class.getSimpleName() + "/returningHttpStatus";
+        try (final CloseableHttpClient httpClient = HttpClients.createDefault())
+        {
+            HttpPut httpPut = new HttpPut(url);
+            httpPut.setEntity(new StringEntity(PATH_PARAMETER_VALUE, "UTF-8"));
+            try (CloseableHttpResponse response = httpClient.execute(httpPut))
+            {
+                final StatusLine statusLine = response.getStatusLine();
+                assertEquals(statusLine.getStatusCode(), HttpServletResponse.SC_CREATED);
+                assertEquals(IOUtils.toString(response.getEntity().getContent()), PATH_PARAMETER_VALUE);
+                assertEquals(response.getHeaders("Content-Location").length, 1);
+                assertEquals(response.getHeaders("ETag").length, 1);
+                assertEquals(response.getFirstHeader("Content-Location").getValue(), PATH_PARAMETER_VALUE + ".txt");
+                assertEquals(response.getFirstHeader("ETag").getValue(), PATH_PARAMETER_VALUE + ".etag");
+            }
+        }
+    }
     
     @Test
+    public void returning_http_status_part_2() throws IOException
+    {
+        final String url = getBaseURL() + "/rest/" + RestWithOnEventDemo.class.getSimpleName() + "/returningHttpStatusSimple";
+        try (final CloseableHttpClient httpClient = HttpClients.createDefault())
+        {
+            HttpPut httpPut = new HttpPut(url);
+            httpPut.setEntity(new StringEntity(PATH_PARAMETER_VALUE, "UTF-8"));
+            try (CloseableHttpResponse response = httpClient.execute(httpPut))
+            {
+                final StatusLine statusLine = response.getStatusLine();
+                assertEquals(statusLine.getStatusCode(), HttpServletResponse.SC_CREATED);
+                assertEquals(IOUtils.toString(response.getEntity().getContent()), "");
+                assertEquals(response.getAllHeaders().length, 2); // content-length, server
+            }
+        }
+    }
+
+    @Test
     public void asset_requested_with_head() throws IOException
     {
         openLinks("AssetDemo");
@@ -199,8 +242,8 @@ public class RestTests extends App1TestCase
         }
     }
 
-    private String getUrl() {
-        return getBaseURL() + ENDPOINT_URL + "/" + 
+    private String getUrl(String page) {
+        return getBaseURL() + "/rest/" + page + "/" + 
                 RestWithOnEventDemo.SUBPATH + "/" + PATH_PARAMETER_VALUE;
     }
 
diff --git a/tapestry-openapi-viewer/src/main/java/org/apache/tapestry5/tapestryopenapiviewer/modules/TapestryOpenApiViewerModule.java b/tapestry-openapi-viewer/src/main/java/org/apache/tapestry5/tapestryopenapiviewer/modules/TapestryOpenApiViewerModule.java
index dddb421..95e3b37 100644
--- a/tapestry-openapi-viewer/src/main/java/org/apache/tapestry5/tapestryopenapiviewer/modules/TapestryOpenApiViewerModule.java
+++ b/tapestry-openapi-viewer/src/main/java/org/apache/tapestry5/tapestryopenapiviewer/modules/TapestryOpenApiViewerModule.java
@@ -15,7 +15,10 @@
 package org.apache.tapestry5.tapestryopenapiviewer.modules;
 
 import org.apache.tapestry5.commons.Configuration;
+import org.apache.tapestry5.integration.app1.services.AppModule;
+import org.apache.tapestry5.ioc.annotations.Contribute;
 import org.apache.tapestry5.services.LibraryMapping;
+import org.apache.tapestry5.services.rest.MappedEntityManager;
 
 /**
  * Defines services and definitions for the Tapestry OpenAPI viewer.
@@ -26,4 +29,10 @@ public class TapestryOpenApiViewerModule
     {
         configuration.add(new LibraryMapping("openapiviewer", "org.apache.tapestry5.tapestryopenapiviewer"));
     }
+    
+    @Contribute(MappedEntityManager.class)
+    public static void provideMappedEntities(Configuration<String> configuration)
+    {
+        AppModule.provideMappedEntities(configuration);
+    }
 }