Skip to content

Commit

Permalink
Produces-annotation from JAX-RS; custom Produces-annotation deleted
Browse files Browse the repository at this point in the history
  • Loading branch information
Gmugra committed Jan 20, 2021
1 parent f683c81 commit e93d118
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 80 deletions.
36 changes: 16 additions & 20 deletions core/src/main/java/net/cactusthorn/routing/EntryPointScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,22 @@ public static final class EntryPoint {

private PathTemplate pathTemplate;
private MethodInvoker methodInvoker;
private String produces;
private List<MediaType> producesMediaTypes;
private Set<MediaType> consumesMediaTypes;
private String template;
private Set<String> userRoles;

private EntryPoint(PathTemplate pathTemplate, String produces, String template, Set<MediaType> consumesMediaTypes,
MethodInvoker methodInvoker, Set<String> userRoles) {
private EntryPoint(PathTemplate pathTemplate, String template, List<MediaType> producesMediaTypes,
Set<MediaType> consumesMediaTypes, MethodInvoker methodInvoker, Set<String> userRoles) {
this.pathTemplate = pathTemplate;
this.produces = produces;
this.producesMediaTypes = producesMediaTypes;
this.template = template;
this.consumesMediaTypes = consumesMediaTypes;
this.methodInvoker = methodInvoker;
this.userRoles = userRoles;
}

public Response invoke(HttpServletRequest req, HttpServletResponse res, ServletContext con, PathValues pathValues,
List<MediaType> accept) {
public Response invoke(HttpServletRequest req, HttpServletResponse res, ServletContext con, PathValues pathValues) {
Object result = methodInvoker.invoke(req, res, con, pathValues);
if (result instanceof Response) {
return (Response) result;
Expand Down Expand Up @@ -85,19 +84,15 @@ public String template() {
return template;
}

public String produces() {
return produces;
}

// TODO support multiple values in Produces
public boolean matchAccept(List<MediaType> accept) {
MediaType producesMediaType = MediaType.valueOf(produces);
for (MediaType mediaType : accept) {
if (mediaType.isCompatible(producesMediaType)) {
return true;
public Optional<MediaType> matchAccept(List<MediaType> accept) {
for (MediaType acceptMediaType : accept) {
for (MediaType producesMediaType : producesMediaTypes) {
if (acceptMediaType.isCompatible(producesMediaType)) {
return Optional.of(producesMediaType);
}
}
}
return false;
return Optional.empty();
}

public boolean matchUserRole(HttpServletRequest req) {
Expand Down Expand Up @@ -136,6 +131,7 @@ public Map<String, List<EntryPoint>> scan() {

String classPath = PATHTEMPLATE_PARSER.prepare(routingConfig.applicationPath(), clazz.getAnnotation(Path.class));
Set<MediaType> classConsumesMediaTypes = CONSUMES_PARSER.consumes(clazz);
List<MediaType> classProducesMediaTypes = PRODUCES_PARSER.produces(clazz);

for (Method method : clazz.getMethods()) {

Expand All @@ -147,7 +143,7 @@ public Map<String, List<EntryPoint>> scan() {

PathTemplate pathTemplate = PATHTEMPLATE_PARSER.create(method, classPath);

String produces = PRODUCES_PARSER.produces(method);
List<MediaType> producesMediaTypes = PRODUCES_PARSER.produces(method, classProducesMediaTypes);

Set<MediaType> consumesMediaTypes = CONSUMES_PARSER.consumes(method, classConsumesMediaTypes);

Expand All @@ -157,8 +153,8 @@ public Map<String, List<EntryPoint>> scan() {

Set<String> userRoles = findUserRoles(method);

EntryPoint entryPoint = new EntryPoint(pathTemplate, produces, template, consumesMediaTypes, methodInvoker,
userRoles);
EntryPoint entryPoint = new EntryPoint(pathTemplate, template, producesMediaTypes, consumesMediaTypes,
methodInvoker, userRoles);
entryPoints.get(httpMethod).add(entryPoint);
}
}
Expand Down
19 changes: 2 additions & 17 deletions core/src/main/java/net/cactusthorn/routing/Http.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.RuntimeDelegate;
import javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate;

Expand Down Expand Up @@ -83,9 +80,9 @@ public static void writeHeaders(HttpServletResponse response, MultivaluedMap<Str
}
}

public static List<MediaType> parseAccept(HttpServletRequest request) {
public static List<MediaType> parseAccept(Enumeration<String> acceptHeader) {
List<MediaType> mediaTypes = new ArrayList<>();
for (Enumeration<String> e = request.getHeaders(HttpHeaders.ACCEPT); e.hasMoreElements();) {
for (Enumeration<String> e = acceptHeader; e.hasMoreElements();) {
String header = e.nextElement();
String[] parts = header.split(",");
for (String part : parts) {
Expand All @@ -99,16 +96,4 @@ public static List<MediaType> parseAccept(HttpServletRequest request) {
}
return mediaTypes;
}

// TODO support multiple values in Produces
public static MediaType findResponseMediaType(Response response, String produces, String defaultCharset) {
if (response.getMediaType() == null) {
MediaType producesMediaType = MediaType.valueOf(produces);
return producesMediaType.withCharset(defaultCharset);
}
if (response.getMediaType().getParameters().get(MediaType.CHARSET_PARAMETER) == null) {
return response.getMediaType().withCharset(defaultCharset);
}
return response.getMediaType();
}
}
34 changes: 29 additions & 5 deletions core/src/main/java/net/cactusthorn/routing/ProducesParser.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
package net.cactusthorn.routing;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import net.cactusthorn.routing.annotation.Produces;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

public final class ProducesParser {

public static final String PRODUCES_DEFAULT = "text/plain";
List<MediaType> produces(Class<?> clazz) {
Produces produces = clazz.getAnnotation(Produces.class);
if (produces != null) {
return parseProduces(produces.value());
}
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.TEXT_PLAIN_TYPE);
return mediaTypes;
}

String produces(Method method) {
List<MediaType> produces(Method method, List<MediaType> classMediaTypes) {
Produces produces = method.getAnnotation(Produces.class);
if (produces != null) {
return produces.value();
return parseProduces(produces.value());
}
return classMediaTypes;
}

private List<MediaType> parseProduces(String[] consumes) {
List<MediaType> mediaTypes = new ArrayList<>();
for (String value : consumes) {
for (String subValue : value.split(",")) {
String[] parts = subValue.trim().split(";")[0].split("/");
mediaTypes.add(new MediaType(parts[0], parts[1]));
}
}
return PRODUCES_DEFAULT;
Collections.sort(mediaTypes, Http.ACCEPT_COMPARATOR);
return mediaTypes;
}
}
40 changes: 34 additions & 6 deletions core/src/main/java/net/cactusthorn/routing/RoutingServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand All @@ -11,6 +12,7 @@
import javax.ws.rs.HttpMethod;
import javax.ws.rs.ServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
Expand Down Expand Up @@ -108,7 +110,7 @@ private void process(HttpServletRequest req, HttpServletResponse resp, List<Entr
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not Found");
return;
}
List<MediaType> accept = Http.parseAccept(req);
List<MediaType> accept = Http.parseAccept(req.getHeaders(HttpHeaders.ACCEPT));
boolean matchContentTypeFail = false;
boolean matchAcceptFail = false;
for (EntryPoint entryPoint : entryPoints) {
Expand All @@ -123,12 +125,20 @@ private void process(HttpServletRequest req, HttpServletResponse resp, List<Entr
matchContentTypeFail = true;
continue;
}
if (!entryPoint.matchAccept(accept)) {
Optional<MediaType> producesMediaType = entryPoint.matchAccept(accept);
if (!producesMediaType.isPresent()) {
matchAcceptFail = true;
continue;
}
Response result = entryPoint.invoke(req, resp, servletContext, pathValues, accept);
produce(resp, entryPoint, result);
Response result = entryPoint.invoke(req, resp, servletContext, pathValues);
//It could be that in resource method Response object was created manually and media-type was set,
//and this media-type do not match request Accept-header.
//In this case -> response error at ones.
if (!matchAccept(accept, result)) {
resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "Not acceptable");
return;
}
produce(resp, entryPoint, result, producesMediaType.get());
} catch (WebApplicationException wae) {
resp.sendError(wae.getResponse().getStatus(), wae.getMessage());
} catch (Exception e) {
Expand Down Expand Up @@ -167,7 +177,7 @@ private String getPath(String contentType, HttpServletRequest req) {
return path;
}

private void produce(HttpServletResponse resp, EntryPoint entryPoint, Response result) throws IOException {
private void produce(HttpServletResponse resp, EntryPoint entryPoint, Response result, MediaType producesMediaType) throws IOException {

StatusType status = result.getStatusInfo();
resp.setStatus(status.getStatusCode());
Expand All @@ -176,7 +186,13 @@ private void produce(HttpServletResponse resp, EntryPoint entryPoint, Response r
return;
}

MediaType responseMediaType = Http.findResponseMediaType(result, entryPoint.produces(), responseCharacterEncoding);
MediaType responseMediaType = producesMediaType;
if (result.getMediaType() != null) {
responseMediaType = result.getMediaType();
}
if (responseMediaType.getParameters().get(MediaType.CHARSET_PARAMETER) == null) {
responseMediaType = responseMediaType.withCharset(responseCharacterEncoding);
}
resp.setCharacterEncoding(responseMediaType.getParameters().get(MediaType.CHARSET_PARAMETER));
resp.setContentType(new MediaType(responseMediaType.getType(), responseMediaType.getSubtype()).toString());

Expand All @@ -199,4 +215,16 @@ private MessageBodyWriter findBodyWriter(MediaType responseMediaType, ReturnObje
}
throw new ServerErrorException("MessageBodyWriter not found", Status.INTERNAL_SERVER_ERROR);
}

private boolean matchAccept(List<MediaType> accept, Response result) {
if (result.getMediaType() == null) {
return true;
}
for (MediaType acceptMediaType : accept) {
if (acceptMediaType.isCompatible(result.getMediaType())) {
return true;
}
}
return false;
}
}

This file was deleted.

12 changes: 2 additions & 10 deletions core/src/test/java/net/cactusthorn/routing/HttpTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

public class HttpTest {

Expand Down Expand Up @@ -59,13 +56,11 @@ private String _toString(MediaType mediaType) {

@Test //
public void parseAccept() {
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
List<String> accept = new ArrayList<>();
accept.add(
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9");
Mockito.when(request.getHeaders(HttpHeaders.ACCEPT)).thenReturn(Collections.enumeration(accept));

List<MediaType> mediaTypes = Http.parseAccept(request);
List<MediaType> mediaTypes = Http.parseAccept(Collections.enumeration(accept));
assertEquals("text/html", mediaTypes.get(0).toString());
assertEquals("application/xhtml+xml", mediaTypes.get(1).toString());
assertEquals("image/avif", mediaTypes.get(2).toString());
Expand All @@ -78,10 +73,7 @@ public void parseAccept() {

@Test //
public void parseEmptyAccept() {
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
Mockito.when(request.getHeaders(HttpHeaders.ACCEPT)).thenReturn(Collections.emptyEnumeration());

List<MediaType> mediaTypes = Http.parseAccept(request);
List<MediaType> mediaTypes = Http.parseAccept(Collections.emptyEnumeration());
assertEquals(MediaType.WILDCARD, mediaTypes.get(0).toString());
}
}
32 changes: 27 additions & 5 deletions core/src/test/java/net/cactusthorn/routing/RoutingServletTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
Expand All @@ -52,7 +53,7 @@

public class RoutingServletTest {

@javax.ws.rs.Produces(MediaType.TEXT_HTML) public static class TestTemplated implements TemplatedMessageBodyWriter {
@Produces(MediaType.TEXT_HTML) public static class TestTemplated implements TemplatedMessageBodyWriter {

@Override public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return true;
Expand Down Expand Up @@ -159,13 +160,18 @@ public Response role() throws URISyntaxException {
@POST @Path("api/template") //
public Response template(@Context HttpServletRequest request, @Context HttpServletResponse response) {
Templated t = new Templated(request, response, "t", "templated result");
return Response.ok(t).type(MediaType.TEXT_HTML_TYPE).build();
return Response.ok(t).type(MediaType.TEXT_HTML_TYPE.withCharset("UTF-8")).build();
}

@POST @Path("api/template/A") @Template("/xyz.html") @Produces("text/html")//
public String templateA(@Context HttpServletRequest request, @Context HttpServletResponse response) {
@POST @Path("api/template/A") @Template("/xyz.html") @Produces("text/html,*/*")//
public String templateA() {
return "some value";
}

@GET @Path("api/wrong/produces") @Produces(MediaType.TEXT_HTML) //
public Response wrongProduces() {
return Response.ok("some value").type(MediaType.TEXT_PLAIN_TYPE).build();
}
}

public static class EntryPoint1Provider implements ComponentProvider {
Expand Down Expand Up @@ -200,7 +206,6 @@ void setUp() throws IOException {
req = Mockito.mock(HttpServletRequest.class);
resp = Mockito.mock(HttpServletResponse.class);
Mockito.when(req.getHeaders(HttpHeaders.ACCEPT)).thenReturn(Collections.emptyEnumeration());
// Mockito.when(req.getPathInfo()).thenReturn("/api/wrong/abc");
outputStream = new ServletTestOutputStream();
Mockito.when(resp.getOutputStream()).thenReturn(outputStream);
}
Expand Down Expand Up @@ -488,6 +493,23 @@ public void userRoles() throws ServletException, IOException {
assertEquals(403, code.getValue());
}

@Test
public void wrongProduces() throws ServletException, IOException {
Mockito.when(req.getPathInfo()).thenReturn("/api/wrong/produces");
Mockito.when(req.getMethod()).thenReturn(HttpMethod.GET);
List<String> accept = new ArrayList<>();
accept.add(MediaType.TEXT_HTML);
Mockito.when(req.getHeaders(HttpHeaders.ACCEPT)).thenReturn(Collections.enumeration(accept));

servlet.doGet(req, resp);

ArgumentCaptor<Integer> code = ArgumentCaptor.forClass(Integer.class);

Mockito.verify(resp).sendError(code.capture(), Mockito.any());

assertEquals(406, code.getValue());
}

public static class EntryPointWrong {

@GET @Path("/dddd{/") //
Expand Down
Loading

0 comments on commit e93d118

Please sign in to comment.