From 6e95891d4f6d9e89e6a2d954e350b0e982215a4f Mon Sep 17 00:00:00 2001 From: Hazel Belmont Date: Mon, 29 Jul 2024 14:51:30 -0400 Subject: [PATCH 1/6] create new response class and use it in all routes and helper classes (where necessary) --- .gitignore | 4 +- src/main/java/Mongo.java | 78 ++++++++++------- src/main/java/Routes.java | 75 +++++++---------- src/main/java/httputils/Response.java | 116 ++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 80 deletions(-) create mode 100644 src/main/java/httputils/Response.java diff --git a/.gitignore b/.gitignore index bc353a7..c0fd8a7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ bin .vscode .idea -Credentials.java \ No newline at end of file +Credentials.java + +.DS_Store \ No newline at end of file diff --git a/src/main/java/Mongo.java b/src/main/java/Mongo.java index 27fc4c9..9c560e1 100644 --- a/src/main/java/Mongo.java +++ b/src/main/java/Mongo.java @@ -32,6 +32,8 @@ import com.mongodb.client.model.Updates; import com.mongodb.client.result.InsertOneResult; +import httputils.Response; + public class Mongo { private ConnectionString connectionString = Credentials.connectionString; private MongoClientSettings clientSettings = MongoClientSettings.builder().applyConnectionString(connectionString) @@ -54,16 +56,18 @@ public Mongo(Crypto crypto) { * Gets all documents from the posts collection and puts them in an ArrayList. * @return an ArrayList of JSON strings from MongoDB */ - public ArrayList getAllPosts() { + public Response getAllPosts() { try { FindIterable docs = db.getCollection("posts").find(); ArrayList docsJSON = new ArrayList(); docs.forEach(doc -> docsJSON.add(doc.toJson())); - return docsJSON; + + // TODO: update javadoc to reflect new return value + return new Response().withCode(200).withAllowGetMethodHeader().withBody("{\"posts\": " + docsJSON.toString() + "}"); } catch(Exception e) { - System.out.println(e); - return new ArrayList(); + e.printStackTrace(); + return new Response().withBody("{\"error\": " + e + "}"); } } @@ -72,16 +76,20 @@ public ArrayList getAllPosts() { * @param dateString The datestring to search for formatted MMDDYY, i.e. 092123 * @return The JSON string returned from the MongoDB posts collection */ - public String getPost(String dateString) { + public Response getPost(String dateString) { try { - MongoCollection posts = db.getCollection("posts"); - String doc = posts.find(Filters.eq("dateString", dateString)).first().toJson(); - return doc; + Document doc = posts.find(Filters.eq("dateString", dateString)).first(); + if(doc == null) { + return new Response().withCode(400).withAllowGetMethodHeader().withBody("{\"error\": \"A post with the dateString " + dateString + " could not be found.\"}"); + } + + // TODO: update javadoc to reflect new return value + return new Response().withCode(200).withAllowGetMethodHeader().withBody(doc.toJson()); } catch(Exception e) { - System.out.println(e); - return ""; + e.printStackTrace(); + return new Response().withBody("{\"error\": " + e + "}"); } } @@ -90,21 +98,26 @@ public String getPost(String dateString) { * @param oid the hex string _id of the image to get * @return a base64 encoded string image (WITH leading data URL format, i.e. "data:image/[format];base64,") */ - public String getImage(String oid) { + public Response getImage(String oid) { try { - GridFSBucket bucket = GridFSBuckets.create(db, "images"); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); GridFSFile fileToDownload = bucket.find(Filters.eq("_id", new BsonObjectId(new ObjectId(oid)))).first(); - bucket.downloadToStream(new ObjectId(oid), outputStream); + + if(fileToDownload == null) { + return new Response().withCode(400).withAllowGetMethodHeader().withBody("{\"error\": \"An image with the oid " + oid + " could not be found.\"}"); + } + bucket.downloadToStream(new ObjectId(oid), outputStream); String imageb64 = outputStream.toString(); - return "{\"fileName\": \"" + fileToDownload.getFilename() + "\", \"featured\":" + fileToDownload.getMetadata().get("featured") + ", \"data\": \"" + imageb64 + "\"}"; + String body = "{\"fileName\": \"" + fileToDownload.getFilename() + "\", \"featured\": " + fileToDownload.getMetadata().get("featured") + ", \"data\": \"" + imageb64 + "\"}"; + outputStream.close(); + + return new Response().withCode(200).withAllowGetMethodHeader().withBody(body); } catch (Exception e) { e.printStackTrace(); - System.out.println(e); - return ""; + return new Response().withBody("{\"error\": " + e + "}"); } } @@ -114,9 +127,8 @@ public String getImage(String oid) { * @param fileName the filename to associate with the file, i.e. "file.jpeg" * @return the hex string _id of the inserted image */ - public String putImage(String imageb64, String fileName, Boolean featured) { + public Response postImage(String imageb64, String fileName, Boolean featured) { try { - GridFSBucket bucket = GridFSBuckets.create(db, "images"); MongoCollection imagesCollection = db.getCollection("images.files"); GridFSUploadOptions options = new GridFSUploadOptions().metadata(new Document("featured", featured)); @@ -133,12 +145,12 @@ public String putImage(String imageb64, String fileName, Boolean featured) { bucket.delete(id); } id = bucket.find(query).first().getObjectId(); - return id.toHexString(); + + return new Response().withCode(200).withAllowPostMethodHeader().withBody("{\"uploadedID\": \"" + id.toHexString() + "\"}"); } catch(Exception e) { - System.out.println(e); - // TODO: better error handling - return "Error" + e; + e.printStackTrace(); + return new Response().withBody("{\"error\": " + e + "}"); } } @@ -149,7 +161,7 @@ public String putImage(String imageb64, String fileName, Boolean featured) { * @param description a description of the day's meeting * @return the hex string _id of the inserted post document */ - public String putPost(String dateString, JSONArray imageIDs, String description) { + public Response putPost(String dateString, JSONArray imageIDs, String description) { try { ArrayList ids = new ArrayList(); for(int i = 0; i < imageIDs.length(); i++) { @@ -158,11 +170,12 @@ public String putPost(String dateString, JSONArray imageIDs, String description) MongoCollection posts = db.getCollection("posts"); InsertOneResult result = posts.insertOne(new Document("dateString", dateString).append("images", new BsonArray(ids)).append("description", description)); - return result.getInsertedId().asObjectId().getValue().toHexString(); + + return new Response().withCode(200).withAllowPostMethodHeader().withBody("{\"uploadedID\": \"" + result.getInsertedId().asObjectId().getValue().toHexString() + "\"}"); } catch(Exception e) { - System.out.println(e); - return "Error" + e; + e.printStackTrace(); + return new Response().withBody("{\"error\": " + e + "}"); } } @@ -175,7 +188,6 @@ public String putPost(String dateString, JSONArray imageIDs, String description) public boolean checkCredentials(String uname, String pword) { boolean valid = false; try { - MongoCollection auth = db.getCollection("auth"); Document creds = auth.find().first(); Set keys = creds.keySet(); @@ -203,7 +215,7 @@ public boolean checkCredentials(String uname, String pword) { * Creates a random eight-character string that will serve as an access token for verified users. * @return the random eight-character access token. */ - public String createToken(String username) { + public Response createToken(String username) { Random generator = new Random(); long seed = generator.nextInt(10000) * username.hashCode(); generator.setSeed(seed); @@ -212,15 +224,16 @@ public String createToken(String username) { .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) .toString(); try { - MongoCollection auth = db.getCollection("auth"); auth.findOneAndUpdate(Filters.empty(), Updates.push("activeSessions", new Document("token", generatedString).append("dateTime", new BsonDateTime(new Date().getTime())).append("username", username))); + + return new Response().withCode(200).withAllowGetMethodHeader().withBody("{\"token\": \"" + generatedString + "\"}"); } catch(Exception e) { e.printStackTrace(); + return new Response().withBody("{\"error\": " + e + "}"); } - return generatedString; } /** @@ -231,10 +244,12 @@ public String createToken(String username) { public boolean checkToken(String token) { boolean valid = false; try { - MongoCollection auth = db.getCollection("auth"); Document creds = auth.find().first(); + + @SuppressWarnings("unchecked") ArrayList active = (ArrayList) creds.get("activeSessions"); + for(Document session : active) { if(session.get("token").equals(token)) { valid = true; @@ -252,7 +267,6 @@ public boolean checkToken(String token) { */ public void deleteExpired() { try { - MongoCollection auth = db.getCollection("auth"); auth.findOneAndUpdate(Filters.empty(), Updates.pull("activeSessions", Filters.lt("dateTime", new BsonDateTime(new Date().getTime() - 3 * 60 * 60 * 1000)))); // 3 * 60 * 60 * 1000 = 3 hours diff --git a/src/main/java/Routes.java b/src/main/java/Routes.java index 46ef8e1..a1eb16f 100644 --- a/src/main/java/Routes.java +++ b/src/main/java/Routes.java @@ -11,10 +11,7 @@ public Routes(Mongo mongo, Crypto auth) { this.routeDate = new Route() { @Override public Object handle(Request request, Response response) { - response.status(200); - response.body(mongo.getPost(request.params("date"))); - response.header("Access-Control-Allow-Origin", "*"); - response.header("Access-Control-Allow-Methods", "GET"); + response = mongo.getPost(request.params("date")).asSparkResponse(response); return response.body(); } }; @@ -22,10 +19,7 @@ public Object handle(Request request, Response response) { this.routeAll = new Route() { @Override public Object handle(Request request, Response response) { - response.status(200); - response.body("{\"posts\": " + mongo.getAllPosts().toString() + "}"); - response.header("Access-Control-Allow-Origin", "*"); - response.header("Access-Control-Allow-Methods", "GET"); + response = mongo.getAllPosts().asSparkResponse(response); return response.body(); } }; @@ -33,10 +27,7 @@ public Object handle(Request request, Response response) { this.routeImage = new Route() { @Override public Object handle(Request request, Response response) { - response.status(200); - response.body(mongo.getImage(request.params("oidString"))); - response.header("Access-Control-Allow-Origin", "*"); - response.header("Access-Control-Allow-Methods", "GET"); + response = mongo.getImage(request.params("oidString")).asSparkResponse(response); return response.body(); } }; @@ -46,16 +37,11 @@ public Object handle(Request request, Response response) { public Object handle(Request request, Response response) { if(mongo.checkToken(request.headers("Authorization").substring("Bearer ".length()))) { JSONObject json = new JSONObject(request.body()); - String id = mongo.putImage(json.get("data").toString(), json.get("fileName").toString(), json.getBoolean("featured")); - response.status(200); - response.body("{\"uploadedID\": \"" + id + "\"}"); + response = mongo.postImage(json.get("data").toString(), json.get("fileName").toString(), json.getBoolean("featured")).asSparkResponse(response); } else { - response.status(401); - response.body("{\"error\": \"401: You do not have authorization to view this information.\"}"); + response = httputils.Response.unauthorizedError().asSparkResponse(response); } - response.header("Access-Control-Allow-Origin", "*"); - response.header("Access-Control-Allow-Methods", "GET"); return response.body(); } }; @@ -63,24 +49,19 @@ public Object handle(Request request, Response response) { this.routeUploadPost = new Route() { @Override public Object handle(Request request, Response response) { - response.header("Access-Control-Allow-Origin", "*"); mongo.deleteExpired(); try { if(mongo.checkToken(request.headers("Authorization").substring("Bearer ".length()))) { - response.status(200); - JSONObject json = new JSONObject(request.body()); - String id = mongo.putPost(json.getString("dateString"), json.getJSONArray("images"), json.getString("description")); - response.body("{\"uploadedID\": \"" + id + "\"}"); + response = mongo.putPost(json.getString("dateString"), json.getJSONArray("images"), json.getString("description")).asSparkResponse(response); } else { - response.status(401); - response.body("{\"error\": \"401: You do not have authorization to view this information.\"}"); + response = httputils.Response.unauthorizedError().asSparkResponse(response); } } catch(Exception e) { e.printStackTrace(); - response.body("{\"error\": \"" + e.getMessage() + "\"}"); + httputils.Response.defaultServerError(e); } return response.body(); } @@ -89,12 +70,11 @@ public Object handle(Request request, Response response) { this.routeOptions = new Route() { @Override public Object handle(Request request, Response response) { - response.header("Access-Control-Allow-Origin", "*"); - response.header("Access-Control-Allow-Methods", "*"); - response.header("Access-Control-Allow-Headers", "Authorization"); - response.header("Access-Control-Allow-Credentials", "true"); - response.status(200); - return ""; + response = new httputils.Response().withCode(200) + .withAllowAllMethodsHeader() + .withHeader("Access-Control-Allow-Headers", "Authorization") + .withHeader("Access-Control-Allow-Credentials", "true").asSparkResponse(response); + return response.body(); } }; // Checks whether the given encrypted username and password correspond with a user in the database. @@ -103,24 +83,24 @@ public Object handle(Request request, Response response) { @Override public Object handle(Request request, Response response) { mongo.deleteExpired(); - response.header("Access-Control-Allow-Origin", "*"); try { String creds = auth.decrypt(request.headers("authorization").substring("Basic ".length())); String uname = creds.split(":")[0]; String pword = creds.split(":")[1]; if(mongo.checkCredentials(uname, pword) || mongo.checkCredentials(uname, pword)) { - response.status(200); - return "{\"token\": \"" + mongo.createToken(uname) + "\"}"; + response = mongo.createToken(uname).asSparkResponse(response); } else { - response.status(401); - return "{\"error\": \"401: Wrong username or password.\"}"; + response = new httputils.Response().withCode(401).withAllowGetMethodHeader() + .withBody("{\"error\": \"Username and/or password are incorrect.\"}") + .asSparkResponse(response); } + return response.body(); } catch(Exception e) { e.printStackTrace(); - response.status(500); - return "{\"error\": \"" + e.getMessage() + "\"}"; + response = httputils.Response.defaultServerError(e).asSparkResponse(response); + return response.body(); } } }; @@ -129,21 +109,22 @@ public Object handle(Request request, Response response) { @Override public Object handle(Request request, Response response) { mongo.deleteExpired(); - response.header("Access-Control-Allow-Origin", "*"); try { if(mongo.checkToken(request.headers("Authorization").substring("Bearer ".length()))) { - response.status(200); - return "{\"status\": \"ok\"}"; + response = new httputils.Response().withCode(200).asSparkResponse(response); + return response.body(); } else { - response.status(401); - return "{\"error\": \"401: Wrong username or password.\"}"; + response = httputils.Response.unauthorizedError() + .withBody("{\"error\": \"Your session is invalid. Please log in again.\"}") + .asSparkResponse(response); + return response.body(); } } catch(Exception e) { e.printStackTrace(); - response.status(500); - return "{\"error\": \"" + e.getMessage() + "\"}"; + response = httputils.Response.defaultServerError(e).asSparkResponse(response); + return response.body(); } } }; diff --git a/src/main/java/httputils/Response.java b/src/main/java/httputils/Response.java new file mode 100644 index 0000000..063eabe --- /dev/null +++ b/src/main/java/httputils/Response.java @@ -0,0 +1,116 @@ +package httputils; +import java.util.Hashtable; + +public class Response { + private int code; + private String body; + private Hashtable headers; + + /** + * Creates a default response with an empty body, no headers, and a code 500. + * The default code being 500 is intended to not only make error handling easier, + * but also to ensure that HTTP codes are being set and used with intention rather + * than ignoring or being lazy with them. + */ + public Response() { + this(500, "", new Hashtable<>()); + this.headers.put("Access-Control-Allow-Origin", "*"); + } + + public Response(int code, String body, Hashtable headers) { + this.code = code; + this.body = body; + this.headers = headers; + } + + public Response withCode(int code) { + this.code = code; + return this; + } + + public Response withBody(String body) { + this.body = body; + return this; + } + + public Response withHeaders(Hashtable headers) { + this.headers = headers; + return this; + } + + public Response withHeader(String key, Object value) { + this.headers.put(key, value); + return this; + } + + public Response withAllowGetMethodHeader() { + this.headers.put("Access-Control-Allow-Methods", "GET"); + return this; + } + + public Response withAllowPostMethodHeader() { + this.headers.put("Access-Control-Allow-Methods", "POST"); + return this; + } + + public Response withAllowAllMethodsHeader() { + this.headers.put("Access-Control-Allow-Methods", "*"); + return this; + } + + public void setCode(int code) { + this.code = code; + } + + public void setBody(String body) { + this.body = body; + } + + public void setHeaders(Hashtable headers) { + this.headers = headers; + } + + public void setHeader(String key, Object value) { + this.headers.put(key, value); + } + + public int getCode() { + return this.code; + } + + public String getBody() { + return this.body; + } + + public Hashtable getHeaders() { + return this.headers; + } + + public Object getHeader(String key) { + return this.headers.get(key); + } + + /** + * Converts this object into a spark response object. + * @param responseShell The initial spark response object to modify. This + * is needed as the new Response() constructor is not + * visible. + * @return + */ + public spark.Response asSparkResponse(spark.Response responseShell) { + responseShell.status(this.code); + headers.forEach((key, value) -> { + responseShell.header(key, value.toString()); + }); + responseShell.body(body); + return responseShell; + } + + public static Response defaultServerError(Exception e) { + return new Response().withBody("{\"error\": " + e.getMessage() + "}"); + } + + public static Response unauthorizedError() { + return new Response().withCode(401).withBody("{\"error\": \"401: You do not have authorization to view or edit this information.\"}"); + } +} From 46151c3b5a36efbd50877783b9cc99fed8f9844d Mon Sep 17 00:00:00 2001 From: Hazel Belmont Date: Mon, 29 Jul 2024 23:34:23 -0400 Subject: [PATCH 2/6] change URLs to match REST standard of non-action-based URLs --- src/main/java/Main.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 1a64b9b..3271f3e 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -14,9 +14,9 @@ public static void main(String[] args) { Spark.get("/db/posts/:date", routes.routeDate); Spark.get("/db/posts", routes.routeAll); Spark.get("/db/images/:oidString", routes.routeImage); - Spark.post("/db/upload/images", routes.routeUploadImage); - Spark.options("/db/upload/images", routes.routeOptions); - Spark.post("/db/upload/posts", routes.routeUploadPost); - Spark.options("/db/upload/posts", routes.routeOptions); + Spark.post("/db/images", routes.routeUploadImage); + Spark.options("/db/images", routes.routeOptions); + Spark.post("/db/posts", routes.routeUploadPost); + Spark.options("/db/posts", routes.routeOptions); } } \ No newline at end of file From a491113d47f7a6c547750a2478781aeca3704fb9 Mon Sep 17 00:00:00 2001 From: Hazel Belmont Date: Tue, 30 Jul 2024 00:04:33 -0400 Subject: [PATCH 3/6] add javadoc to Response --- src/main/java/Routes.java | 2 +- src/main/java/httputils/Response.java | 80 +++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/main/java/Routes.java b/src/main/java/Routes.java index a1eb16f..6cdfca6 100644 --- a/src/main/java/Routes.java +++ b/src/main/java/Routes.java @@ -91,7 +91,7 @@ public Object handle(Request request, Response response) { response = mongo.createToken(uname).asSparkResponse(response); } else { - response = new httputils.Response().withCode(401).withAllowGetMethodHeader() + response = httputils.Response.unauthorizedError() .withBody("{\"error\": \"Username and/or password are incorrect.\"}") .asSparkResponse(response); } diff --git a/src/main/java/httputils/Response.java b/src/main/java/httputils/Response.java index 063eabe..6d67ff5 100644 --- a/src/main/java/httputils/Response.java +++ b/src/main/java/httputils/Response.java @@ -17,59 +17,122 @@ public Response() { this.headers.put("Access-Control-Allow-Origin", "*"); } + /** + * The full constructor for a response with a code, body, and headers. + *

+ * Note that the desired structure in this codebase is a fluent interface architecture, + * so the no-parameter constructor is preferred heavily. + * For the same reason, there are no other options for constructors with different parameters. + * @param code an HTTP response code (ex. 200 or 401) + * @param body the body of the HTTP response, preferably as a String containing JSON. + * @param headers a hashtable containing key-value pairs of HTTP headers. + */ public Response(int code, String body, Hashtable headers) { this.code = code; this.body = body; this.headers = headers; } + /** + * Sets the response code to the given response code and returns this. + * @param code the desired response code. + * @return the Response object with the new response code. + */ public Response withCode(int code) { this.code = code; return this; } + /** + * Sets the body to the given String and returns this. + * @param code the desired body (preferrably as a String containing JSON). + * @return the Response object with the new body. + */ public Response withBody(String body) { this.body = body; return this; } + /** + * Sets the headers table to the given hashtable and returns this. + * This hashtable should contain only key-value pairs of HTTP headers. + * @param code the desired headers table. + * @return the Response object with the new headers. + */ public Response withHeaders(Hashtable headers) { this.headers = headers; return this; } + /** + * Sets the specified header in the headers table to the given value + * and returns this. + * @param key the name of the header to add or modify. + * @param value the new value of the header. + * @return the Response object with the new header. + */ public Response withHeader(String key, Object value) { this.headers.put(key, value); return this; } + /** + * Adds the Access-Control-Allow-Methods header to the headers table with + * the value "GET", then returns this. + * @return the Response object with the Access-Control-Allow-Methods header set to "GET". + */ public Response withAllowGetMethodHeader() { this.headers.put("Access-Control-Allow-Methods", "GET"); return this; } + /** + * Adds the Access-Control-Allow-Methods header to the headers table with + * the value "POST", then returns this. + * @return the Response object with the Access-Control-Allow-Methods header set to "POST". + */ public Response withAllowPostMethodHeader() { this.headers.put("Access-Control-Allow-Methods", "POST"); return this; } + /** + * Adds the Access-Control-Allow-Methods header to the headers table with + * the value "*", then returns this. + * @return the Response object with the Access-Control-Allow-Methods header set to "*". + */ public Response withAllowAllMethodsHeader() { this.headers.put("Access-Control-Allow-Methods", "*"); return this; } + /** + * @param code the new code for this Response to use. + */ public void setCode(int code) { this.code = code; } + /** + * @param body the new body for this Response to use, preferrably as + * a String containing JSON. + */ public void setBody(String body) { this.body = body; } + /** + * @param headers a hashtable containing only key-value pairs of HTTP headers. + */ public void setHeaders(Hashtable headers) { this.headers = headers; } + /** + * Sets the header to the desired value. + * @param key the name of the HTTP header. + * @param value the new value of the HTTP header. + */ public void setHeader(String key, Object value) { this.headers.put(key, value); } @@ -92,10 +155,11 @@ public Object getHeader(String key) { /** * Converts this object into a spark response object. - * @param responseShell The initial spark response object to modify. This - * is needed as the new Response() constructor is not - * visible. - * @return + * See {@link spark.Response}. + * @param responseShell A spark response object to modify. This is needed + * because the new spark.Response() + * constructor is not visible. + * @return the {@link spark.Response} object with all of the data from this object. */ public spark.Response asSparkResponse(spark.Response responseShell) { responseShell.status(this.code); @@ -106,10 +170,18 @@ public spark.Response asSparkResponse(spark.Response responseShell) { return responseShell; } + /** + * @param e the exception that caused the 500 error. + * @return a generic 500 error with the Java exception message as the body. + */ public static Response defaultServerError(Exception e) { return new Response().withBody("{\"error\": " + e.getMessage() + "}"); } + /** + * @return a generic 401 unauthorized error (user does not have a valid session + * or logged in with invalid credentials) + */ public static Response unauthorizedError() { return new Response().withCode(401).withBody("{\"error\": \"401: You do not have authorization to view or edit this information.\"}"); } From dfc043c1c35e5bd5bdfef7f2b4d67280ef8e3841 Mon Sep 17 00:00:00 2001 From: Hazel Belmont Date: Tue, 30 Jul 2024 00:30:13 -0400 Subject: [PATCH 4/6] remove /db from all routes - too redundant, add endpoints endpoint, change port to 5002 --- src/main/java/Main.java | 25 +++++++++++++------------ src/main/java/Routes.java | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 3271f3e..85300c9 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -6,17 +6,18 @@ public class Main { private static Routes routes = new Routes(mongo, crypto); public static void main(String[] args) { - Spark.port(5003); - Spark.get("/db/auths", routes.routeAuth); - Spark.options("/db/auths", routes.routeOptions); - Spark.get("/db/auths/check", routes.routeAuthCheck); - Spark.options("/db/auths/check", routes.routeOptions); - Spark.get("/db/posts/:date", routes.routeDate); - Spark.get("/db/posts", routes.routeAll); - Spark.get("/db/images/:oidString", routes.routeImage); - Spark.post("/db/images", routes.routeUploadImage); - Spark.options("/db/images", routes.routeOptions); - Spark.post("/db/posts", routes.routeUploadPost); - Spark.options("/db/posts", routes.routeOptions); + Spark.port(5002); + Spark.get("/auths", routes.routeAuth); + Spark.options("/auths", routes.routeOptions); + Spark.get("/auths/check", routes.routeAuthCheck); + Spark.options("/auths/check", routes.routeOptions); + Spark.get("/posts/:date", routes.routeDate); + Spark.get("/posts", routes.routeAll); + Spark.get("/images/:oidString", routes.routeImage); + Spark.post("/images", routes.routeUploadImage); + Spark.options("/images", routes.routeOptions); + Spark.post("/posts", routes.routeUploadPost); + Spark.options("/posts", routes.routeOptions); + Spark.get("/endpoints", routes.routeEndpoints); } } \ No newline at end of file diff --git a/src/main/java/Routes.java b/src/main/java/Routes.java index 6cdfca6..e2f5f87 100644 --- a/src/main/java/Routes.java +++ b/src/main/java/Routes.java @@ -1,11 +1,14 @@ +import org.json.JSONArray; import org.json.JSONObject; import spark.Request; import spark.Response; import spark.Route; +import spark.Spark; +import spark.routematch.RouteMatch; public class Routes { - public final Route routeDate, routeAll, routeImage, routeUploadImage, routeUploadPost, routeOptions, routeAuth, routeAuthCheck; + public final Route routeDate, routeAll, routeImage, routeUploadImage, routeUploadPost, routeOptions, routeAuth, routeAuthCheck, routeEndpoints; public Routes(Mongo mongo, Crypto auth) { // Returns the post that corresponds with the given date. this.routeDate = new Route() { @@ -128,5 +131,15 @@ public Object handle(Request request, Response response) { } } }; + this.routeEndpoints = new Route() { + public Object handle(Request request, Response response) { + JSONArray json = new JSONArray(); + for(RouteMatch route : Spark.routes()) { + json.put(new JSONObject().put("url", route.getMatchUri()).put("method", route.getHttpMethod())); + } + response = new httputils.Response().withCode(200).withAllowGetMethodHeader().withBody(json.toString()).asSparkResponse(response); + return response.body(); + } + }; } } From f5d3adc72ef1477e8365bcbb9f79dd966e0c94f9 Mon Sep 17 00:00:00 2001 From: Hazel Belmont Date: Wed, 31 Jul 2024 10:26:41 -0400 Subject: [PATCH 5/6] change name of asSparkResponse to dumpToSparkResponse and add new preset header methods, change with methods to use setters --- src/main/java/Routes.java | 36 ++++++++-------- src/main/java/httputils/Response.java | 60 +++++++++++++++++++++------ 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/main/java/Routes.java b/src/main/java/Routes.java index e2f5f87..5963e8e 100644 --- a/src/main/java/Routes.java +++ b/src/main/java/Routes.java @@ -14,7 +14,7 @@ public Routes(Mongo mongo, Crypto auth) { this.routeDate = new Route() { @Override public Object handle(Request request, Response response) { - response = mongo.getPost(request.params("date")).asSparkResponse(response); + mongo.getPost(request.params("date")).dumpToSparkResponse(response); return response.body(); } }; @@ -22,7 +22,7 @@ public Object handle(Request request, Response response) { this.routeAll = new Route() { @Override public Object handle(Request request, Response response) { - response = mongo.getAllPosts().asSparkResponse(response); + mongo.getAllPosts().dumpToSparkResponse(response); return response.body(); } }; @@ -30,7 +30,7 @@ public Object handle(Request request, Response response) { this.routeImage = new Route() { @Override public Object handle(Request request, Response response) { - response = mongo.getImage(request.params("oidString")).asSparkResponse(response); + mongo.getImage(request.params("oidString")).dumpToSparkResponse(response); return response.body(); } }; @@ -40,10 +40,10 @@ public Object handle(Request request, Response response) { public Object handle(Request request, Response response) { if(mongo.checkToken(request.headers("Authorization").substring("Bearer ".length()))) { JSONObject json = new JSONObject(request.body()); - response = mongo.postImage(json.get("data").toString(), json.get("fileName").toString(), json.getBoolean("featured")).asSparkResponse(response); + mongo.postImage(json.get("data").toString(), json.get("fileName").toString(), json.getBoolean("featured")).dumpToSparkResponse(response); } else { - response = httputils.Response.unauthorizedError().asSparkResponse(response); + httputils.Response.unauthorizedError().dumpToSparkResponse(response); } return response.body(); } @@ -56,10 +56,10 @@ public Object handle(Request request, Response response) { try { if(mongo.checkToken(request.headers("Authorization").substring("Bearer ".length()))) { JSONObject json = new JSONObject(request.body()); - response = mongo.putPost(json.getString("dateString"), json.getJSONArray("images"), json.getString("description")).asSparkResponse(response); + mongo.putPost(json.getString("dateString"), json.getJSONArray("images"), json.getString("description")).dumpToSparkResponse(response); } else { - response = httputils.Response.unauthorizedError().asSparkResponse(response); + httputils.Response.unauthorizedError().dumpToSparkResponse(response); } } catch(Exception e) { @@ -73,10 +73,10 @@ public Object handle(Request request, Response response) { this.routeOptions = new Route() { @Override public Object handle(Request request, Response response) { - response = new httputils.Response().withCode(200) + new httputils.Response().withCode(200) .withAllowAllMethodsHeader() .withHeader("Access-Control-Allow-Headers", "Authorization") - .withHeader("Access-Control-Allow-Credentials", "true").asSparkResponse(response); + .withHeader("Access-Control-Allow-Credentials", "true").dumpToSparkResponse(response); return response.body(); } }; @@ -91,18 +91,18 @@ public Object handle(Request request, Response response) { String uname = creds.split(":")[0]; String pword = creds.split(":")[1]; if(mongo.checkCredentials(uname, pword) || mongo.checkCredentials(uname, pword)) { - response = mongo.createToken(uname).asSparkResponse(response); + mongo.createToken(uname).dumpToSparkResponse(response); } else { - response = httputils.Response.unauthorizedError() + httputils.Response.unauthorizedError() .withBody("{\"error\": \"Username and/or password are incorrect.\"}") - .asSparkResponse(response); + .dumpToSparkResponse(response); } return response.body(); } catch(Exception e) { e.printStackTrace(); - response = httputils.Response.defaultServerError(e).asSparkResponse(response); + httputils.Response.defaultServerError(e).dumpToSparkResponse(response); return response.body(); } } @@ -114,19 +114,19 @@ public Object handle(Request request, Response response) { mongo.deleteExpired(); try { if(mongo.checkToken(request.headers("Authorization").substring("Bearer ".length()))) { - response = new httputils.Response().withCode(200).asSparkResponse(response); + new httputils.Response().withCode(200).dumpToSparkResponse(response); return response.body(); } else { - response = httputils.Response.unauthorizedError() + httputils.Response.unauthorizedError() .withBody("{\"error\": \"Your session is invalid. Please log in again.\"}") - .asSparkResponse(response); + .dumpToSparkResponse(response); return response.body(); } } catch(Exception e) { e.printStackTrace(); - response = httputils.Response.defaultServerError(e).asSparkResponse(response); + httputils.Response.defaultServerError(e).dumpToSparkResponse(response); return response.body(); } } @@ -137,7 +137,7 @@ public Object handle(Request request, Response response) { for(RouteMatch route : Spark.routes()) { json.put(new JSONObject().put("url", route.getMatchUri()).put("method", route.getHttpMethod())); } - response = new httputils.Response().withCode(200).withAllowGetMethodHeader().withBody(json.toString()).asSparkResponse(response); + new httputils.Response().withCode(200).withAllowGetMethodHeader().withBody(json.toString()).dumpToSparkResponse(response); return response.body(); } }; diff --git a/src/main/java/httputils/Response.java b/src/main/java/httputils/Response.java index 6d67ff5..ee3eb42 100644 --- a/src/main/java/httputils/Response.java +++ b/src/main/java/httputils/Response.java @@ -1,6 +1,8 @@ package httputils; import java.util.Hashtable; +import org.json.JSONObject; + public class Response { private int code; private String body; @@ -14,7 +16,6 @@ public class Response { */ public Response() { this(500, "", new Hashtable<>()); - this.headers.put("Access-Control-Allow-Origin", "*"); } /** @@ -31,6 +32,7 @@ public Response(int code, String body, Hashtable headers) { this.code = code; this.body = body; this.headers = headers; + this.withAllowOriginAllHeader().withContentTypeJSONHeader(); } /** @@ -39,28 +41,39 @@ public Response(int code, String body, Hashtable headers) { * @return the Response object with the new response code. */ public Response withCode(int code) { - this.code = code; + this.setCode(code); return this; } /** * Sets the body to the given String and returns this. - * @param code the desired body (preferrably as a String containing JSON). + * @param body the desired body (preferrably as a String containing JSON). * @return the Response object with the new body. */ public Response withBody(String body) { - this.body = body; + this.setBody(body); + return this; + } + + /** + * Sets the body to the string representation of the given JSON and returns + * this. + * @param code the desired body as a JSONObject + * @return the Response object with the new body. + */ + public Response withBody(JSONObject body) { + this.setBody(body.toString()); return this; } /** * Sets the headers table to the given hashtable and returns this. * This hashtable should contain only key-value pairs of HTTP headers. - * @param code the desired headers table. + * @param headers the desired headers table. * @return the Response object with the new headers. */ public Response withHeaders(Hashtable headers) { - this.headers = headers; + this.setHeaders(headers); return this; } @@ -72,7 +85,7 @@ public Response withHeaders(Hashtable headers) { * @return the Response object with the new header. */ public Response withHeader(String key, Object value) { - this.headers.put(key, value); + this.setHeader(key, value); return this; } @@ -82,7 +95,7 @@ public Response withHeader(String key, Object value) { * @return the Response object with the Access-Control-Allow-Methods header set to "GET". */ public Response withAllowGetMethodHeader() { - this.headers.put("Access-Control-Allow-Methods", "GET"); + this.setHeader("Access-Control-Allow-Methods", "GET"); return this; } @@ -92,7 +105,7 @@ public Response withAllowGetMethodHeader() { * @return the Response object with the Access-Control-Allow-Methods header set to "POST". */ public Response withAllowPostMethodHeader() { - this.headers.put("Access-Control-Allow-Methods", "POST"); + this.setHeader("Access-Control-Allow-Methods", "POST"); return this; } @@ -102,7 +115,27 @@ public Response withAllowPostMethodHeader() { * @return the Response object with the Access-Control-Allow-Methods header set to "*". */ public Response withAllowAllMethodsHeader() { - this.headers.put("Access-Control-Allow-Methods", "*"); + this.setHeader("Access-Control-Allow-Methods", "*"); + return this; + } + + /** + * Adds the Access-Control-Allow-Origin header to the headers table with + * the value "*", then returns this. + * @return the Response object with the Access-Control-Allow-Origin header set to "*" + */ + public Response withAllowOriginAllHeader() { + this.setHeader("Access-Control-Allow-Origin", "*"); + return this; + } + + /** + * Adds the Content-Type header to the headers table with the value + * "application/json", then returns this. + * @return the Response object with the Content-Type header set to "application/json". + */ + public Response withContentTypeJSONHeader() { + this.setHeader("Content-Type", "application/json"); return this; } @@ -154,14 +187,15 @@ public Object getHeader(String key) { } /** - * Converts this object into a spark response object. - * See {@link spark.Response}. + * Copies the body, headers, and response code to the given Spark response. + * The responseShell is modified by this method, so the return value is not actually + * necessary, but it is returned for your convenience. * @param responseShell A spark response object to modify. This is needed * because the new spark.Response() * constructor is not visible. * @return the {@link spark.Response} object with all of the data from this object. */ - public spark.Response asSparkResponse(spark.Response responseShell) { + public spark.Response dumpToSparkResponse(spark.Response responseShell) { responseShell.status(this.code); headers.forEach((key, value) -> { responseShell.header(key, value.toString()); From a0bbf91b205f90d34dc9746e62a68127a8d50bbf Mon Sep 17 00:00:00 2001 From: Hazel Belmont Date: Thu, 24 Oct 2024 20:10:39 -0400 Subject: [PATCH 6/6] change javadoc to reflect new Response return values, add JSONObject to Responses --- src/main/java/Mongo.java | 16 +++++------ src/main/java/Routes.java | 6 ++-- src/main/java/httputils/Response.java | 40 +++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/main/java/Mongo.java b/src/main/java/Mongo.java index 9c560e1..6905a9b 100644 --- a/src/main/java/Mongo.java +++ b/src/main/java/Mongo.java @@ -54,7 +54,7 @@ public Mongo(Crypto crypto) { /** * Gets all documents from the posts collection and puts them in an ArrayList. - * @return an ArrayList of JSON strings from MongoDB + * @return a new Respobse object containing all posts as JSON in the body. */ public Response getAllPosts() { try { @@ -62,7 +62,6 @@ public Response getAllPosts() { ArrayList docsJSON = new ArrayList(); docs.forEach(doc -> docsJSON.add(doc.toJson())); - // TODO: update javadoc to reflect new return value return new Response().withCode(200).withAllowGetMethodHeader().withBody("{\"posts\": " + docsJSON.toString() + "}"); } catch(Exception e) { @@ -74,7 +73,8 @@ public Response getAllPosts() { /** * Gets a post that was posted on the given datestring * @param dateString The datestring to search for formatted MMDDYY, i.e. 092123 - * @return The JSON string returned from the MongoDB posts collection + * @return a new Response object where the body is the JSON string returned + * from the MongoDB posts collection */ public Response getPost(String dateString) { try { @@ -84,7 +84,6 @@ public Response getPost(String dateString) { return new Response().withCode(400).withAllowGetMethodHeader().withBody("{\"error\": \"A post with the dateString " + dateString + " could not be found.\"}"); } - // TODO: update javadoc to reflect new return value return new Response().withCode(200).withAllowGetMethodHeader().withBody(doc.toJson()); } catch(Exception e) { @@ -96,7 +95,8 @@ public Response getPost(String dateString) { /** * Gets an image in base64 from the GridFS bucket stored in the MongoDB database * @param oid the hex string _id of the image to get - * @return a base64 encoded string image (WITH leading data URL format, i.e. "data:image/[format];base64,") + * @return a new Response object with a base64 encoded string image (WITH leading data URL format, + * i.e. "data:image/[format];base64,") as the body */ public Response getImage(String oid) { try { @@ -125,7 +125,7 @@ public Response getImage(String oid) { * Uploads an image from a base64 string with a given filename to the GridFS bucket stored in the MongoDB database * @param imageb64 the base64 encoded image (WITHOUT leading data URL format, i.e. "data:image/[format];base64,") * @param fileName the filename to associate with the file, i.e. "file.jpeg" - * @return the hex string _id of the inserted image + * @return a new Response object with the hex string _id of the inserted image as the body */ public Response postImage(String imageb64, String fileName, Boolean featured) { try { @@ -159,7 +159,7 @@ public Response postImage(String imageb64, String fileName, Boolean featured) { * @param dateString a datestring formatted mmddyy, i.e. 092123 for September 21, 2023 * @param imageIDs a JSON array of hex string _ids, i.e. ["65128e7bf44ec02f9eac0f66", "65128e7bf44ec02f9eac0f67", "65128e7bf44ec02f9eac0f68"] * @param description a description of the day's meeting - * @return the hex string _id of the inserted post document + * @return a new Response object with the hex string _id of the inserted post document as the body */ public Response putPost(String dateString, JSONArray imageIDs, String description) { try { @@ -213,7 +213,7 @@ public boolean checkCredentials(String uname, String pword) { /** * Creates a random eight-character string that will serve as an access token for verified users. - * @return the random eight-character access token. + * @return a Response object containing the random eight-character access token as the body. */ public Response createToken(String username) { Random generator = new Random(); diff --git a/src/main/java/Routes.java b/src/main/java/Routes.java index 5963e8e..bc0a711 100644 --- a/src/main/java/Routes.java +++ b/src/main/java/Routes.java @@ -95,7 +95,7 @@ public Object handle(Request request, Response response) { } else { httputils.Response.unauthorizedError() - .withBody("{\"error\": \"Username and/or password are incorrect.\"}") + .withBody(new JSONObject().put("error", "Username and/or password are incorrect.")) .dumpToSparkResponse(response); } return response.body(); @@ -119,7 +119,7 @@ public Object handle(Request request, Response response) { } else { httputils.Response.unauthorizedError() - .withBody("{\"error\": \"Your session is invalid. Please log in again.\"}") + .withBody(new JSONObject().put("error", "Your session is invalid. Please log in again.")) .dumpToSparkResponse(response); return response.body(); } @@ -137,7 +137,7 @@ public Object handle(Request request, Response response) { for(RouteMatch route : Spark.routes()) { json.put(new JSONObject().put("url", route.getMatchUri()).put("method", route.getHttpMethod())); } - new httputils.Response().withCode(200).withAllowGetMethodHeader().withBody(json.toString()).dumpToSparkResponse(response); + new httputils.Response().withCode(200).withAllowGetMethodHeader().withBody(json).dumpToSparkResponse(response); return response.body(); } }; diff --git a/src/main/java/httputils/Response.java b/src/main/java/httputils/Response.java index ee3eb42..4bae3e6 100644 --- a/src/main/java/httputils/Response.java +++ b/src/main/java/httputils/Response.java @@ -1,6 +1,7 @@ package httputils; import java.util.Hashtable; +import org.json.JSONArray; import org.json.JSONObject; public class Response { @@ -66,6 +67,41 @@ public Response withBody(JSONObject body) { return this; } + /** + * Sets the body to the string representation of the given JSON and returns + * this. + * @param code the desired body as a JSONArray + * @return the Response object with the new body. + */ + public Response withBody(JSONArray body) { + this.setBody(body.toString()); + return this; + } + + /** + * Sets the body to json containing error text. + * @param userFriendlyErrorMessage + * @param logMessage + * @return + */ + public Response withErrorBody(String userFriendlyErrorMessage, String logMessage) { + this.setBody(new JSONObject() + .put("error", userFriendlyErrorMessage) + .put("fullError", logMessage) + .toString() + ); + return this; + } + + public Response withErrorBody(String userFriendlyErrorMessage) { + this.setBody(new JSONObject() + .put("error", userFriendlyErrorMessage) + .put("fullError", "") + .toString() + ); + return this; + } + /** * Sets the headers table to the given hashtable and returns this. * This hashtable should contain only key-value pairs of HTTP headers. @@ -209,7 +245,7 @@ public spark.Response dumpToSparkResponse(spark.Response responseShell) { * @return a generic 500 error with the Java exception message as the body. */ public static Response defaultServerError(Exception e) { - return new Response().withBody("{\"error\": " + e.getMessage() + "}"); + return new Response().withBody(new JSONObject().put("error", e.getMessage())); } /** @@ -217,6 +253,6 @@ public static Response defaultServerError(Exception e) { * or logged in with invalid credentials) */ public static Response unauthorizedError() { - return new Response().withCode(401).withBody("{\"error\": \"401: You do not have authorization to view or edit this information.\"}"); + return new Response().withCode(401).withBody(new JSONObject().put("error", "401: You do not have authorization to view or edit this information.")); } }