Committing where I'm at even though I've discovered that jax.rs would be a better approach
diff --git a/org.apache.sling.servlets.json/pom.xml b/org.apache.sling.servlets.json/pom.xml
index 6c42b60..7c5b214 100644
--- a/org.apache.sling.servlets.json/pom.xml
+++ b/org.apache.sling.servlets.json/pom.xml
@@ -44,6 +44,14 @@
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
+ <configuration>
+ <bnd>
+ <![CDATA[
+ -fixupmessages "Classes found in the wrong directory"; restrict:=error; is:=warning
+ -includeresource: glob-[0-9\.]*.jar;lib:=true
+ ]]>
+ </bnd>
+ </configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
@@ -162,6 +170,13 @@
<groupId>org.osgi</groupId>
<artifactId>org.osgi.annotation.versioning</artifactId>
</dependency>
+
+ <dependency>
+ <groupId>com.hrakaroo</groupId>
+ <artifactId>glob</artifactId>
+ <version>0.9.0</version>
+ </dependency>
+
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/BaseJsonServlet.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/BaseJsonServlet.java
index 9932510..e63c12f 100644
--- a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/BaseJsonServlet.java
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/BaseJsonServlet.java
@@ -20,8 +20,6 @@
import java.util.Optional;
import java.util.Set;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -30,11 +28,8 @@
import org.apache.sling.auth.core.AuthenticationSupport;
import org.apache.sling.servlets.json.problem.Problem;
import org.apache.sling.servlets.json.problem.ProblemBuilder;
-import org.apache.sling.servlets.json.problem.Problematic;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ConsumerType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -50,129 +45,13 @@
* Problem response based on the thrown exception
*/
@ConsumerType
-public abstract class BaseJsonServlet extends HttpServlet {
+public interface BaseJsonServlet {
- private static final String RESPONSE_CONTENT_TYPE = "application/json";
+ static final String RESPONSE_CONTENT_TYPE = "application/json";
- private static final Set<String> SERVLET_SUPPORTED_METHODS = Set.of("GET", "HEAD", "POST", "PUT", "DELETE",
+ static final Set<String> SERVLET_SUPPORTED_METHODS = Set.of("GET", "HEAD", "POST", "PUT", "DELETE",
"OPTIONS", "TRACE");
- private static final Logger log = LoggerFactory.getLogger(BaseJsonServlet.class);
-
- /**
- * Called by the
- * {@link #service(HttpServletRequest, HttpServletResponse)} method
- * to handle an HTTP <em>GET</em> request.
- * <p>
- * This default implementation reports back to the client that the method is
- * not supported.
- * <p>
- * Implementations of this class should overwrite this method with their
- * implementation for the HTTP <em>PATCH</em> method support.
- *
- * @param request The HTTP request
- * @param response The HTTP response
- * @throws ServletException Not thrown by this implementation.
- * @throws IOException If the error status cannot be reported back to the
- * client.
- */
- @Override
- protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
- throws ServletException, IOException {
- sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
- }
-
- /**
- * Called by the
- * {@link #service(HttpServletRequest, HttpServletResponse)} method
- * to handle an HTTP <em>POST</em> request.
- * <p>
- * This default implementation reports back to the client that the method is
- * not supported.
- * <p>
- * Implementations of this class should overwrite this method with their
- * implementation for the HTTP <em>PATCH</em> method support.
- *
- * @param request The HTTP request
- * @param response The HTTP response
- * @throws ServletException Not thrown by this implementation.
- * @throws IOException If the error status cannot be reported back to the
- * client.
- */
- @Override
- protected void doPost(final HttpServletRequest req, final HttpServletResponse resp)
- throws ServletException, IOException {
- sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
- }
-
- /**
- * Called by the
- * {@link #service(HttpServletRequest, HttpServletResponse)} method
- * to handle an HTTP <em>PUT</em> request.
- * <p>
- * This default implementation reports back to the client that the method is
- * not supported.
- * <p>
- * Implementations of this class should overwrite this method with their
- * implementation for the HTTP <em>PATCH</em> method support.
- *
- * @param request The HTTP request
- * @param response The HTTP response
- * @throws ServletException Not thrown by this implementation.
- * @throws IOException If the error status cannot be reported back to the
- * client.
- */
- @Override
- protected void doPut(final HttpServletRequest req, final HttpServletResponse resp)
- throws ServletException, IOException {
- sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
- }
-
- /**
- * Called by the
- * {@link #service(HttpServletRequest, HttpServletResponse)} method
- * to handle an HTTP <em>DELETE</em> request.
- * <p>
- * This default implementation reports back to the client that the method is
- * not supported.
- * <p>
- * Implementations of this class should overwrite this method with their
- * implementation for the HTTP <em>PATCH</em> method support.
- *
- * @param request The HTTP request
- * @param response The HTTP response
- * @throws ServletException Not thrown by this implementation.
- * @throws IOException If the error status cannot be reported back to the
- * client.
- */
- @Override
- protected void doDelete(final HttpServletRequest req, final HttpServletResponse resp)
- throws ServletException, IOException {
- sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
- }
-
- /**
- * Called by the
- * {@link #service(HttpServletRequest, HttpServletResponse)} method
- * to handle an HTTP <em>PATCH</em> request.
- * <p>
- * This default implementation reports back to the client that the method is
- * not supported.
- * <p>
- * Implementations of this class should overwrite this method with their
- * implementation for the HTTP <em>PATCH</em> method support.
- *
- * @param request The HTTP request
- * @param response The HTTP response
- * @throws ServletException Not thrown by this implementation.
- * @throws IOException If the error status cannot be reported back to the
- * client.
- */
- protected void doPatch(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response)
- throws ServletException, IOException {
- handleMethodNotImplemented(request, response);
- }
-
/**
* Retrieves a <code>ResourceResolver</code> that can be used to perform various
* operations against the underlying repository.
@@ -180,56 +59,22 @@
* @return Resolver for performing operations. Will not be null.
* @throws LoginException unable to find resource resolver in request
*/
- public @NotNull ResourceResolver getResourceResolver(@NotNull HttpServletRequest request) throws LoginException {
+ default @NotNull ResourceResolver getResourceResolver(@NotNull HttpServletRequest request) throws LoginException {
return Optional.ofNullable(request.getAttribute(AuthenticationSupport.REQUEST_ATTRIBUTE_RESOLVER))
.map(ResourceResolver.class::cast)
.orElseThrow(() -> new LoginException("Could not get ResourceResolver from request"));
}
/**
- * Tries to handle the request by calling a Java method implemented for the
- * respective HTTP request method.
- * <p>
- * This implementation first calls the base class implementation and only if
- * the base class cannot dispatch will try to dispatch the supported methods
- * <em>PATCH</em>
- * <p>
- * In addition, this method catches ServletException, IOException and
- * RuntimeExceptions thrown from the called methods and sends a JSON
- * Problem response based on the thrown exception
+ * Read an object from the request, handing invalid or missing request bodies
+ * and returning a 400 response.
*
- * @param request The HTTP request
- * @param response The HTTP response
- * @return <code>true</code> if the requested method
- * (<code>request.getMethod()</code>)
- * is known. Otherwise <code>false</code> is returned.
- * @throws ServletException Forwarded from any of the dispatched methods
- * @throws IOException Forwarded from any of the dispatched methods
+ * @param <T> the type of object to be read from the request
+ * @param request the request from which to read the object
+ * @param type the class of the type to read
+ * @return the object read from the request
*/
- @Override
- protected void service(@NotNull HttpServletRequest request,
- @NotNull HttpServletResponse response) throws ServletException,
- IOException {
- final String method = request.getMethod();
- try {
- // assume the method is known for now
- if (SERVLET_SUPPORTED_METHODS.contains(method)) {
- super.service(request, response);
- } else if ("PATCH".equals(method)) {
- doPatch(request, response);
- } else {
- handleMethodNotImplemented(request, response);
- }
- } catch (IOException | ServletException | RuntimeException e) {
- if (e instanceof Problematic) {
- sendProblemResponse(response, ((Problematic) e).getProblem());
- } else {
- log.error("Handing uncaught exception", e);
- sendProblemResponse(response, ProblemBuilder.get().fromException(e).build());
- }
- }
-
- }
+ <T> T readRequestBody(HttpServletRequest request, Class<T> type);
/**
* Read an object from the request, handing invalid or missing request bodies
@@ -240,18 +85,7 @@
* @param type the class of the type to read
* @return the object read from the request
*/
- protected abstract <T> T readRequestBody(HttpServletRequest request, Class<T> type);
-
- /**
- * Read an object from the request, handing invalid or missing request bodies
- * and returning a 400 response.
- *
- * @param <T> the type of object to be read from the request
- * @param request the request from which to read the object
- * @param type the class of the type to read
- * @return the object read from the request
- */
- protected abstract <T> T readRequestBody(HttpServletRequest request, TypeReference<T> type);
+ <T> T readRequestBody(HttpServletRequest request, TypeReference<T> type);
/**
* Sends a JSON response with the content type application/json and a 200 status
@@ -261,7 +95,7 @@
* @param responseBody the object to write to the response
* @throws IOException an exception occurs writing the object to the response
*/
- protected void sendJsonResponse(HttpServletResponse response, Object responseBody)
+ default void sendJsonResponse(HttpServletResponse response, Object responseBody)
throws IOException {
sendJsonResponse(response, HttpServletResponse.SC_OK, responseBody);
}
@@ -274,7 +108,7 @@
* @param responseBody the object to write to the response
* @throws IOException an exception occurs writing the object to the response
*/
- protected void sendJsonResponse(HttpServletResponse response, int statusCode, Object responseBody)
+ default void sendJsonResponse(HttpServletResponse response, int statusCode, Object responseBody)
throws IOException {
sendJsonResponse(response, statusCode, RESPONSE_CONTENT_TYPE, responseBody);
}
@@ -288,7 +122,7 @@
* @param responseBody the object to write to the response
* @throws IOException an exception occurs writing the object to the response
*/
- protected abstract void sendJsonResponse(HttpServletResponse response, int statusCode, String contentType,
+ void sendJsonResponse(HttpServletResponse response, int statusCode, String contentType,
Object responseBody) throws IOException;
/**
@@ -300,7 +134,7 @@
* @param problemBuilder the problem to write
* @throws IOException Thrown if the problem cannot be written to the response
*/
- protected void sendProblemResponse(HttpServletResponse response, Problem problem)
+ default void sendProblemResponse(HttpServletResponse response, Problem problem)
throws IOException {
sendJsonResponse(response, problem.getStatus(), ProblemBuilder.RESPONSE_CONTENT_TYPE,
problem);
@@ -314,7 +148,7 @@
* @param response The HTTP response to which the error status is sent.
* @throws IOException Thrown if the status cannot be sent to the client.
*/
- protected void handleMethodNotImplemented(@NotNull HttpServletRequest request,
+ default void handleMethodNotImplemented(@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response) throws IOException {
sendProblemResponse(response,
ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/DynamicRequestServlet.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/DynamicRequestServlet.java
new file mode 100644
index 0000000..55d7f9f
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/DynamicRequestServlet.java
@@ -0,0 +1,82 @@
+/*
+ * 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.sling.servlets.json;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.servlets.json.dynamicrequest.DynamicRequestMapper;
+import org.apache.sling.servlets.json.problem.ProblemBuilder;
+import org.apache.sling.servlets.json.problem.Problematic;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DynamicRequestServlet extends JacksonJsonServlet {
+
+ private static final Logger log = LoggerFactory.getLogger(DynamicRequestServlet.class);
+
+ private final DynamicRequestMapper mapper;
+
+ public DynamicRequestServlet() {
+ mapper = new DynamicRequestMapper(this);
+ }
+
+ /**
+ * Tries to handle the request by calling a Java method implemented for the
+ * respective HTTP request method.
+ * <p>
+ * This implementation first attempts to resolve handling methods by
+ * identifying methods with the @RequestHandler annotation which
+ * apply to the provided request. Any such request handler
+ * <p>
+ * In addition, this method catches ServletException, IOException and
+ * RuntimeExceptions thrown from the called methods and sends a JSON
+ * Problem response based on the thrown exception
+ *
+ * @param request The HTTP request
+ * @param response The HTTP response
+ * @return <code>true</code> if the requested method
+ * (<code>request.getMethod()</code>)
+ * is known. Otherwise <code>false</code> is returned.
+ * @throws ServletException Forwarded from any of the dispatched methods
+ * @throws IOException Forwarded from any of the dispatched methods
+ */
+ @Override
+ protected void service(@NotNull HttpServletRequest request,
+ @NotNull HttpServletResponse response) throws ServletException,
+ IOException {
+ boolean serviced = false;
+ try {
+ serviced = mapper.mayService(request, response);
+ if (!serviced) {
+ super.service(request, response);
+ }
+ } catch (IOException | ServletException | RuntimeException e) {
+ if (e instanceof Problematic) {
+ sendProblemResponse(response, ((Problematic) e).getProblem());
+ } else {
+ log.error("Handing uncaught exception", e);
+ sendProblemResponse(response, ProblemBuilder.get().fromException(e).build());
+ }
+ }
+ }
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/JacksonJsonServlet.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/JacksonJsonServlet.java
index d040274..a220f9a 100644
--- a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/JacksonJsonServlet.java
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/JacksonJsonServlet.java
@@ -19,10 +19,14 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.sling.servlets.json.problem.ProblemBuilder;
+import org.apache.sling.servlets.json.problem.Problematic;
+import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ConsumerType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,7 +40,7 @@
* An extension of the BaseJsonServlet using Jackson for serialization.
*/
@ConsumerType
-public abstract class JacksonJsonServlet extends BaseJsonServlet {
+public abstract class JacksonJsonServlet extends HttpServlet implements BaseJsonServlet {
private static final Logger log = LoggerFactory.getLogger(JacksonJsonServlet.class);
@@ -45,6 +49,164 @@
private static final ObjectReader objectReader = objectMapper.reader();
/**
+ * Called by the
+ * {@link #service(HttpServletRequest, HttpServletResponse)} method
+ * to handle an HTTP <em>GET</em> request.
+ * <p>
+ * This default implementation reports back to the client that the method is
+ * not supported.
+ * <p>
+ * Implementations of this class should overwrite this method with their
+ * implementation for the HTTP <em>PATCH</em> method support.
+ *
+ * @param request The HTTP request
+ * @param response The HTTP response
+ * @throws ServletException Not thrown by this implementation.
+ * @throws IOException If the error status cannot be reported back to the
+ * client.
+ */
+ @Override
+ protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
+ throws ServletException, IOException {
+ sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
+ }
+
+ /**
+ * Called by the
+ * {@link #service(HttpServletRequest, HttpServletResponse)} method
+ * to handle an HTTP <em>POST</em> request.
+ * <p>
+ * This default implementation reports back to the client that the method is
+ * not supported.
+ * <p>
+ * Implementations of this class should overwrite this method with their
+ * implementation for the HTTP <em>PATCH</em> method support.
+ *
+ * @param request The HTTP request
+ * @param response The HTTP response
+ * @throws ServletException Not thrown by this implementation.
+ * @throws IOException If the error status cannot be reported back to the
+ * client.
+ */
+ @Override
+ protected void doPost(final HttpServletRequest req, final HttpServletResponse resp)
+ throws ServletException, IOException {
+ sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
+ }
+
+ /**
+ * Called by the
+ * {@link #service(HttpServletRequest, HttpServletResponse)} method
+ * to handle an HTTP <em>PUT</em> request.
+ * <p>
+ * This default implementation reports back to the client that the method is
+ * not supported.
+ * <p>
+ * Implementations of this class should overwrite this method with their
+ * implementation for the HTTP <em>PATCH</em> method support.
+ *
+ * @param request The HTTP request
+ * @param response The HTTP response
+ * @throws ServletException Not thrown by this implementation.
+ * @throws IOException If the error status cannot be reported back to the
+ * client.
+ */
+ @Override
+ protected void doPut(final HttpServletRequest req, final HttpServletResponse resp)
+ throws ServletException, IOException {
+ sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
+ }
+
+ /**
+ * Called by the
+ * {@link #service(HttpServletRequest, HttpServletResponse)} method
+ * to handle an HTTP <em>DELETE</em> request.
+ * <p>
+ * This default implementation reports back to the client that the method is
+ * not supported.
+ * <p>
+ * Implementations of this class should overwrite this method with their
+ * implementation for the HTTP <em>PATCH</em> method support.
+ *
+ * @param request The HTTP request
+ * @param response The HTTP response
+ * @throws ServletException Not thrown by this implementation.
+ * @throws IOException If the error status cannot be reported back to the
+ * client.
+ */
+ @Override
+ protected void doDelete(final HttpServletRequest req, final HttpServletResponse resp)
+ throws ServletException, IOException {
+ sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
+ }
+
+ /**
+ * Called by the
+ * {@link #service(HttpServletRequest, HttpServletResponse)} method
+ * to handle an HTTP <em>PATCH</em> request.
+ * <p>
+ * This default implementation reports back to the client that the method is
+ * not supported.
+ * <p>
+ * Implementations of this class should overwrite this method with their
+ * implementation for the HTTP <em>PATCH</em> method support.
+ *
+ * @param request The HTTP request
+ * @param response The HTTP response
+ * @throws ServletException Not thrown by this implementation.
+ * @throws IOException If the error status cannot be reported back to the
+ * client.
+ */
+ protected void doPatch(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response)
+ throws ServletException, IOException {
+ handleMethodNotImplemented(request, response);
+ }
+
+ /**
+ * Tries to handle the request by calling a Java method implemented for the
+ * respective HTTP request method.
+ * <p>
+ * This implementation first calls the base class implementation and only if
+ * the base class cannot dispatch will try to dispatch the supported methods
+ * <em>PATCH</em>
+ * <p>
+ * In addition, this method catches ServletException, IOException and
+ * RuntimeExceptions thrown from the called methods and sends a JSON
+ * Problem response based on the thrown exception
+ *
+ * @param request The HTTP request
+ * @param response The HTTP response
+ * @return <code>true</code> if the requested method
+ * (<code>request.getMethod()</code>)
+ * is known. Otherwise <code>false</code> is returned.
+ * @throws ServletException Forwarded from any of the dispatched methods
+ * @throws IOException Forwarded from any of the dispatched methods
+ */
+ @Override
+ protected void service(@NotNull HttpServletRequest request,
+ @NotNull HttpServletResponse response) throws ServletException,
+ IOException {
+ final String method = request.getMethod();
+ try {
+ // assume the method is known for now
+ if (SERVLET_SUPPORTED_METHODS.contains(method)) {
+ super.service(request, response);
+ } else if ("PATCH".equals(method)) {
+ doPatch(request, response);
+ } else {
+ handleMethodNotImplemented(request, response);
+ }
+ } catch (IOException | ServletException | RuntimeException e) {
+ if (e instanceof Problematic) {
+ sendProblemResponse(response, ((Problematic) e).getProblem());
+ } else {
+ log.error("Handing uncaught exception", e);
+ sendProblemResponse(response, ProblemBuilder.get().fromException(e).build());
+ }
+ }
+ }
+
+ /**
* Provides the Jackson ObjectWriter instance to use for writing objects to the
* response.
* <p>
@@ -53,7 +215,7 @@
*
* @return the ObjectWriter
*/
- protected ObjectWriter getObjectWriter() {
+ public ObjectWriter getObjectWriter() {
return objectWriter;
}
@@ -66,7 +228,7 @@
*
* @return the ObjectReader
*/
- protected ObjectReader getObjectReader() {
+ public ObjectReader getObjectReader() {
return objectReader;
}
@@ -80,7 +242,7 @@
* @return the object read from the request
*/
@Override
- protected <T> T readRequestBody(HttpServletRequest request, Class<T> type) {
+ public <T> T readRequestBody(HttpServletRequest request, Class<T> type) {
try {
return getObjectReader().readValue(request.getReader(), type);
} catch (IOException e) {
@@ -99,7 +261,7 @@
* @return the object read from the request
*/
@Override
- protected <T> T readRequestBody(HttpServletRequest request, TypeReference<T> type) {
+ public <T> T readRequestBody(HttpServletRequest request, TypeReference<T> type) {
try {
return getObjectReader().forType(type).readValue(request.getReader());
} catch (IOException e) {
@@ -118,7 +280,7 @@
* @throws IOException an exception occurs writing the object to the response
*/
@Override
- protected void sendJsonResponse(HttpServletResponse response, int statusCode, String contentType,
+ public void sendJsonResponse(HttpServletResponse response, int statusCode, String contentType,
Object responseBody) throws IOException {
if (!response.isCommitted()) {
response.reset();
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestBody.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestBody.java
new file mode 100644
index 0000000..2f6980a
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestBody.java
@@ -0,0 +1,28 @@
+/*
+ * 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.sling.servlets.json.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface RequestBody {
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestHandler.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestHandler.java
new file mode 100644
index 0000000..36335b7
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestHandler.java
@@ -0,0 +1,32 @@
+/*
+ * 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.sling.servlets.json.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface RequestHandler {
+
+ String[] methods();
+
+ String path();
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestParameter.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestParameter.java
new file mode 100644
index 0000000..78eb5b9
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestParameter.java
@@ -0,0 +1,30 @@
+/*
+ * 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.sling.servlets.json.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface RequestParameter {
+
+ String name();
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapper.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapper.java
new file mode 100644
index 0000000..b5f4e3b
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapper.java
@@ -0,0 +1,183 @@
+/*
+ * 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.sling.servlets.json.dynamicrequest;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.servlets.json.DynamicRequestServlet;
+import org.apache.sling.servlets.json.annotations.RequestBody;
+import org.apache.sling.servlets.json.annotations.RequestHandler;
+import org.apache.sling.servlets.json.annotations.RequestParameter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DynamicRequestMapper {
+
+ private static final Logger log = LoggerFactory.getLogger(DynamicRequestMapper.class);
+
+ private final Set<String> mappingKeys = new HashSet<>();
+ private final Map<String, List<DynamicRequestMapping>> mappings = new HashMap<>();
+ private final DynamicRequestServlet instance;
+
+ public DynamicRequestMapper(DynamicRequestServlet instance) {
+ readRequestHandlers(instance);
+ this.instance = instance;
+ }
+
+ public boolean mayService(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+ final String method = request.getMethod();
+ final String path = request.getRequestURI();
+ Optional<DynamicRequestMapping> mappingOp = Optional.ofNullable(mappings.get(method))
+ .orElse(Collections.emptyList()).stream().filter(drm -> drm.matches(path)).findFirst();
+ if (mappingOp.isPresent()) {
+ callMethod(request, response, mappingOp.get());
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void callMethod(final HttpServletRequest request, final HttpServletResponse response,
+ final DynamicRequestMapping dynamicRequestMapping) throws IOException, ServletException {
+ final Method method = dynamicRequestMapping.getMethod();
+ final List<Object> values = new ArrayList<>();
+ final ValueMap parameters = new ValueMapDecorator(request.getParameterMap());
+ for (Parameter param : method.getParameters()) {
+ if (ServletRequest.class.isAssignableFrom(param.getType())) {
+ log.trace("Adding request for param: {}", param);
+ values.add(request);
+ } else if (ServletResponse.class.isAssignableFrom(param.getType())) {
+ log.trace("Adding response for param: {}", param);
+ values.add(response);
+ } else if (param.isAnnotationPresent(RequestBody.class)) {
+ log.debug("Adding response body as {} for param: {}", param.getType(), param);
+ values.add(instance.readRequestBody(request, param.getType()));
+ } else if (param.isAnnotationPresent(RequestParameter.class)) {
+ RequestParameter rp = param.getAnnotation(RequestParameter.class);
+ log.debug("Adding response parameter {} as {} for param: {}", rp.name(), param.getType(), param);
+ values.add(parameters.get(rp.name(), param.getType()));
+ } else {
+ throw new RequestMappingException("Failed to call : " + dynamicRequestMapping
+ + " parameter " + param.getName()
+ + " must either be a ServletRequest, ServletResponse or be annotated with request value mappings");
+ }
+ }
+
+ try {
+ log.trace("Invoking method {} with parameters {}", method, values);
+ Object value = method.invoke(instance, values.toArray());
+ if (value != null) {
+ log.trace("Recieved response {}", value);
+ instance.sendJsonResponse(response, value);
+ }
+ } catch (IllegalAccessException | IllegalArgumentException e) {
+ throw new RequestMappingException("Unexpected exception invoking method " + method.toGenericString(), e);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ }
+ if (cause instanceof IOException) {
+ throw (IOException) cause;
+ }
+ if (cause instanceof ServletException) {
+ throw (ServletException) cause;
+ }
+ throw new RequestMappingException("Unexpected exception invoking method " + method.toGenericString(), e);
+ }
+ }
+
+ private void readRequestHandlers(HttpServlet instance) {
+ log.debug("Loading request handlers from: {}", instance.getClass());
+ for (Method method : instance.getClass().getDeclaredMethods()) {
+ log.trace("Evaluating method: {}", method);
+ RequestHandler handler = method.getAnnotation(RequestHandler.class);
+ if (handler != null) {
+ readRequestHandler(handler, method);
+ }
+ }
+ }
+
+ private void readRequestHandler(RequestHandler handler, Method method) {
+ log.trace("Found request handler {} in method: {}", handler, method);
+ DynamicRequestMapping mapping = new DynamicRequestMapping(handler, method);
+ validateMapping(mapping);
+ Arrays.stream(handler.methods()).forEach(m -> {
+
+ log.trace("Adding request handler {} for method: {}", mapping, m);
+ mappings.computeIfAbsent(m, k -> new ArrayList<>());
+ mappings.get(m).add(mapping);
+ });
+ }
+
+ private Collection<String> getMappingKeys(RequestHandler handler) {
+ return Arrays.stream(handler.methods()).map(m -> '[' + m + "] " + handler.path()).collect(Collectors.toList());
+ }
+
+ private void validateMapping(DynamicRequestMapping mapping) {
+ // validate that there aren't duplicate request handlers
+ log.trace("Validing that requst handler is duplicate");
+ getMappingKeys(mapping.getHandler()).forEach(kp -> {
+ if (mappingKeys.contains(kp)) {
+ throw new RequestMappingException("Validation failed for method: " + mapping.getMethod()
+ + " a request handler is already registred for " + kp);
+ }
+ mappingKeys.add(kp);
+ });
+
+ log.trace("Validing request handler can be injecteds");
+ validateCanInject(mapping.getMethod());
+ }
+
+ private void validateCanInject(Method method) {
+ for (Parameter param : method.getParameters()) {
+ if (!ServletRequest.class.isAssignableFrom(param.getType())
+ && !ServletResponse.class.isAssignableFrom(param.getType())
+ && !param.isAnnotationPresent(RequestBody.class)
+ && !param.isAnnotationPresent(RequestParameter.class)) {
+ throw new RequestMappingException("Validation failed for method: " + method
+ + " parameter " + param.getName()
+ + " must either be a ServletRequest, ServletResponse or be annotated with request value mappings");
+ }
+ }
+ }
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapping.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapping.java
new file mode 100644
index 0000000..7c521c8
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapping.java
@@ -0,0 +1,76 @@
+/*
+ * 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.sling.servlets.json.dynamicrequest;
+
+import java.lang.reflect.Method;
+
+import org.apache.sling.servlets.json.annotations.RequestHandler;
+
+import com.hrakaroo.glob.GlobPattern;
+import com.hrakaroo.glob.MatchingEngine;
+
+public class DynamicRequestMapping {
+
+ private final RequestHandler handler;
+ private final Method method;
+ private final MatchingEngine matcher;
+
+ /**
+ * @param handler
+ * @param method
+ */
+ public DynamicRequestMapping(RequestHandler handler, Method method) {
+ this.handler = handler;
+ this.method = method;
+ matcher = GlobPattern.compile(handler.path());
+ }
+
+ /**
+ * @return the handler
+ */
+ public RequestHandler getHandler() {
+ return handler;
+ }
+
+ /**
+ * @return the method
+ */
+ public Method getMethod() {
+ return method;
+ }
+
+ /**
+ * If true, this DynamicRequestMapping instance matches the request path
+ *
+ * @param requestPath the request path for this request
+ * @return whether or not the request path matches
+ */
+ public boolean matches(String requestPath) {
+ return matcher.matches(requestPath);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "DynamicRequestMapping [handler=" + handler + ", method=" + method + "]";
+ }
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/RequestMappingException.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/RequestMappingException.java
new file mode 100644
index 0000000..3d9bdce
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/RequestMappingException.java
@@ -0,0 +1,57 @@
+/*
+ * 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.sling.servlets.json.dynamicrequest;
+
+import org.apache.sling.api.SlingException;
+
+public class RequestMappingException extends SlingException {
+
+ /**
+ * Constructs a new RequestMappingException
+ */
+ protected RequestMappingException() {
+ super();
+ }
+
+ /**
+ * Constructs a new RequestMappingException with the given text
+ *
+ * @param text the exception text
+ */
+ protected RequestMappingException(String text) {
+ super(text);
+ }
+
+ /**
+ * Constructs a new RequestMappingException with a cause
+ *
+ * @param text the exception text
+ * @param cause the root cause
+ */
+ public RequestMappingException(String text, Throwable cause) {
+ super(text, cause);
+ }
+
+ /**
+ * Constructs a new RequestMappingException with only a cause
+ *
+ * @param cause the root cause
+ */
+ protected RequestMappingException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/DynamicRequestServletTest.java b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/DynamicRequestServletTest.java
new file mode 100644
index 0000000..5d9e2b8
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/DynamicRequestServletTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.sling.servlets.json;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.sling.servlets.json.dynamicrequest.DuplicateHandlersServlet;
+import org.apache.sling.servlets.json.dynamicrequest.InvalidArgumentsServlet;
+import org.apache.sling.servlets.json.dynamicrequest.RequestMappingException;
+import org.apache.sling.servlets.json.dynamicrequest.SampleDynamicRequestServlet;
+import org.apache.sling.testing.mock.sling.junit5.SlingContext;
+import org.apache.sling.testing.mock.sling.junit5.SlingContextExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+@ExtendWith(SlingContextExtension.class)
+class DynamicRequestServletTest {
+
+ private final SampleDynamicRequestServlet servlet = new SampleDynamicRequestServlet();
+
+ private final SlingContext context = new SlingContext();
+
+ @Test
+ void testSimple() throws ServletException, IOException {
+ context.request().setServletPath("/simple");
+ context.request().setMethod("GET");
+ servlet.service(context.request(), context.response());
+
+ assertEquals(200, context.response().getStatus());
+ assertEquals("application/json;charset=UTF-8", context.response().getContentType());
+ assertEquals("{\"Hello\":\"World\"}", context.response().getOutputAsString());
+ }
+
+ @ParameterizedTest
+ @CsvSource(value = {
+ "/simple/bob,200",
+ "/simple/bob/sal,200",
+ "/simple/bob.png,200",
+ "/simple2/glob,405"
+ })
+ void testGlob(String path, int status) throws ServletException, IOException {
+ context.request().setServletPath(path);
+ context.request().setMethod("GET");
+ servlet.service(context.request(), context.response());
+ assertEquals(status, context.response().getStatus());
+ }
+
+ @Test
+ void testOrdering() throws ServletException, IOException {
+ context.request().setServletPath("/simple/glob");
+ context.request().setMethod("GET");
+ servlet.service(context.request(), context.response());
+ assertEquals(200, context.response().getStatus());
+ assertEquals("{\"Hello\":\"World2\"}", context.response().getOutputAsString());
+ }
+
+ @Test
+ void testNoResponse() throws ServletException, IOException {
+ context.request().setServletPath("/no-response");
+ context.request().setMethod("POST");
+ servlet.service(context.request(), context.response());
+ assertEquals(202, context.response().getStatus());
+ assertEquals("", context.response().getOutputAsString());
+ }
+
+ @Test
+ void supportsParameters() throws ServletException, IOException {
+ context.request().setServletPath("/with-param");
+ context.request().setMethod("GET");
+ context.request().addRequestParameter("name", "Sling");
+ servlet.service(context.request(), context.response());
+ assertEquals(200, context.response().getStatus());
+ assertEquals("{\"Hello\":\"Sling\"}", context.response().getOutputAsString());
+ }
+
+ @Test
+ void supportsMissingParams() throws ServletException, IOException {
+ context.request().setServletPath("/with-param");
+ context.request().setMethod("GET");
+ servlet.service(context.request(), context.response());
+ assertEquals(200, context.response().getStatus());
+ assertEquals("{\"Hello\":null}", context.response().getOutputAsString());
+ }
+
+ @Test
+ void handlesThrownExceptions() throws ServletException, IOException {
+ context.request().setServletPath("/npe");
+ context.request().setMethod("GET");
+ servlet.service(context.request(), context.response());
+ assertEquals(500, context.response().getStatus());
+ assertEquals("application/problem+json;charset=UTF-8", context.response().getContentType());
+ assertEquals(
+ "{\"title\":\"Internal Server Error\",\"status\":500,\"detail\":\"java.lang.NullPointerException\"}",
+ context.response().getOutputAsString());
+ }
+
+ @Test
+ void wontConstructWithDuplicateHandlers() {
+ assertThrows(RequestMappingException.class, () -> new DuplicateHandlersServlet());
+ }
+
+ @Test
+ void wontConstructWithInvalidArguments() {
+ assertThrows(Exception.class, () -> new InvalidArgumentsServlet());
+ }
+}
diff --git a/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/DuplicateHandlersServlet.java b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/DuplicateHandlersServlet.java
new file mode 100644
index 0000000..dccfbfb
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/DuplicateHandlersServlet.java
@@ -0,0 +1,32 @@
+/*
+ * 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.sling.servlets.json.dynamicrequest;
+
+import org.apache.sling.servlets.json.DynamicRequestServlet;
+import org.apache.sling.servlets.json.annotations.RequestHandler;
+
+public class DuplicateHandlersServlet extends DynamicRequestServlet {
+
+ @RequestHandler(methods = { "GET" }, path = "/simple")
+ public void simpleHandler() {
+ }
+
+ @RequestHandler(methods = { "GET" }, path = "/simple")
+ public void duplicateHandler() {
+ }
+
+}
diff --git a/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/InvalidArgumentsServlet.java b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/InvalidArgumentsServlet.java
new file mode 100644
index 0000000..058e90e
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/InvalidArgumentsServlet.java
@@ -0,0 +1,28 @@
+/*
+ * 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.sling.servlets.json.dynamicrequest;
+
+import org.apache.sling.servlets.json.DynamicRequestServlet;
+import org.apache.sling.servlets.json.annotations.RequestHandler;
+
+public class InvalidArgumentsServlet extends DynamicRequestServlet {
+
+ @RequestHandler(methods = { "GET" }, path = "/simple")
+ public void duplicateHandler(String name) {
+ }
+
+}
diff --git a/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/SampleDynamicRequestServlet.java b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/SampleDynamicRequestServlet.java
new file mode 100644
index 0000000..31d91fa
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/SampleDynamicRequestServlet.java
@@ -0,0 +1,59 @@
+/*
+ * 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.sling.servlets.json.dynamicrequest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.servlets.json.DynamicRequestServlet;
+import org.apache.sling.servlets.json.annotations.RequestHandler;
+import org.apache.sling.servlets.json.annotations.RequestParameter;
+
+public class SampleDynamicRequestServlet extends DynamicRequestServlet {
+
+ @RequestHandler(methods = { "GET" }, path = "/simple")
+ public Map<String, Object> simpleHandler() {
+ return Map.of("Hello", "World");
+ }
+
+ @RequestHandler(methods = { "GET" }, path = "/simple/**")
+ public Map<String, Object> globHandler() {
+ return Map.of("Hello", "World2");
+ }
+
+ @RequestHandler(methods = { "POST" }, path = "/no-response")
+ public void noResponse(HttpServletResponse response) {
+ response.setStatus(HttpServletResponse.SC_ACCEPTED);
+ }
+
+ @RequestHandler(methods = { "GET" }, path = "/with-param")
+ public Map<String, Object> withParameter(HttpServletResponse response,
+ @RequestParameter(name = "name") String name) {
+ Map<String, Object> resp = new HashMap<>();
+ resp.put("Hello", name);
+ return resp;
+ }
+
+ @RequestHandler(methods = { "GET" }, path = "/npe")
+ public void npe(HttpServletResponse response,
+ @RequestParameter(name = "name") String name) {
+ name.length();
+ }
+
+}