From cc92d157fd79df2ba7622ebd7063927492823d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 10 Mar 2021 13:07:14 -0500 Subject: [PATCH 1/5] Ensure error messages use JsonEncoding Close #40 --- src/main/java/co/zeroae/gate/App.java | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/co/zeroae/gate/App.java b/src/main/java/co/zeroae/gate/App.java index 2716ff0..80152b8 100644 --- a/src/main/java/co/zeroae/gate/App.java +++ b/src/main/java/co/zeroae/gate/App.java @@ -142,14 +142,28 @@ public APIGatewayProxyResponseEvent handleExecute(APIGatewayProxyRequestEvent in logger.error(e); AWSXRay.getCurrentSubsegmentOptional().ifPresent((segment -> segment.addException(e))); response.getHeaders().put("Content-Type", "application/json"); - return response.withStatusCode(400).withBody(String.format( - "{\"message\":\"%s\"}", e.getMessage())); + try { + return response.withStatusCode(400).withBody(new ObjectMapper().writeValueAsString( + new HashMap() {{ + put("message", e.getMessage()); + }} + )); + } catch (JsonProcessingException jsonProcessingException) { + throw new RuntimeException(jsonProcessingException); + } } catch (IOException e) { logger.error(e); AWSXRay.getCurrentSubsegmentOptional().ifPresent((segment -> segment.addException(e))); response.getHeaders().put("Content-Type", "application/json"); - return response.withStatusCode(406).withBody(String.format( - "{\"message\":\"%s\"}", e.getMessage())); + try { + return response.withStatusCode(406).withBody(new ObjectMapper().writeValueAsString( + new HashMap() {{ + put("message", e.getMessage()); + }} + )); + } catch (JsonProcessingException jsonProcessingException) { + throw new RuntimeException(jsonProcessingException); + } } } From f1c9f8bfa077cc8f3f42263c8cfc389d15326112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 10 Mar 2021 13:08:17 -0500 Subject: [PATCH 2/5] Include the invalid path expression in error message --- src/main/java/co/zeroae/gate/App.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/co/zeroae/gate/App.java b/src/main/java/co/zeroae/gate/App.java index 80152b8..bf75fab 100644 --- a/src/main/java/co/zeroae/gate/App.java +++ b/src/main/java/co/zeroae/gate/App.java @@ -70,7 +70,7 @@ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent in else if (path.matches("^/([^/]*)/metadata/?$")) return handleMetadata(input, context); else - throw new RuntimeException("How did you get here?"); + throw new RuntimeException("Unexpected path expression " + path); } public APIGatewayProxyResponseEvent handleMetadata(APIGatewayProxyRequestEvent input, final Context context) { From b3fed102e9741481964e8e727f81ccce5ef4bb40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 10 Mar 2021 13:26:27 -0500 Subject: [PATCH 3/5] Refactor request and response validation code into Utils. --- src/main/java/co/zeroae/gate/App.java | 26 ++++----------------- src/main/java/co/zeroae/gate/Utils.java | 28 +++++++++++++++++++++++ src/test/java/co/zeroae/gate/AppTest.java | 8 +++---- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/main/java/co/zeroae/gate/App.java b/src/main/java/co/zeroae/gate/App.java index bf75fab..93f9b50 100644 --- a/src/main/java/co/zeroae/gate/App.java +++ b/src/main/java/co/zeroae/gate/App.java @@ -24,7 +24,6 @@ import java.net.URL; import java.net.URLStreamHandler; import java.util.*; -import java.util.function.Supplier; /** * This class implements a GATE application using AWS Lambda. @@ -54,10 +53,6 @@ public class App implements RequestHandler exporters = AWSXRay.createSegment( - "Gate Exporters", Utils::loadExporters - ); - private static final DocumentLRUCache cache = AWSXRay.createSegment("Cache Init", () -> new DocumentLRUCache(App.CACHE_DIR, App.CACHE_DIR_USAGE)); @@ -94,26 +89,13 @@ public APIGatewayProxyResponseEvent handleExecute(APIGatewayProxyRequestEvent in final Map> mQueryStringParams = Optional.ofNullable(input.getMultiValueQueryStringParameters()).orElse(new HashMap<>()); try { final String acceptHeader = input.getHeaders().getOrDefault("Accept", "application/json"); - final String responseType = ((Supplier) () -> { - for (String mimeType : acceptHeader.split(",")) { - if (exporters.containsKey(mimeType.trim())) - return mimeType.trim(); - else if (exporters.containsKey(mimeType.split(";")[0].trim())) - return mimeType.split(";")[0].trim(); - } - return null; - }).get(); - if (responseType != null) - response.getHeaders().put("Content-Type", responseType.split(";")[0].trim()); - - final DocumentExporter exporter = exporters.get(responseType); - if (exporter == null) - throw new IOException("Unsupported response content type."); - + final String responseType = Utils.ensureValidResponseType(acceptHeader); + final DocumentExporter exporter = Utils.exporters.get(responseType); + response.getHeaders().put("Content-Type", responseType.split(";")[0].trim()); final FeatureMap featureMap = Factory.newFeatureMap(); final Integer nextAnnotationId = Integer.parseInt(queryStringParams.getOrDefault("nextAnnotationId", "0")); - final String contentType = input.getHeaders().getOrDefault("Content-Type", "text/plain"); + final String contentType = Utils.ensureValidRequestContentType(input.getHeaders().getOrDefault("Content-Type", "text/plain")); final String contentDigest = AWSXRay.createSubsegment("Message Digest",() -> { String rv = Utils.computeMessageDigest(contentType + input.getBody() + nextAnnotationId + DIGEST_SALT); AWSXRay.getCurrentSubsegment().putMetadata("SHA256", rv); diff --git a/src/main/java/co/zeroae/gate/Utils.java b/src/main/java/co/zeroae/gate/Utils.java index d16ff6f..1ee8e05 100644 --- a/src/main/java/co/zeroae/gate/Utils.java +++ b/src/main/java/co/zeroae/gate/Utils.java @@ -18,6 +18,34 @@ public class Utils { + static final Map exporters = loadExporters(); + + static String ensureValidRequestContentType(String contentType) throws GateException { + final String rv = contentType.equals("application/json") ? "text/json" : contentType; + if (!DocumentFormat.getSupportedMimeTypes().contains(rv)) { + throw new GateException( + "Unsupported MIME type " + contentType + " valid options are " + + Arrays.toString(DocumentFormat.getSupportedMimeTypes() + .stream() + .map((type) -> type.equals("text/json") ? "application/json" : type) + .sorted() + .toArray()) + ); + } + return rv; + } + + static String ensureValidResponseType(String acceptHeader) throws GateException { + for (String mimeType : acceptHeader.split(",")) { + if (exporters.containsKey(mimeType.trim())) + return mimeType.trim(); + else if (exporters.containsKey(mimeType.split(";")[0].trim())) + return mimeType.split(";")[0].trim(); + } + throw new GateException("Unsupported MIME response type " + acceptHeader + ", valid options are " + + Arrays.toString(exporters.keySet().stream().sorted().toArray())); + } + @FunctionalInterface interface GATESupplier { T get() throws GateException; diff --git a/src/test/java/co/zeroae/gate/AppTest.java b/src/test/java/co/zeroae/gate/AppTest.java index ec74ad6..39e4cc8 100644 --- a/src/test/java/co/zeroae/gate/AppTest.java +++ b/src/test/java/co/zeroae/gate/AppTest.java @@ -11,7 +11,7 @@ import com.fasterxml.jackson.core.JsonToken; import com.sun.xml.fastinfoset.stax.StAXDocumentParser; import gate.Document; -import gate.DocumentFormat; +import gate.util.GateException; import org.apache.commons.codec.binary.Base64InputStream; import org.junit.After; import org.junit.Before; @@ -201,11 +201,11 @@ public void testCache() { } @Test - public void testInputTypes() { + public void testInputTypes() throws GateException { String[] types = { "application/fastinfoset", + "application/json", "text/html", - "text/json", "text/plain", "text/xml", "text/x-cochrane", @@ -213,7 +213,7 @@ public void testInputTypes() { "text/x-json-datasift", "text/x-json-twitter", }; - for (String type: types) assertNotNull(DocumentFormat.getMimeTypeForString(type)); + for (String type: types) assertNotNull(Utils.ensureValidRequestContentType(type)); } @Test From ecf3f25ed3b59640878252aa7b900e250bdfbfb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 10 Mar 2021 14:16:26 -0500 Subject: [PATCH 4/5] Add asJson call to write neat error messages. --- src/main/java/co/zeroae/gate/App.java | 28 +++++++++---------------- src/main/java/co/zeroae/gate/Utils.java | 12 +++++++++++ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/main/java/co/zeroae/gate/App.java b/src/main/java/co/zeroae/gate/App.java index 93f9b50..27349e5 100644 --- a/src/main/java/co/zeroae/gate/App.java +++ b/src/main/java/co/zeroae/gate/App.java @@ -124,28 +124,20 @@ public APIGatewayProxyResponseEvent handleExecute(APIGatewayProxyRequestEvent in logger.error(e); AWSXRay.getCurrentSubsegmentOptional().ifPresent((segment -> segment.addException(e))); response.getHeaders().put("Content-Type", "application/json"); - try { - return response.withStatusCode(400).withBody(new ObjectMapper().writeValueAsString( - new HashMap() {{ - put("message", e.getMessage()); - }} - )); - } catch (JsonProcessingException jsonProcessingException) { - throw new RuntimeException(jsonProcessingException); - } + return response.withStatusCode(400).withBody(Utils.asJson( + new HashMap() {{ + put("message", e.getMessage()); + }} + )); } catch (IOException e) { logger.error(e); AWSXRay.getCurrentSubsegmentOptional().ifPresent((segment -> segment.addException(e))); response.getHeaders().put("Content-Type", "application/json"); - try { - return response.withStatusCode(406).withBody(new ObjectMapper().writeValueAsString( - new HashMap() {{ - put("message", e.getMessage()); - }} - )); - } catch (JsonProcessingException jsonProcessingException) { - throw new RuntimeException(jsonProcessingException); - } + return response.withStatusCode(406).withBody(Utils.asJson( + new HashMap() {{ + put("message", e.getMessage()); + }} + )); } } diff --git a/src/main/java/co/zeroae/gate/Utils.java b/src/main/java/co/zeroae/gate/Utils.java index 1ee8e05..aab2c52 100644 --- a/src/main/java/co/zeroae/gate/Utils.java +++ b/src/main/java/co/zeroae/gate/Utils.java @@ -1,5 +1,7 @@ package co.zeroae.gate; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import gate.*; import gate.corpora.*; import gate.corpora.export.GATEJsonExporter; @@ -18,8 +20,18 @@ public class Utils { + static final ObjectMapper objectMapper = new ObjectMapper(); + static final Map exporters = loadExporters(); + static String asJson(Map obj) { + try { + return objectMapper.writeValueAsString(obj); + } catch (JsonProcessingException jsonProcessingException) { + throw new RuntimeException(jsonProcessingException); + } + } + static String ensureValidRequestContentType(String contentType) throws GateException { final String rv = contentType.equals("application/json") ? "text/json" : contentType; if (!DocumentFormat.getSupportedMimeTypes().contains(rv)) { From cd50f023939a719857460a993cfde3541af7cd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sodr=C3=A9?= Date: Wed, 10 Mar 2021 14:25:58 -0500 Subject: [PATCH 5/5] Refactoring... --- src/main/java/co/zeroae/gate/App.java | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/java/co/zeroae/gate/App.java b/src/main/java/co/zeroae/gate/App.java index 27349e5..c88a1e3 100644 --- a/src/main/java/co/zeroae/gate/App.java +++ b/src/main/java/co/zeroae/gate/App.java @@ -85,24 +85,29 @@ public APIGatewayProxyResponseEvent handleMetadata(APIGatewayProxyRequestEvent i public APIGatewayProxyResponseEvent handleExecute(APIGatewayProxyRequestEvent input, final Context context) { final APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(new HashMap<>()); - final Map queryStringParams = Optional.ofNullable(input.getQueryStringParameters()).orElse(new HashMap<>()); - final Map> mQueryStringParams = Optional.ofNullable(input.getMultiValueQueryStringParameters()).orElse(new HashMap<>()); + final Map headers = input.getHeaders(); + final Map queryStringParams = Optional.ofNullable( + input.getQueryStringParameters()).orElse(new HashMap<>()); + final Map> mQueryStringParams = Optional.ofNullable( + input.getMultiValueQueryStringParameters()).orElse(new HashMap<>()); try { - final String acceptHeader = input.getHeaders().getOrDefault("Accept", "application/json"); - final String responseType = Utils.ensureValidResponseType(acceptHeader); + final String responseType = Utils.ensureValidResponseType(headers.getOrDefault( + "Accept", "application/json")); final DocumentExporter exporter = Utils.exporters.get(responseType); response.getHeaders().put("Content-Type", responseType.split(";")[0].trim()); final FeatureMap featureMap = Factory.newFeatureMap(); - final Integer nextAnnotationId = Integer.parseInt(queryStringParams.getOrDefault("nextAnnotationId", "0")); - final String contentType = Utils.ensureValidRequestContentType(input.getHeaders().getOrDefault("Content-Type", "text/plain")); + final Integer nextAnnotationId = Integer.parseInt(queryStringParams.getOrDefault( + "nextAnnotationId", "0")); + final String contentType = Utils.ensureValidRequestContentType(headers.getOrDefault( + "Content-Type", "text/plain")); final String contentDigest = AWSXRay.createSubsegment("Message Digest",() -> { String rv = Utils.computeMessageDigest(contentType + input.getBody() + nextAnnotationId + DIGEST_SALT); AWSXRay.getCurrentSubsegment().putMetadata("SHA256", rv); return rv; }); featureMap.put("nextAnnotationId", nextAnnotationId); - putRequestBody(featureMap, contentType, contentDigest, input.getBody(), input.getIsBase64Encoded()); + featureMapPutContent(featureMap, contentType, contentDigest, input.getBody(), input.getIsBase64Encoded()); response.getHeaders().put("x-zae-gate-cache", "HIT"); final Document doc = cache.computeIfNull(contentDigest, () -> { @@ -141,7 +146,13 @@ public APIGatewayProxyResponseEvent handleExecute(APIGatewayProxyRequestEvent in } } - private void putRequestBody(FeatureMap featureMap, String mimeType, String contentDigest, String content, boolean isBase64Encoded) throws MalformedURLException { + private void featureMapPutContent( + FeatureMap featureMap, + String mimeType, + String contentDigest, + String content, + boolean isBase64Encoded + ) throws MalformedURLException { featureMap.put(Document.DOCUMENT_MIME_TYPE_PARAMETER_NAME, mimeType); if (!isBase64Encoded) featureMap.put(Document.DOCUMENT_STRING_CONTENT_PARAMETER_NAME, content);