Make required updates to App.java

Before proceeding, if you haven't already, you should git add and commit your Database.java and pom.xml.

  • At this point it should represent a nice increment of work.
  • You should have been coding iteratively and incrementally, being sure to save/compile/run/test as you go...
  • ... so at this point you should have code that is in compilable, working state.

You will find your admin-cli's App.java a valuable reference: admin:admin-cli/src/main/java/edu/lehigh/cse216/sml3/admin/App.java.

It had a simpleManualTests method, which we can use as a reference while updating "mock" calls with calls that hit the database:

/** * Reads arguments from the environment and then uses those * arguments to connect to the database. Either DATABASE_URI should be set, * or the values of four other variables POSTGRES_{IP, PORT, USER, PASS, DBNAME}. */ public static void simpleManualTests( String[] argv ){ /* holds connection to the database created from environment variables */ Database db = Database.getDatabase(); db.dropTable(); db.createTable(); db.insertRow("test subject", "test message"); db.updateOne(1, "updated test message"); ArrayList<Database.RowData> list_rd = db.selectAll(); System.out.println( "Row data:" ); for( Database.RowData rd : list_rd ) System.out.println( ">\t" + rd ); Database.RowData single_rd = db.selectOne(1); System.out.println( "Single row: " + single_rd ); db.deleteRow(1); if( db != null ) db.disconnect(); }

Now, let's make the remainder of the required changes to our backend's App.java that allow us to use Database.java.

Some design decisions may be made but, when finished, your backend should successfully "hit the db".

That is, when you are done with your changes your backend application as defined in backend-webserver:backend/src/.../App.java should be able to

  1. connect to the database specified by environment variables
  2. perform read/write/update/delete operations on the database through the use of prepared statements
  3. respond to RESTful HTTP requests made on routes that invoked those database operations

Among the biggest changes to make:

  • It should now use RowData instead of MockDataRow (we are no longer using a mock database to store application data)
    • In essence, a short-term fix is to find all uses of MockDataRow class, and replace them with references to RowData
    • Update the calls to the mock datastore methods to instead be appropriate calls to the real database methods
  • The backend should already be reading its database connection information from environment variables, as this functionality was defined in Database.java
    • We should update the javadoc of our App.java to reflect this.

Note

Depending on how you organized your code, you may be editing one of many different files.

I'm not going to guess how you organized your code, instead I'll leave it up to you to figure out where the critical blocks of code need to go, and how to use them.

Also, be mindful of whether, where, and when you disconnect from your database.

Warning

If you tried to disconnect from the database at the end of your backend's App.main(), it will close the db connection and you'll wonder why you keep getting sql exceptions.

Therefore, you will likely not want to issue a db.disconnect() at the end of your main().

Before changing your backend's App.main, you might instead wish to "rename it away" so you have it as a direct reference, then you can build up a new main as you go (you might already have a main_helloworld).

Call your old main something like main_inMemory_datastore, and make a new main that lets you call your other mains.

After doing so, you might have something a little like...

package edu.lehigh.cse216.sml3.backend; // Javalin package for creating HTTP GET, PUT, POST, etc routes import io.javalin.Javalin; // Import Google's JSON library import com.google.gson.*; /** * A Javalin-based HTTP server. There are different mains that demonstrate different functionality. */ public class App { /** Not particularly elegant, but we can activate different mains by commenting/uncommenting */ public static void main( String[] args ){ // main_helloworld(args); // main_inMemory_datastore(args); main_uses_database(args); } /** * Makes a simple webserver that responds to a GET at /hello * Uses default port; customizes the logger. * * When an HTTP client connects to this server on the default Javalin port (8080), * and requests /hello, we return "Hello World". Otherwise, we produce an error. */ public static void main_helloworld( String[] args ){ // // the below line needs java 10 for `var`, and imo isn't very readable // var app = Javalin // .create(/*config*/) // .get("/hello", ctx -> ctx.result("Hello World")) // .start(7070); // // // so we instead avoid use of `var` but otherwise do the same Javalin app = Javalin .create( config -> { config.requestLogger.http( (ctx, ms) -> { System.out.printf( "%s%n", "=".repeat(42) ); System.out.printf( "%s\t%s\t%s%nfull url: %s%n", ctx.scheme(), ctx.method().name(), ctx.path(), ctx.fullUrl() ); } ); } ).get( "/hello", ctx -> ctx.result("Hello World") ) .start( /*default is 8080*/ ); } /** * A simple webserver that uses an IN-MEMORY datastore. * Uses default port; customizes the logger. * * Supports GET to /messages to retrieve all messages (without their body) * Supports POST to /messages to create a new message * Supports {GET, PUT, DELETE} to /messages/{id} to do associated action on message with given id */ public static void main_inMemory_datastore( String[] args ){ // our javalin app on which most operations must be performed Javalin app = Javalin .create( config -> { config.requestLogger.http( (ctx, ms) -> { System.out.printf( "%s%n", "=".repeat(42) ); System.out.printf( "%s\t%s\t%s%nfull url: %s%n", ctx.scheme(), ctx.method().name(), ctx.path(), ctx.fullUrl() ); } ); } ); // gson provides us a way to turn JSON into objects, and objects into JSON. // // NB: it must be final, so that it can be accessed from our lambdas // // NB: Gson is thread-safe. See // https://stackoverflow.com/questions/10380835/is-it-ok-to-use-gson-instance-as-a-static-field-in-a-model-bean-reuse final Gson gson = new Gson(); // dataStore holds all of the data that has been provided via HTTP requests // // NB: every time we shut down the server, we will lose all data, and // every time we start the server, we'll have an empty dataStore, // with IDs starting over from 0. final MockDataStore dataStore = new MockDataStore(); // GET route that returns all message titles and Ids. All we do is get // the data, embed it in a StructuredResponse, turn it into JSON, and // return it. If there's no data, we return "[]", so there's no need // for error handling. app.get( "/messages", ctx -> { ctx.status( 200 ); // status 200 OK ctx.contentType( "application/json" ); // MIME type of JSON StructuredResponse resp = new StructuredResponse( "ok" , null, dataStore.readAll() ); ctx.result( gson.toJson( resp ) ); // return JSON representation of response } ); // GET route that returns everything for a single row in the MockDataStore. // The "{id}" suffix in the first parameter to get() becomes // ctx.pathParam("id"), so that we can get the requested row ID. If // "{id}" isn't a number, Javalin will reply with a status 500 Internal // Server Error. Otherwise, we have an integer, and the only possible // error is that it doesn't correspond to a row with data. app.get( "/messages/{id}", ctx -> { // NB: the {} syntax "/messages/{id}" does not allow slashes ('/') as part of the parameter // NB: the <> syntax "/messages/<id>" allows slashes ('/') as part of the parameter int idx = Integer.parseInt( ctx.pathParam("id") ); // NB: even on error, we return 200, but with a JSON object that describes the error. ctx.status( 200 ); // status 200 OK ctx.contentType( "application/json" ); // MIME type of JSON MockDataRow data = dataStore.readOne(idx); StructuredResponse resp = null; if (data == null) { // row not found, so return an error response resp = new StructuredResponse("error", "Data with row id " + idx + " not found", null); } else { // we found it, so just return the data resp = new StructuredResponse("ok", null, data); } ctx.result( gson.toJson( resp ) ); // return JSON representation of response } ); // POST route for adding a new element to the MockDataStore. This will read // JSON from the body of the request, turn it into a SimpleRequest // object, extract the title and message, insert them, and return the // ID of the newly created row. app.post("/messages", ctx -> { // NB: even on error, we return 200, but with a JSON object that describes the error. ctx.status( 200 ); // status 200 OK ctx.contentType( "application/json" ); // MIME type of JSON StructuredResponse resp = null; // get the request json from the ctx body, turn it into SimpleRequest instance // NB: if gson.Json fails, expect server reply with status 500 Internal Server Error SimpleRequest req = gson.fromJson(ctx.body(), SimpleRequest.class); // NB: add to MockDataStore; createEntry checks for null title and message int newId = dataStore.createEntry(req.mTitle(), req.mMessage()); if (newId == -1) { resp = new StructuredResponse("error", "error performing insertion (title or message null?)", null); } else { resp = new StructuredResponse("ok", Integer.toString(newId), null); } ctx.result( gson.toJson( resp ) ); // return JSON representation of response }); // PUT route for updating a row in the DataStore. This is almost exactly the same as POST app.put("/messages/{id}", ctx -> { // If we can't get an ID or can't parse the JSON, javalin sends a status 500 int idx = Integer.parseInt( ctx.pathParam("id") ); // NB: even on error, we return 200, but with a JSON object that describes the error. ctx.status( 200 ); // status 200 OK ctx.contentType( "application/json" ); // MIME type of JSON StructuredResponse resp = null; // get the request json from the ctx body, turn it into SimpleRequest instance // NB: if gson.Json fails, expect server reply with status 500 Internal Server Error SimpleRequest req = gson.fromJson(ctx.body(), SimpleRequest.class); // NB: update entry in MockDataStore; updateOne checks for null title and message and invalid ids MockDataRow result = dataStore.updateOne(idx, req.mTitle(), req.mMessage()); if (result == null) { resp = new StructuredResponse("error", "unable to update row " + idx, null); } else { resp = new StructuredResponse("ok", null, result); } ctx.result( gson.toJson( resp ) ); // return JSON representation of response }); // DELETE route for removing a row from the MockDataStore app.delete("/messages/{id}", ctx -> { // If we can't get an ID or can't parse the JSON, javalin sends a status 500 int idx = Integer.parseInt( ctx.pathParam("id") ); // NB: even on error, we return 200, but with a JSON object that describes the error. ctx.status( 200 ); // status 200 OK ctx.contentType( "application/json" ); // MIME type of JSON StructuredResponse resp = null; // NB: we won't concern ourselves too much with the quality of the // message sent on a successful delete boolean result = dataStore.deleteOne(idx); if (!result) { resp = new StructuredResponse("error", "unable to delete row " + idx, null); } else { resp = new StructuredResponse("ok", null, null); } ctx.result( gson.toJson( resp ) ); // return JSON representation of response }); // don't forget: nothing happens until we `start` the server app.start( /*default is 8080*/ ); } /** * A simple webserver that connects to and uses a database. * Uses default port; customizes the logger. * * Reads arguments from the environment and then uses those arguments to connect to the database. * Either DATABASE_URI should be set, or the values of POSTGRES_{IP, PORT, USER, PASS, DBNAME}. * * Features yet to be implemented: * Supports GET to /messages to retrieve all messages (without their body) * Supports POST to /messages to create a new message * Supports {GET, PUT, DELETE} to /messages/{id} to do associated action on message with given id */ public static void main_uses_database( String[] args){ /* holds connection to the database created from environment variables */ Database db = Database.getDatabase(); /* the server logic will go here */ if( db != null ) db.disconnect(); } }

Having "stubbed" out our new main, we can now incrementally and iteratively develop and test by filling out portions of behavior in main_uses_database one behavior at a time.

  • Had we refactored our in-memory code before starting on the database connection task, this might have been a simpler or harder effort, depending upon your design decisions.
  • This means you should (after saving and verifying it compiles) git status . and add then commit your work with a message like WIP: ready to begin connecting http routes to db calls.

With your work safely committed you might then proceed in a top-down manner.

To proceed in a top-down manner, we would next copy from main_inMemory_datastore the code that sets up Javalin, and starts it.

public static void main_uses_database( String[] args){ /* holds connection to the database created from environment variables */ Database db = Database.getDatabase(); // our javalin app on which most operations must be performed Javalin app = Javalin .create( config -> { config.requestLogger.http( (ctx, ms) -> { System.out.printf( "%s%n", "=".repeat(42) ); System.out.printf( "%s\t%s\t%s%nfull url: %s%n", ctx.scheme(), ctx.method().name(), ctx.path(), ctx.fullUrl() ); } ); } ); // gson provides us a way to turn JSON into objects, and objects into JSON. // // NB: it must be final, so that it can be accessed from our lambdas // // NB: Gson is thread-safe. See // https://stackoverflow.com/questions/10380835/is-it-ok-to-use-gson-instance-as-a-static-field-in-a-model-bean-reuse final Gson gson = new Gson(); /* ----- the server routing logic will go here ----- */ // don't forget: nothing happens until we `start` the server app.start( /*default is 8080*/ ); if( db != null ) db.disconnect(); }

It again feels like we are "programming by copy and paste", with a lot of duplicate code.

We may be violating the "DRY" principle.

This should again be a warning sign, a "code smell", that suggests we should be thinking about whether there is a cleaner design and implementation.

For now, we will abide by the "rule of three":

Rule of three ("Three strikes and you refactor") is a code refactoring rule of thumb to decide when similar pieces of code should be refactored to avoid duplication. It states that two instances of similar code do not require refactoring, but when similar code is used three times, it should be extracted into a new procedure. [...]

Duplication is considered a bad practice in programming because it makes the code harder to maintain.

When the rule encoded in a replicated piece of code changes, whoever maintains the code will have to change it in all places correctly.

However, choosing an appropriate design to avoid duplication might benefit from more examples to see patterns in. Attempting premature refactoring risks selecting a wrong abstraction, which can result in worse code as new requirements emerge and will eventually need to be refactored again.