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/12/02 18:00:05 UTC

[tapestry-5] branch rest updated: TAP5-2696: generating descriptions of request bodies, plus some fixes

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 58540b9  TAP5-2696: generating descriptions of request bodies, plus some fixes
58540b9 is described below

commit 58540b90971f799980431b471a37eaebbc450a02
Author: Thiago H. de Paula Figueiredo <th...@arsmachina.com.br>
AuthorDate: Thu Dec 2 14:59:53 2021 -0300

    TAP5-2696: generating descriptions of request bodies, plus some fixes
---
 .../org/apache/tapestry5/annotations/RestInfo.java |  6 +--
 .../rest/DefaultOpenApiDescriptionGenerator.java   | 44 ++++++++++++++++++++--
 .../services/rest/DefaultOpenApiTypeDescriber.java |  2 +-
 .../resources/org/apache/tapestry5/core.properties | 20 +++++++++-
 tapestry-core/src/test/app1/WEB-INF/app.properties |  4 +-
 .../integration/app1/base/BaseRestDemoPage.java    |  3 ++
 .../integration/app1/data/rest/entities/Point.java | 10 -----
 .../app1/pages/rest/RestRequestNotHandledDemo.java |  2 +
 .../app1/pages/rest/RestTypeDescriptionsDemo.java  | 21 ++++++-----
 .../rest/RestWithEventHandlerMethodNameDemo.java   |  6 ++-
 .../app1/pages/rest/RestWithOnEventDemo.java       |  4 ++
 .../tapestry5/rest/jackson/test/pages/Index.java   | 12 +++++-
 12 files changed, 101 insertions(+), 33 deletions(-)

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
index 398236e..f26b19d 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/annotations/RestInfo.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/annotations/RestInfo.java
@@ -28,7 +28,7 @@ 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()}.
+ * used by all methods without the annotation, except for {{@link #returnType()}.
  * 
  * @see OpenApiDescriptionGenerator
  * @see OpenApiTypeDescriber
@@ -51,10 +51,10 @@ public @interface RestInfo
     String[] produces() default "";
 
     /**
-     * Defines the returned type of this REST event handler method in successful requests.
+     * Defines the return 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;
+    Class<?> returnType() default Object.class;
     
 }
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 0e814c5..b3bdb53 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
@@ -32,6 +32,7 @@ import javax.servlet.http.HttpServletResponse;
 import org.apache.tapestry5.SymbolConstants;
 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.RestInfo;
 import org.apache.tapestry5.annotations.StaticActivationContextValue;
@@ -322,14 +323,18 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
         {
             final JSONObject parameterDescription = new JSONObject();
             if (!isIgnored(parameter) && 
-                    parameter.getAnnotation(StaticActivationContextValue.class) == null)
+                    !parameter.isAnnotationPresent(StaticActivationContextValue.class))
             {
                 parameterDescription.put("in", "path");
             }
-            else if (parameter.getAnnotation(RequestParameter.class) != null)
+            else if (parameter.isAnnotationPresent(RequestParameter.class))
             {
                 parameterDescription.put("in", "query");
             }
+            else if (parameter.isAnnotationPresent(RequestBody.class))
+            {
+                processRequestBody(method, uri, httpMethod, methodDescription, parametersAsJsonArray, parameter);
+            }
             if (!parameterDescription.isEmpty())
             {
 //                Optional<String> parameterName = getValue(method, uri, httpMethod, parameter, "name");
@@ -349,6 +354,34 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
         }
     }
 
+    private void processRequestBody(Method method,
+            final String uri,
+            final String httpMethod,
+            final JSONObject methodDescription,
+            JSONArray parametersAsJsonArray,
+            Parameter parameter) {
+        JSONObject requestBodyDescription = new JSONObject();
+        requestBodyDescription.put("required", 
+                !(parameter.getAnnotation(RequestBody.class).allowEmpty()));
+        getValue(method, uri, httpMethod, "requestbody.description")
+            .ifPresent((v) -> requestBodyDescription.put("description", v));
+        
+        RestInfo restInfo = method.getAnnotation(RestInfo.class);
+        if (restInfo != null)
+        {
+            JSONObject contentDescription = new JSONObject();
+            for (String contentType : restInfo.consumes()) 
+            {
+                JSONObject schemaDescription = new JSONObject();
+                typeDescriber.describe(schemaDescription, parameter);
+                schemaDescription.remove("required");
+                contentDescription.put(contentType, schemaDescription);
+            }
+            requestBodyDescription.put("content", contentDescription);
+        }
+        methodDescription.put("requestBody", requestBodyDescription);
+    }
+
     private String getParameterName(Parameter parameter) {
         String name = null;
         final RequestParameter requestParameter = parameter.getAnnotation(RequestParameter.class);
@@ -403,7 +436,7 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
         else
         {
             restInfo = method.getDeclaringClass().getAnnotation(RestInfo.class);
-            if (isNonEmptyConsumes(restInfo))
+            if (isNonEmptyProduces(restInfo))
             {
                 produces = restInfo.produces();
             }
@@ -432,6 +465,11 @@ public class DefaultOpenApiDescriptionGenerator implements OpenApiDescriptionGen
     {
         return restInfo != null && !(restInfo.produces().length == 1 && "".equals(restInfo.produces()[0]));
     }
+    
+    private boolean isNonEmptyProduces(RestInfo restInfo)
+    {
+        return restInfo != null && !(restInfo.produces().length == 1 && "".equals(restInfo.produces()[0]));
+    }
 
     private boolean isPresent(JSONArray array, JSONObject object) 
     {
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
index 5f33382..bf4350e 100644
--- 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
@@ -97,7 +97,7 @@ public class DefaultOpenApiTypeDescriber implements OpenApiTypeDescriber
         final RestInfo restInfo = method.getAnnotation(RestInfo.class);
         if (restInfo != null)
         {
-            returnedType = restInfo.returnedType();
+            returnedType = restInfo.returnType();
         }
         else 
         {
diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/core.properties b/tapestry-core/src/main/resources/org/apache/tapestry5/core.properties
index 7f99509..61cac58 100644
--- a/tapestry-core/src/main/resources/org/apache/tapestry5/core.properties
+++ b/tapestry-core/src/main/resources/org/apache/tapestry5/core.properties
@@ -140,4 +140,22 @@ private-default-localdate-format=lll
 # see ComponentLibraries page
 not-informed=Not informed
 
-openapi.viewer-title=OpenAPI definition viewer
\ No newline at end of file
+# OpenAPI generation
+openapi.viewer-title=OpenAPI definition viewer
+openapi-title=OpenAPI description
+openapi.response.200=OK
+openapi.response.201=Created
+openapi.response.202=Accepted
+openapi.response.203=Non-Authoritative Information
+openapi.response.204=No Content
+openapi.response.303=See Other
+openapi.response.304=Not Modified
+openapi.response.400=Bad Request
+openapi.response.401=Unauthorized
+openapi.response.403=Forbidden
+openapi.response.404=Not found
+openapi.response.415=Unsupported Media Type
+openapi.response.418=I'm a teapot
+openapi.response.500=Internal Server Error
+openapi.response.501=Not Implemeted
+openapi.response.503=Service Unavailable
\ No newline at end of file
diff --git a/tapestry-core/src/test/app1/WEB-INF/app.properties b/tapestry-core/src/test/app1/WEB-INF/app.properties
index 27a3532..9259d5e 100644
--- a/tapestry-core/src/test/app1/WEB-INF/app.properties
+++ b/tapestry-core/src/test/app1/WEB-INF/app.properties
@@ -47,8 +47,8 @@ openapi.RestRequestNotHandledDemo.tag.name=nameTag
 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.BaseRestDemoPage./requestnothandleddemo/returningHttpStatus.put.requestbody.description = The request body must describe a point.
+openapi./requestnothandleddemo/returningHttpStatus.put.requestbody.description = The request body must describe a point, but less specifically.
 
 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!
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 f2e54a7..573fe8b 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
@@ -18,6 +18,7 @@ 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.RestInfo;
 import org.apache.tapestry5.annotations.StaticActivationContextValue;
 import org.apache.tapestry5.http.services.Response;
 import org.apache.tapestry5.json.JSONArray;
@@ -65,6 +66,7 @@ public class BaseRestDemoPage extends AbstractRestDemoPage {
     }
     
     @OnEvent(EventConstants.HTTP_PUT)
+    @RestInfo(produces = "text/plain")
     public Object returningHttpStatus(
             @StaticActivationContextValue("returningHttpStatus") String ignored,
             @RequestBody String parameter)
@@ -75,6 +77,7 @@ public class BaseRestDemoPage extends AbstractRestDemoPage {
     }
     
     @OnEvent(EventConstants.HTTP_PUT)
+    @RestInfo(consumes = "text/plain")
     public Object returningHttpStatusSimple(
             @StaticActivationContextValue("returningHttpStatusSimple") String ignored,
             @RequestBody String parameter)
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
index f616526..2a5f2cd 100644
--- 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
@@ -6,8 +6,6 @@ public class Point {
 
     private int y;
     
-    private Point nextPoint;
-
     public int getX() {
         return x;
     }
@@ -24,12 +22,4 @@ public class Point {
         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/rest/RestRequestNotHandledDemo.java b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/rest/RestRequestNotHandledDemo.java
index 0a62062..7a1bcbc 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
@@ -12,10 +12,12 @@
 package org.apache.tapestry5.integration.app1.pages.rest;
 
 import org.apache.tapestry5.EventConstants;
+import org.apache.tapestry5.annotations.RestInfo;
 import org.apache.tapestry5.integration.app1.base.BaseRestDemoPage;
 
 public class RestRequestNotHandledDemo extends BaseRestDemoPage {
     
+    @RestInfo(consumes = "text/plain")
     Object onHttpPut()
     {
         return createResponse(EventConstants.HTTP_PUT, "no body", "no parameter");
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
index acc9c44..0c6adf1 100644
--- 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
@@ -29,12 +29,13 @@ public class RestTypeDescriptionsDemo {
     private static final String TEXT_PLAIN = "text/plain";
 
     @OnEvent(EventConstants.HTTP_GET)
+    @RestInfo(consumes = "application/javascript", produces = "application/javascript")
     Object point(@StaticActivationContextValue("point") String ignored, Point p1, @RequestBody Point p2) {
         return null;
     }
     
     @OnEvent(EventConstants.HTTP_GET)
-    @RestInfo(returnedType = JSONArray.class)
+    @RestInfo(returnType = JSONArray.class, consumes = "application/javascript", produces = "application/javascript")    
     Object jsonArray(
             @StaticActivationContextValue("jsonArray") String ignored, 
             JSONArray jsonArray1,
@@ -43,55 +44,55 @@ public class RestTypeDescriptionsDemo {
     }
     
     @OnEvent(EventConstants.HTTP_GET)
-    @RestInfo(returnedType = JSONObject.class)    
+    @RestInfo(returnType = JSONObject.class, consumes = "application/javascript", produces = "application/javascript")    
     Object jsonObject(@StaticActivationContextValue("jsonObject") String ignored, JSONObject jsonObject, @RequestBody JSONObject jsonObject2) {
         return null;
     }
     
     @OnEvent(EventConstants.HTTP_GET)
-    @RestInfo(produces = TEXT_PLAIN, returnedType = boolean.class)
+    @RestInfo(produces = TEXT_PLAIN, returnType = 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)
+    @RestInfo(produces = TEXT_PLAIN, consumes = 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)
+    @RestInfo(produces = TEXT_PLAIN, consumes = 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)
+    @RestInfo(produces = TEXT_PLAIN, returnType = byte.class, consumes = TEXT_PLAIN)
     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)
+    @RestInfo(produces = TEXT_PLAIN, returnType = short.class, consumes = TEXT_PLAIN)
     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)
+    @RestInfo(produces = TEXT_PLAIN, returnType = int.class, consumes = TEXT_PLAIN)
     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)
+    @RestInfo(produces = TEXT_PLAIN, returnType = long.class, consumes = TEXT_PLAIN)
     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)
+    @RestInfo(produces = TEXT_PLAIN, returnType = String.class, consumes = TEXT_PLAIN)
     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 9b869ca..d1ec2ba 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
@@ -12,8 +12,8 @@
 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.base.BaseRestDemoPage;
 
@@ -34,6 +34,7 @@ public class RestWithEventHandlerMethodNameDemo extends BaseRestDemoPage {
         return createResponse(EventConstants.HTTP_HEAD, null, parameter);
     }
 
+    @RestInfo(consumes = "text/plain")
     Object onHttpPatch(
             @StaticActivationContextValue(SUBPATH) String subpath, 
             String parameter, 
@@ -42,6 +43,7 @@ public class RestWithEventHandlerMethodNameDemo extends BaseRestDemoPage {
         return createResponse(EventConstants.HTTP_PATCH, body, parameter);
     }
 
+    @RestInfo(consumes = "text/plain")
     Object onHttpPost(
             @StaticActivationContextValue(SUBPATH) String subpath, 
             String parameter, 
@@ -50,7 +52,7 @@ public class RestWithEventHandlerMethodNameDemo extends BaseRestDemoPage {
         return createResponse(EventConstants.HTTP_POST, body, parameter);
     }
 
-    @OnEvent(EventConstants.HTTP_PUT)
+    @RestInfo(consumes = "text/plain")
     Object onHttpPut(
             @StaticActivationContextValue(SUBPATH) String subpath, 
             String parameter, 
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 7534954..1111c26 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
@@ -14,6 +14,7 @@ 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.base.BaseRestDemoPage;
 import org.apache.tapestry5.util.TextStreamResponse;
@@ -39,6 +40,7 @@ public class RestWithOnEventDemo extends BaseRestDemoPage {
     }
 
     @OnEvent(EventConstants.HTTP_PATCH)
+    @RestInfo(consumes = "application/javascript")
     Object patch(
             @StaticActivationContextValue(SUBPATH) String subpath, 
             String parameter, 
@@ -48,6 +50,7 @@ public class RestWithOnEventDemo extends BaseRestDemoPage {
     }
 
     @OnEvent(EventConstants.HTTP_POST)
+    @RestInfo(consumes = "application/javascript")
     Object post(
             @StaticActivationContextValue(SUBPATH) String subpath, 
             String parameter, 
@@ -57,6 +60,7 @@ public class RestWithOnEventDemo extends BaseRestDemoPage {
     }
 
     @OnEvent(EventConstants.HTTP_PUT)
+    @RestInfo(consumes = "application/javascript")
     Object put(
             @StaticActivationContextValue(SUBPATH) String subpath, 
             String parameter, 
diff --git a/tapestry-rest-jackson/src/test/java/org/apache/tapestry5/rest/jackson/test/pages/Index.java b/tapestry-rest-jackson/src/test/java/org/apache/tapestry5/rest/jackson/test/pages/Index.java
index 7b53a6e..47e1f2d 100644
--- a/tapestry-rest-jackson/src/test/java/org/apache/tapestry5/rest/jackson/test/pages/Index.java
+++ b/tapestry-rest-jackson/src/test/java/org/apache/tapestry5/rest/jackson/test/pages/Index.java
@@ -26,6 +26,7 @@ import org.apache.tapestry5.rest.jackson.test.rest.entities.Attribute;
 import org.apache.tapestry5.rest.jackson.test.rest.entities.User;
 import org.apache.tapestry5.services.HttpStatus;
 import org.apache.tapestry5.services.PageRenderLinkSource;
+import org.apache.tapestry5.util.TextStreamResponse;
 
 public class Index 
 {
@@ -34,7 +35,7 @@ public class Index
     
     @Inject private PageRenderLinkSource pageRenderLinkSource;
     
-    @RestInfo(returnedType = User.class)
+    @RestInfo(returnType = User.class, produces = "application/json")
     @OnEvent(EventConstants.HTTP_GET)
     public Object getUserByEmail(String email)
     {
@@ -42,6 +43,14 @@ public class Index
         return user.isPresent() ? user.get() : HttpStatus.notFound();
     }
     
+    @RestInfo(returnType = int.class, produces = "text/plain")
+    @OnEvent(EventConstants.HTTP_GET)
+    public Object getUserCount(@StaticActivationContextValue("count") String ignored)
+    {
+        return new TextStreamResponse("UTF-8", String.valueOf(USERS.size()));
+    }
+    
+    @RestInfo(consumes = "application/json")
     public Object onHttpPut(@RequestBody User user) throws UnsupportedEncodingException
     {
         HttpStatus status;
@@ -58,6 +67,7 @@ public class Index
                 pageRenderLinkSource.createPageRenderLinkWithContext(Index.class, user.getEmail()));
     }
     
+    @RestInfo(returnType = User.class, produces = "application/json")
     @OnEvent(EventConstants.HTTP_GET)
     public User getExample(@StaticActivationContextValue("example") String example)
     {