Skip to content

Commit

Permalink
Merge pull request #80 from UMM-CSci-3601/update-server-to-match-iter…
Browse files Browse the repository at this point in the history
…ation-template

Bring in changes to the server from Lab 3 and iteration template
  • Loading branch information
NicMcPhee authored Feb 12, 2024
2 parents 575fe72 + 6786628 commit 2b53d94
Show file tree
Hide file tree
Showing 9 changed files with 556 additions and 166 deletions.
2 changes: 1 addition & 1 deletion server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ dependencies {

application {
// Define the main class for the application
mainClass = 'umm3601.Server' // 'umm3601.Main'
mainClass = 'umm3601.Main'
}

test {
Expand Down
38 changes: 38 additions & 0 deletions server/src/main/java/umm3601/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package umm3601;

import io.javalin.Javalin;

/**
* Interface for classes that can add routes to a Javalin server.
*
* Any class that implements this interface can be used to add routes to the
* server via the specified `addRoutes()` method.
*
* This is useful for organizing routes into separate files, and for testing
* routes without starting the server (except that the inability to compare
* lambdas in fact makes this very hard to test).
*
* Any controller class that provides routes for the Javalin server
* must implement this interface since the `Server` class
* is just handed an array of `Controller` objects in its constructor. This
* allows us to add routes to the server without having to modify the `Server`,
* and without having the server know about any specific controller implementations.
*
* Note that this interface definition is _complete_ and you shouldn't need to
* add anything to it. You just need to make sure that any new controllers
* you implement also implement this interface, providing their own `addRoutes()`
* method.
*/
public interface Controller {
/**
* Add routes to the server.
*
* If you have a controller that implements this interface, for example,
* your implementation of `addRoutes()` would add all the routes for your
* controller's datatype, by calling
* `server.get(...)`, `server.post(...)`, etc.
*
* @param server The Javalin server to add routes to
*/
void addRoutes(Javalin server);
}
68 changes: 68 additions & 0 deletions server/src/main/java/umm3601/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package umm3601;

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoDatabase;

import umm3601.user.UserController;

public class Main {

public static void main(String[] args) {
// Get the MongoDB address and database name from environment variables and
// if they aren't set, use the defaults of "localhost" and "dev".
String mongoAddr = Main.getEnvOrDefault("MONGO_ADDR", "localhost");
String databaseName = Main.getEnvOrDefault("MONGO_DB", "dev");

// Set up the MongoDB client
MongoClient mongoClient = Server.configureDatabase(mongoAddr);
// Get the database
MongoDatabase database = mongoClient.getDatabase(databaseName);

// The implementations of `Controller` used for the server. These will presumably
// be one or more controllers, each of which implements the `Controller` interface.
// You'll add your own controllers in `getControllers` as you create them.
final Controller[] controllers = Main.getControllers(database);

// Construct the server
Server server = new Server(mongoClient, controllers);

// Start the server
server.startServer();
}

/**
* Get the value of an environment variable, or return a default value if it's not set.
*
* @param envName The name of the environment variable to get
* @param defaultValue The default value to use if the environment variable isn't set
*
* @return The value of the environment variable, or the default value if it's not set
*/
static String getEnvOrDefault(String envName, String defaultValue) {
return System.getenv().getOrDefault(envName, defaultValue);
}

/**
* Get the implementations of `Controller` used for the server.
*
* These will presumably be one or more controllers, each of which
* implements the `Controller` interface. You'll add your own controllers
* in to the array returned by this method as you create them.
*
* @param database The MongoDB database object used by the controllers
* to access the database.
* @return An array of implementations of `Controller` for the server.
*/
static Controller[] getControllers(MongoDatabase database) {
Controller[] controllers = new Controller[] {
// You would add additional controllers here, as you create them,
// although you need to make sure that each of your new controllers implements
// the `Controller` interface.
//
// You can also remove this UserController once you don't need it.
new UserController(database)
};
return controllers;
}

}
185 changes: 136 additions & 49 deletions server/src/main/java/umm3601/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,73 +6,120 @@
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;

import org.bson.UuidRepresentation;

import io.javalin.Javalin;
import io.javalin.plugin.bundled.RouteOverviewPlugin;
import io.javalin.http.InternalServerErrorResponse;
import umm3601.user.UserController;

/**
* The class used to configure and start a Javalin server.
*/
public class Server {

// The port that the server should run on.
private static final int SERVER_PORT = 4567;

public static void main(String[] args) {

// Get the MongoDB address and database name from environment variables and
// if they aren't set, use the defaults of "localhost" and "dev".
String mongoAddr = System.getenv().getOrDefault("MONGO_ADDR", "localhost");
String databaseName = System.getenv().getOrDefault("MONGO_DB", "dev");
// The `mongoClient` field is used to access the MongoDB
private final MongoClient mongoClient;

// The `controllers` field is an array of all the `Controller` implementations
// for the server. This is used to add routes to the server.
private Controller[] controllers;

/**
* Construct a `Server` object that we'll use (via `startServer()`) to configure
* and start the server.
*
* @param mongoClient The MongoDB client object used to access to the database
* @param controllers The implementations of `Controller` used for this server
*/
public Server(MongoClient mongoClient, Controller[] controllers) {
this.mongoClient = mongoClient;
// This is what is known as a "defensive copy". We make a copy of
// the array so that if the caller modifies the array after passing
// it in, we don't have to worry about it. If we didn't do this,
// the caller could modify the array after passing it in, and then
// we'd be using the modified array without realizing it.
this.controllers = Arrays.copyOf(controllers, controllers.length);
}

/**
* Setup the MongoDB database connection.
*
* This "wires up" the database using either system environment variables
* or default values. If you're running the server locally without any environment
* variables set, this will connect to the `dev` database running on your computer
* (`localhost`). If you're running the server on Digital Ocean using our setup
* script, this will connect to the production database running on server.
*
* This sets both the `mongoClient` and `database` fields
* so they can be used when setting up the Javalin server.
* @param mongoAddr The address of the MongoDB server
*
* @return The MongoDB client object
*/
static MongoClient configureDatabase(String mongoAddr) {
// Setup the MongoDB client object with the information we set earlier
MongoClient mongoClient
= MongoClients.create(MongoClientSettings
.builder()
.applyToClusterSettings(builder -> builder.hosts(Arrays.asList(new ServerAddress(mongoAddr))))
// Old versions of the mongodb-driver-sync package encoded UUID values (universally unique identifiers) in
// a non-standard way. This option says to use the standard encoding.
// See: https://studio3t.com/knowledge-base/articles/mongodb-best-practices-uuid-data/
.uuidRepresentation(UuidRepresentation.STANDARD)
.build());

// Get the database
MongoDatabase database = mongoClient.getDatabase(databaseName);

// Initialize dependencies
UserController userController = new UserController(database);
MongoClient mongoClient = MongoClients.create(MongoClientSettings
.builder()
.applyToClusterSettings(builder -> builder.hosts(Arrays.asList(new ServerAddress(mongoAddr))))
// Old versions of the mongodb-driver-sync package encoded UUID values (universally unique identifiers) in
// a non-standard way. This option says to use the standard encoding.
// See: https://studio3t.com/knowledge-base/articles/mongodb-best-practices-uuid-data/
.uuidRepresentation(UuidRepresentation.STANDARD)
.build());

return mongoClient;
}

/**
* Configure and start the server.
*
* This configures and starts the Javalin server, which will start listening for HTTP requests.
* It also sets up the server to shut down gracefully if it's killed or if the
* JVM is shut down.
*/
void startServer() {
Javalin javalin = configureJavalin();
setupRoutes(javalin);
javalin.start(SERVER_PORT);
}

/**
* Configure the Javalin server. This includes
*
* - Adding a route overview plugin to make it easier to see what routes
* are available.
* - Setting it up to shut down gracefully if it's killed or if the
* JVM is shut down.
* - Setting up a handler for uncaught exceptions to return an HTTP 500
* error.
*
* @return The Javalin server instance
*/
private Javalin configureJavalin() {
/*
* Create a Javalin server instance. We're using the "create" method
* rather than the "start" method here because we want to set up some
* things before the server actually starts. If we used "start" it would
* start the server immediately and we wouldn't be able to do things like
* set up routes. We'll call the "start" method later to actually start
* the server.
*
* `plugins.register(new RouteOverviewPlugin("/api"))` adds
* a helpful endpoint for us to use during development. In particular
* `http://localhost:4567/api` shows all of the available endpoints and
* what HTTP methods they use. (Replace `localhost` and `4567` with whatever server
* and port you're actually using, if they are different.)
*/
Javalin server = Javalin.create(config ->
config.plugins.register(new RouteOverviewPlugin("/api"))
);
/*
* We want to shut the `mongoClient` down if the server either
* fails to start, or when it's shutting down for whatever reason.
* Since the mongClient needs to be available throughout the
* life of the server, the only way to do this is to wait for
* these events and close it then.
*/
server.events(event -> {
event.serverStartFailed(mongoClient::close);
event.serverStopped(mongoClient::close);
});
Runtime.getRuntime().addShutdownHook(new Thread(server::stop));

server.start(SERVER_PORT);

// List users, filtered using query parameters
server.get("/api/users", userController::getUsers);

// Get the specified user
server.get("/api/users/{id}", userController::getUser);

// Delete the specified user
server.delete("/api/users/{id}", userController::deleteUser);

// Add new user with the user info being in the JSON body
// of the HTTP request
server.post("/api/users", userController::addNewUser);
// Configure the MongoDB client and the Javalin server to shut down gracefully.
configureShutdowns(server);

// This catches any uncaught exceptions thrown in the server
// code and turns them into a 500 response ("Internal Server
Expand All @@ -86,5 +133,45 @@ public static void main(String[] args) {
server.exception(Exception.class, (e, ctx) -> {
throw new InternalServerErrorResponse(e.toString());
});

return server;
}

/**
* Configure the server and the MongoDB client to shut down gracefully.
*
* @param mongoClient The MongoDB client
* @param server The Javalin server instance
*/
private void configureShutdowns(Javalin server) {
/*
* We want the server to shut down gracefully if we kill it
* or if the JVM dies for some reason.
*/
Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
/*
* We want to shut the `mongoClient` down if the server either
* fails to start, or when it's shutting down for whatever reason.
* Since the mongClient needs to be available throughout the
* life of the server, the only way to do this is to wait for
* these events and close it then.
*/
server.events(event -> {
event.serverStartFailed(mongoClient::close);
event.serverStopped(mongoClient::close);
});
}

/**
* Setup routes for the server.
*
* @param server The Javalin server instance
*/
private void setupRoutes(Javalin server) {
// Add the routes for each of the implementations of `Controller` in the
// `controllers` array.
for (Controller controller : controllers) {
controller.addRoutes(server);
}
}
}
13 changes: 7 additions & 6 deletions server/src/main/java/umm3601/user/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,27 @@

// There are two examples of suppressing CheckStyle
// warnings in this class. If you create new classes
// that mirror data in the database and that will be managed
// by Jackson, then you'll probably need to suppress
// that mirror data in MongoDB and that will be managed
// by MongoJack, then you'll probably need to suppress
// the same warnings in your classes as well so that
// CheckStyle doesn't shout at you and cause the build
// to fail.

// Normally you'd want all fields to be private, but
// we need the fields in this class to be public since
// they will be written to by the Jackson library. We
// need to suppress the Visibility Modifier
// they will be written to by Mongo via the MongoJack
// library. We need to suppress the Visibility Modifier
// (https://checkstyle.sourceforge.io/config_design.html#VisibilityModifier)
// check in CheckStyle so that we don't get a failed
// build when Gradle runs CheckStyle.
@SuppressWarnings({"VisibilityModifier"})
public class User {

@ObjectId @Id
// By default Java field names shouldn't start with underscores.
// Here, though, we *have* to use the name `_id` to match the
// name of the field in the database.
// name of the field as used by MongoDB.
@SuppressWarnings({"MemberName"})
@ObjectId @Id
public String _id;

public String name;
Expand Down
Loading

0 comments on commit 2b53d94

Please sign in to comment.