diff --git a/.cbfmt.toml b/.cbfmt.toml new file mode 100644 index 0000000..47ec74c --- /dev/null +++ b/.cbfmt.toml @@ -0,0 +1,3 @@ +[languages] +js = ["prettier"] +java = ["google-java-format -"] diff --git a/.vitepress/config.mjs b/.vitepress/config.mjs index 5dc7e15..b9bb333 100644 --- a/.vitepress/config.mjs +++ b/.vitepress/config.mjs @@ -22,6 +22,7 @@ export default defineConfig({ '/bash/': sidebars.bash, '/express/': sidebars.express, '/git/': sidebars.git, + '/javalin/': sidebars.javalin, '/html-css/': sidebars.htmlCss, '/java/': sidebars.java, '/js/': sidebars.javascript, diff --git a/.vitepress/sidebars/index.js b/.vitepress/sidebars/index.js index 884f17b..ce44ce6 100644 --- a/.vitepress/sidebars/index.js +++ b/.vitepress/sidebars/index.js @@ -1,6 +1,7 @@ export { bash } from './bash.js' export { express } from './express.js' export { git } from './git.js' +export { javalin } from './javalin.js' export { htmlCss } from './html-css.js' export { javascript } from './javascript.js' export { java } from './java.js' diff --git a/.vitepress/sidebars/java.js b/.vitepress/sidebars/java.js index 81e0550..32d5ab3 100644 --- a/.vitepress/sidebars/java.js +++ b/.vitepress/sidebars/java.js @@ -18,5 +18,81 @@ export const java = [ }, { text: 'Data types', link: '/java/data-types' } ] + }, + { + text: 'Data and iteration', + collapsed: true, + items: [ + { + text: 'Arrays', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + }, + { + text: 'Strings', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + }, + { + text: 'Loops', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + }, + + { + text: 'Objects', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + } + ] + }, + + { + text: 'Object oriented programming', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' }, + { + text: 'Design patterns', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + } + ] + }, + + { + text: 'Testing', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] + }, + + { + text: 'Building projects', + collapsed: true, + items: [ + { text: 'Creating a Maven project', link: '/java/qq' }, + { text: 'Making a CLI', link: '/java/qq' }, + { text: 'Handling Errors', link: '/java/qq' }, + ] + }, + + { + text: 'Asynchronous code', + collapsed: true, + items: [ + { text: 'qq', link: '/java/qq' } + ] } ] diff --git a/.vitepress/sidebars/javalin.js b/.vitepress/sidebars/javalin.js new file mode 100644 index 0000000..53bbba5 --- /dev/null +++ b/.vitepress/sidebars/javalin.js @@ -0,0 +1,49 @@ +export const javalin = [ + { text: 'Introduction', link: '/javalin/index' }, + { + text: 'Data layer', + items: [ + { + text: 'Managing the database', + link: '/javalin/managing-the-database' + }, + { + text: 'Connecting to a database', + link: '/javalin/connecting-to-a-database' + }, + { + text: 'Adding a model layer', + link: '/javalin/adding-a-model-layer' + } + ] + }, + { + text: 'Creating an API', + items: [ + { text: 'Creating a server', link: '/javalin/creating-a-server' }, + { text: 'Routing and Controllers', link: '/javalin/routing' }, + { + text: 'Request and response', + link: '/javalin/request-response', + collapsed: true, + items: [ + { text: 'Query params', link: '/javalin/query-params' }, + { text: 'URL params', link: '/javalin/url-params' }, + { text: 'Body and headers', link: '/javalin/body-and-headers' } + ] + }, + { text: 'Sending errors', link: '/javalin/sending-errors' }, + ] + }, + { + text: 'Server side rendering', + items: [ + { text: 'Static files', link: '/javalin/static-files' }, + { text: 'Views and templates', link: '/javalin/views-and-templates' }, + { text: 'Using loops', link: '/javalin/using-loops' }, + { text: 'Template partials', link: '/javalin/template-partials' }, + { text: 'CSS and assets', link: '/javalin/css-and-assets' }, + { text: 'User input', link: '/javalin/user-input' } + ] + } +] diff --git a/.vitepress/sidebars/topnav.js b/.vitepress/sidebars/topnav.js index 3d3eb77..3e41d51 100644 --- a/.vitepress/sidebars/topnav.js +++ b/.vitepress/sidebars/topnav.js @@ -74,6 +74,15 @@ export const topnav = [ link: '/dotnet/' } ] + }, + { + text: 'Java', + items: [ + { + text: 'Javalin', + link: '/javalin/' + } + ] } ] }, diff --git a/src/express/web-socket-server.md b/src/express/web-socket-server.md index dc71041..a252ff5 100644 --- a/src/express/web-socket-server.md +++ b/src/express/web-socket-server.md @@ -43,7 +43,7 @@ server.on('connection', (socket) => { }) ``` -When the `'connection'` even is emitted, we are passed a web socket, which we +When the `'connection'` event is emitted, we are passed a web socket, which we can call whatever we want (here, we used `socket`). ## Listening for socket events diff --git a/src/java/maven.md b/src/java/maven.md new file mode 100644 index 0000000..53504f2 --- /dev/null +++ b/src/java/maven.md @@ -0,0 +1,4 @@ +# Creating a Maven project + +## + diff --git a/src/javalin/adding-a-model-layer.md b/src/javalin/adding-a-model-layer.md new file mode 100644 index 0000000..5bd8789 --- /dev/null +++ b/src/javalin/adding-a-model-layer.md @@ -0,0 +1,128 @@ +# Adding a model layer + +We'll need to create the _model_ layer and the _data access layer_. These are the parts of our application that deals with handling data models and persisiting data using the database. + +## Models and Repositories + +Models are classes that we use to represent data within the application. + +```java +package com.corndel.bleeter.models; + +public class User { + private Integer id; + public String username; + public boolean verified; + + public User(Integer id, String username, boolean verified) { + this.id = id; + this.username = username; + this.verified = verified; + } + + public Integer getId() { + return id; + } +} +``` + +Repositories are classes that interact with the database to let us persist, modify, and delete this data. + + +```java +package com.corndel.bleeter.repositories; + +import com.corndel.bleeter.models.User; +import com.corndel.bleeter.DB; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class UserRepository { + public static List findAll() throws SQLException { + var query = "SELECT id, username, verified FROM users"; + + try (var connection = DB.getConnection(); + var statement = connection.createStatement(); + var resultSet = statement.executeQuery(query);) { + + var users = new ArrayList(); + while (resultSet.next()) { + var id = resultSet.getInt("id"); + var username = resultSet.getString("username"); + var verified = resultSet.getBoolean("verified"); + users.add(new User(id, username, verified)); + } + return users; + } + } +} +``` + +## Querying with substitutions + +JDBC lets us set up _Prepared Statements_. These let us substitute in parameters to our SQL queries. + +```java +public static User findById(id) { + var query = "SELECT id, username, verified FROM users WHERE id = ?"; // [!code highlight] + try (var connection = DB.getConnection(); + var statement = connection.prepareStatement(query)) { // [!code highlight] + statement.setInt(1, id) // [!code highlight] + try (var resultSet = statement.executeQuery()) { + if (!resultSet.next()) { + return null; + } + var id = resultSet.getInt("id"); + var username = resultSet.getString("username"); + var verified = resultSet.getBoolean("verified"); + return new User(id, username, verified); + } + } +} +``` + +::: danger + +Do not be tempted to interpolate raw arguments into the query string. This opens +you up to SQL injection attacks. + +Consider + +```java + User.findById("3; DROP TABLE users;"); +``` + +Always use prepared statements! + +::: + +## Inserting data + +We can use an `INSERT` query with several parameters by putting more `?` and +passing the substitutions in with `.setString()`, `.setInt()`, or the appropriate set method for the datatype: + +```java +public static User create(username, verified) { + var query = // [!code highlight:2] + "INSERT INTO users (username, verified) VALUES (?, ?) RETURNING *"; + + try (var connection = DB.getConnection(); + var statement = con.prepareStatement(query)) { + statement.setString(1, username); // [!code highlight] + statement.executeUpdate(); // [!code highlight] + + try (var resultSet = statement.getResultSet()) { + rs.next() + var id = rs.getInt("id"); + return new User(id, username, verified) + } + } +} +``` + +::: info + +Note the `RETURNING *` causes the statement to have a `resultSet` after execution. This lets us get the `id` and other fields of the newly created `User` from the database. + +::: diff --git a/src/javalin/adding-css.md b/src/javalin/adding-css.md new file mode 100644 index 0000000..e69de29 diff --git a/src/javalin/auth.md b/src/javalin/auth.md new file mode 100644 index 0000000..e69de29 diff --git a/src/javalin/body-and-headers.md b/src/javalin/body-and-headers.md new file mode 100644 index 0000000..b77d957 --- /dev/null +++ b/src/javalin/body-and-headers.md @@ -0,0 +1,78 @@ +# Body and headers + +## Headers + +When we send a request, the headers are available in the backend as `ctx.header(String)`. + +## Endpoints with body + +### Accessing the body text + +### Form data + +### JSON + +When we send a request with a JSON body, we can use `ctx.bodyAsClass(Some.class)` to parse the JSON into an object using a class. + +::: code-group + +```java{8-10,13-18} [server] +public class App { + private Javalin app; + + public App() { + app.post( + "/users", + ctx -> { + var body = ctx.bodyAsClass(UserRequestBody.class); + var newUser = UserRepository.create(body.username, body.verified); + res.status(HttpStatus.CREATED).json(newUser); + }); + } + + class UserRequestBody { + public String username; + public boolean verified; + + public UserRequestBody() {} + } +} +``` + +```bash{3} [client] +curl -v -X POST http://localhost:8080/users \ + -H "Content-Type: application/json" \ + -d '{"username": "MinnieMouse", "verified": true}' + +> POST /users HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/7.81.0 +> Accept: */* +> Content-Type: application/json +> Content-Length: 45 + +< HTTP/1.1 200 OK +< Content-Type: application/json; charset=utf-8 +< Content-Length: 47 +< Date: Thu, 22 Feb 2024 16:57:57 GMT + +{ + "id": 21, + "username": "MinnieMouse", + "verified": 1 +} + +``` + +::: + +Since the `ctx.bodyAsClass()` method uses the Jackson ObjectMapper under the hood, it needs a default constructor. It will then fill in the public fields that match betwwen the class and the JSON body. + +::: tip + +According to the +[HTTP standard](https://www.rfc-editor.org/rfc/rfc9110.html#name-terminology-and-core-concep), +`GET` requests cannot have a body, so we generally only use body in requests +with `POST`, `PUT` and `PATCH`. + +::: diff --git a/src/javalin/connecting-to-a-database.md b/src/javalin/connecting-to-a-database.md new file mode 100644 index 0000000..5f17ba1 --- /dev/null +++ b/src/javalin/connecting-to-a-database.md @@ -0,0 +1,144 @@ +# Connecting to the database + +## Setting up the database driver + +We use Java's built-in [JDBC](https://en.wikipedia.org/wiki/Java_Database_Connectivity) library to provide a connection to the database, allowing us to run SQL queries from Java. + +First, we _add_ the `sqlite-jdbc` dependency to our `pom.xml`. + +```xml{3-7} + + + + org.xerial + sqlite-jdbc + 3.46.0.0 + + + + +``` + +This lets JDBC connect to our sqlite database. + +## Use the database connection + +We can then make queries to the database like so: + +```java +package com.corndel.bleeter.repositories; + +import com.corndel.bleeter.models.User; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class UserRepository { + public static List findAll() throws SQLException { + var dbUrl = "jdbc:sqlite:bleeter.db"; // [!code focus:6] + var query = "SELECT id, username, verified FROM users"; + + try (var connection = DriverManager.getConnection(dbUrl); // [!code highlight:3] + var statement = connection.createStatement(); + var resultSet = statement.executeQuery(query);) { + + var users = new ArrayList(); + while (resultSet.next()) { + var id = resultSet.getInt("id"); + var username = resultSet.getString("username"); + var verified = resultSet.getBoolean("verified"); + users.add(new User(id, username, verified)); + } + return users; + } + } +} +``` + +::: info + +Notice the assignments in the highlighted `try` statement. + +This is a [_try-with-resources_ statement](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html). It will automatically `.close()` the `resultSet`, `statement`, and `connection` once the code leaves the try block. + +This is important because the database connection and related objects can tie up system resources, and `.close()`-ing frees these up so other parts of your system can use them again! + +::: + + +## Create a Database connection class + +Imagine if we have _hundreds_ of queries! It could become a hassle if we need to change the way we make connections to the database. We can create a class to manage these connections instead. + +```java +package com.corndel.bleeter; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class DB { // [!code focus:7] + static final String dbUrl = "jdbc:sqlite:bleeter.db"; + + public static Connection getConnection() throws SQLException { + return DriverManager.getConnection(dbUrl); + } +} +``` + +Now we can make a connection through this class instead of having to enter the connection string every time we want to make a query. + +For example, we've made a tiny change to the code above: + +```java +package com.corndel.bleeter.repositories; + +import com.corndel.bleeter.models.User; +import com.corndel.bleeter.DB; // [!code ++] +import java.sql.DriverManager; // [!code --] +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class UserRepository { + public static List findAll() throws SQLException { + var dbUrl = "jdbc:sqlite:bleeter.db"; // [!code --] // [!code focus:7] + var query = "SELECT id, username, verified FROM users"; + + try (var connection = DriverManager.getConnection(dbUrl); // [!code --] + try (var connection = DB.getConnection(); // [!code ++] + var statement = connection.createStatement(); + var resultSet = statement.executeQuery(query);) { + + var users = new ArrayList(); + while (resultSet.next()) { + var id = resultSet.getInt("id"); + var username = resultSet.getString("username"); + var verified = resultSet.getBoolean("verified"); + users.add(new User(id, username, verified)); + } + return users; + } + } +} +``` + +So that, if we needed to change the `dbUrl` for some reason, we need only do that in one place! + +```java +package com.corndel.bleeter; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class DB { // [!code focus:8] + static final String dbUrl = "jdbc:sqlite:bleeter.db"; // [!code --] + static final String dbUrl = "jdbc:sqlite:some-other-file.db"; // [!code ++] + + public static Connection getConnection() throws SQLException { + return DriverManager.getConnection(dbUrl); + } +} +``` diff --git a/src/javalin/creating-a-server.md b/src/javalin/creating-a-server.md new file mode 100644 index 0000000..a0607f9 --- /dev/null +++ b/src/javalin/creating-a-server.md @@ -0,0 +1,96 @@ +# Creating a server + +## Project setup + +### Set up a Maven project + +qq + +### Add the Javalin dependency + +Add the following dependency to `pom.xml` + +```xml + + io.javalin + javalin + 6.1.6 + +``` + +## Configure the app + +In `App.java`, we configure the Javalin application and the `main` entrypoint. + +```java +package com.corndel.bleeter; + +import io.javalin.Javalin; + +public class App { + private Javalin app; + + public static void main(String[] args) { + var javalin = new App().javalinApp(); + javalin.start(8080); + } + + public App() { + app = Javalin.create(); + app.get("/hello", ctx -> ctx.result("Welcome to the Bleeter server!")); + } + + public Javalin javalinApp() { + return app; + } +} +``` + +## Compile and run the app + +We can now either use VSCode to compile and run our server, or run it in the command line with the following command: + +::: code-group + +```bash +./mvnw clean compile exec:java -Dexec.mainClass=com.corndel.bleeter.App +``` + +```console [output] +17:23:14.039 [com.corndel.bleeter.App.main()] INFO io.javalin.Javalin - Starting Javalin ... +17:23:14.042 [com.corndel.bleeter.App.main()] INFO org.eclipse.jetty.server.Server - jetty-11.0.20; built: 2024-01-29T21:04:22.394Z; git: 922f8dc188f7011e60d0361de585fd4ac4d63064; jvm 21.0.3+9-LTS +17:23:14.088 [com.corndel.bleeter.App.main()] INFO o.e.j.s.s.DefaultSessionIdManager - Session workerName=node0 +17:23:14.100 [com.corndel.bleeter.App.main()] INFO o.e.j.server.handler.ContextHandler - Started o.e.j.s.ServletContextHandler@659d0500{/,null,AVAILABLE} +17:23:14.106 [com.corndel.bleeter.App.main()] INFO o.e.jetty.server.AbstractConnector - Started ServerConnector@69c25ba8{HTTP/1.1, (http/1.1)}{0.0.0.0:8080} +17:23:14.111 [com.corndel.bleeter.App.main()] INFO org.eclipse.jetty.server.Server - Started Server@54c69995{STARTING}[11.0.20,sto=0] @2212ms +17:23:14.111 [com.corndel.bleeter.App.main()] INFO io.javalin.Javalin - + __ ___ _____ + / /___ __ ______ _/ (_)___ / ___/ + __ / / __ `/ | / / __ `/ / / __ \ / __ \ +/ /_/ / /_/ /| |/ / /_/ / / / / / / / /_/ / +\____/\__,_/ |___/\__,_/_/_/_/ /_/ \____/ + + https://javalin.io/documentation + +17:23:14.111 [com.corndel.bleeter.App.main()] INFO io.javalin.Javalin - Javalin started in 122ms \o/ +17:23:14.114 [com.corndel.bleeter.App.main()] INFO io.javalin.Javalin - Listening on http://localhost:8080/ +17:23:14.120 [com.corndel.bleeter.App.main()] INFO io.javalin.Javalin - You are running Javalin 6.1.6 (released May 28, 2024). +``` + +::: + +## Make a request + +Now we can make a `GET` request to `localhost:8080/hello` to see the server return a message. + +::: code-group + +```bash +curl localhost:8080/hello +``` + +```console [output] +Welcome to the Bleeter server! +``` + +::: diff --git a/src/javalin/css-and-assets.md b/src/javalin/css-and-assets.md new file mode 100644 index 0000000..d5e8576 --- /dev/null +++ b/src/javalin/css-and-assets.md @@ -0,0 +1,35 @@ +# CSS and assets + + + +## Including assets + +With static file serving set up, you are able to access assets in the +`public folder` from within your views. Suppose you have an image stored at +`public/images/logo.png`. You can display it in your views like this: + +```html{6} [index.ejs] +<%- include('partials/header') %> + +

Welcome to Bleeter

+

Follow the herd

+ +Bleeter logo + +<%- include('partials/footer') %> +``` + +## Applying CSS + +To apply CSS to your files, create a `.css` file in your public folder. For +example, `public/css/index.css`. To apply it, simply include in your HTML +`` as normal: + +```html{4} [header.ejs] + + + + + Bleeter | <%= title %> + +``` diff --git a/src/javalin/index.md b/src/javalin/index.md new file mode 100644 index 0000000..d6a486b --- /dev/null +++ b/src/javalin/index.md @@ -0,0 +1,5 @@ +# Introduction + +

+ +

diff --git a/src/javalin/managing-the-database.md b/src/javalin/managing-the-database.md new file mode 100644 index 0000000..5e9f476 --- /dev/null +++ b/src/javalin/managing-the-database.md @@ -0,0 +1,78 @@ +# Managing the database + +## Creating the schema + +We will set up our database with the following schema: + +::: code-group + +```sql [users] +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + verified BOOLEAN DEFAULT 0 +); +``` + +```sql [bleets] +CREATE TABLE IF NOT EXISTS bleets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL, + createdAt TEXT DEFAULT (datetime('now')), + userId INTEGER NOT NULL, + FOREIGN KEY (userId) REFERENCES USER(id) +); +``` + +```sql [likes] +CREATE TABLE IF NOT EXISTS user_bleet_likes ( + userId INTEGER NOT NULL, + bleetId INTEGER NOT NULL, + PRIMARY KEY (userId, bleetId), + FOREIGN KEY (userId) REFERENCES USER(id), + FOREIGN KEY (bleetId) REFERENCES BLEET(id) +); +``` + +::: + +## Creating the tables + +This SQL, along with the seed data, could be stored in the directory +`src/main/resources/db/migration` in our project. We _could_ run it with, for example, + +```bash +sqlite3 bleeter.sqlite < src/main/resources/db/migration/V1__Reset.sql +``` + +However, we can instead use a tool called flyway to manage this. + +In `pom.xml`, we can configure the flyway plugin to manage the correct database. + +```xml + + + + org.flywaydb + flyway-maven-plugin + 10.15.2 + + jdbc:sqlite:bleeter.sqlite + + + + org.xerial + sqlite-jdbc + 3.46.0.0 + + + + + +``` + +We can then use flyway to run the sql statements in order using the following commmand: + +```bash +mvn flyway:migrate +``` diff --git a/src/javalin/middleware.md b/src/javalin/middleware.md new file mode 100644 index 0000000..4dd1086 --- /dev/null +++ b/src/javalin/middleware.md @@ -0,0 +1 @@ +# Middleware diff --git a/src/javalin/model-layer.md b/src/javalin/model-layer.md new file mode 100644 index 0000000..85b67e6 --- /dev/null +++ b/src/javalin/model-layer.md @@ -0,0 +1 @@ +# Adding a model layer diff --git a/src/javalin/partial-templates.md b/src/javalin/partial-templates.md new file mode 100644 index 0000000..e69de29 diff --git a/src/javalin/query-params.md b/src/javalin/query-params.md new file mode 100644 index 0000000..7ba7bc8 --- /dev/null +++ b/src/javalin/query-params.md @@ -0,0 +1,56 @@ +# Query params + +## URL structure + +If you've ever looked at the URL bar in your browser, you'll recognise query +params. They're the things separated by `&` and `?` after the url: + +```txt +https://www.google.com/search?q=how+to+css&&sclient=gws-wiz +``` + +## Parsing query params + +Javalin does a lot of the hard work for us. We can access the query parameters through the context object using the `queryParam()` method. + +::: code-group + +```java{2,3} [server] +app.get("/users", ctx -> { + var limit = Integer.parseInt(ctx.queryParam("limit")); + var users = UserRepository.findAll(limit); + ctx.json(users); +}) +``` + +```bash{1} [client] +> GET "/users?limit=3 HTTP/1.1" +> Host: localhost:8080 +> User-Agent: curl/7.81.0 +> Accept: */* + +< HTTP/1.1 200 OK +< Content-Type: application/json; charset=utf-8 +< Content-Length: 136 +< Date: Thu, 22 Feb 2024 16:42:33 GMT + +[ + { + "id": 1, + "username": "EluskM", + "verified": 1 + }, + { + "id": 2, + "username": "BillGatekeeper", + "verified": 1 + }, + { + "id": 3, + "username": "JeffWho", + "verified": 0 + } +] +``` + +::: diff --git a/src/javalin/request-response.md b/src/javalin/request-response.md new file mode 100644 index 0000000..8dbbfbd --- /dev/null +++ b/src/javalin/request-response.md @@ -0,0 +1,104 @@ +# Request and response + +## Request and response + +There are several ways to send extra information as part of the request and +response. These include: + +- Query params: parts of the url such as `?x=7&y=cool` +- URL params: dynamic parts of the endpoint, such as the `7` in `/users/7` +- Headers: Meta information about the request or response which is part of the + HTTP standard +- Body: Complex, structured data which accompanies the request or response + +## HTTP tools + +Tools such as [cURL](https://curl.se/docs/), [Postman](https://www.postman.com/) +and the VS Code plugin [Thunderclient](https://www.thunderclient.com/) allows us +to construct more complex requests to test our applications. They simulate +client applications such as CLIs, mobile apps and websites which would be making +these requests in production. + +## Sending and receiving JSON + +Sending JSON is relatively simple if we're happy with exposing the public fields and getters. + +::: code-group + +```java [server] +app.get("/users", ctx -> { + var users = UserRepository.findAll(); + ctx.json(users); +}); +``` + +```bash [client] +> GET /users HTTP/1.1 +> Host: localhost:5000 +> User-Agent: curl/7.81.0 +> Accept: */* + +< HTTP/1.1 200 OK +< X-Powered-By: Express +< Content-Type: application/json; charset=utf-8 +< Content-Length: 465 +< ETag: W/"1d1-ANZR8XoyXChze/h4JDQ5/asCB28" +< Date: Thu, 22 Feb 2024 17:07:31 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 + +[ + { + "id": 1, + "username": "EluskM", + "verified": 1 + }, + { + "id": 2, + "username": "BillGatekeeper", + "verified": 1 + }, + { + "id": 3, + "username": "JeffWho", + "verified": 0 + }, + { + "id": 4, + "username": "OprahWindey", + "verified": 1 + }, + { + "id": 5, + "username": "MarkZeeberg", + "verified": 1 + }, + { + "id": 6, + "username": "LarryFlinger", + "verified": 0 + }, + { + "id": 7, + "username": "CannyWest", + "verified": 1 + }, + { + "id": 8, + "username": "TaylorSquid", + "verified": 0 + }, + { + "id": 9, + "username": "ArianaVenti", + "verified": 1 + }, + { + "id": 10, + "username": "KylieGenner", + "verified": 0 + } +] +``` + +::: diff --git a/src/javalin/routing.md b/src/javalin/routing.md new file mode 100644 index 0000000..6597e2c --- /dev/null +++ b/src/javalin/routing.md @@ -0,0 +1,3 @@ +# Routing and Controllers + +## qq diff --git a/src/javalin/sending-errors.md b/src/javalin/sending-errors.md new file mode 100644 index 0000000..edca46b --- /dev/null +++ b/src/javalin/sending-errors.md @@ -0,0 +1,131 @@ +# Sending errors + +## Error codes + +When a client makes an HTTP request to the server, the server usually responds with a status code. + +Here are some of the more common status codes. + +| Code | Status | Meaning | +| :--: | :-------------------- | :------------------------------------------------------------------------------- | +| 200 | OK | The operation has succeeded | +| 201 | Created | An entity has been created (e.g. rows inserted into the database) | +| 204 | No Content | The operation has succeeded, and the client doesn't need to do anything about it | +| 400 | Bad Request | The data sent by the client is not valid | +| 401 | Unauthorized | In order to access this endpoint, you need to send credentials | +| 403 | Forbidden | The credentials you send are not correct for this endpoint | +| 404 | Not Found | This resource is not found | +| 500 | Internal Server Error | Something went wrong and the server will not explain why | + +Detailed information on all status codes can be found on +[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status). + +In particular, we are interested in the error codes, (`400`-`599`). + +## Error Responses + +Javalin includes some `HttpResponseException`s that we can use to trigger error responses. + +```java +public class HomeController { + public static void post() throws HttpResponseException { + throw new ForbiddenResponse(); + } +} + +public class App { + private Javalin app; + + public App() { + app = Javalin.create(); + app.post("/", HomeController::post); + } +} +``` + +::: tip + +A list of available error responses are available on [Javalin's documentation](https://javalin.io/documentation#default-responses). They are all named consistently in the form `statusName + "Response"`. + +::: + +## Custom exceptions + +## Throwing errors + +In our models, we can throw errors that arise from handling data, which is where most errors occur. + +```java +public class UserRepository { + public static User findById(int id) throws HttpResponseException { + if (id < 0) { + throw new BadRequestResponse("ID is invalid"); + } + + var query = "SELECT id, username, firstName, lastName, email, avatar FROM users WHERE id = ?"; + + try (var con = DB.getConnection(); + var stmt = con.prepareStatement(query)) { + stmt.setInt(1, id); + try (var rs = stmt.executeQuery()) { + if (!rs.next()) { + throw new NotFoundResponse(""); + } + var username = rs.getString("username"); + var email = rs.getString("email"); + var verified = rs.getBoolean("avatar"); + + return new User(id, username, email, verified); + } + } + } +} +``` + +## Catching errors + +We can catch the error in our controller and decide what to do with it. In this case, we pass the error to an error handler. + +```java +public class App { + private Javalin app; + + public App() { + app = Javalin.create(); + app.get("/user/{userId}", ctx -> {}); + } +} +``` + +## Handling errors + +We can send the error code and a useful message to the client by attaching an +error handler to the `app`. + +::: warning + +We should ensure the message doesn't expose our server's internal workings too +much, as this information could be used by hackers. Better not to send the whole +error object, but rather take full control of the information which is sent to +the client. + +::: + +```js +app.use((err, req, res, next) => { + console.error(err) + if (err instanceof AppError) { + res.status(err.code).json({ error: err.message }) + } else { + res.status(500).json({ error: 'Something went wrong.' }) + } +}) +``` + +::: tip + +Make sure that this `.use()` is the last method called on `app` before +`.listen()`. It must accept all 4 parameters `(err, req, res, next)` as this is +how express infers that the function is an error handler. + +::: diff --git a/src/javalin/static-files.md b/src/javalin/static-files.md new file mode 100644 index 0000000..746cca3 --- /dev/null +++ b/src/javalin/static-files.md @@ -0,0 +1,43 @@ +# Static files + +## Serving static files + +Until now, we have been using our server to send back data such as strings or +json. However, we can serve whole files such as pictures or documents, too! + +## Public directory + +Although you can put the files you'd like to serve anywhere, it's convention to +put them in the `resources` directory under a folder called `public` + +``` +src +└── main + └── resources + └── public + ├── logs + │   └── hello.txt + └── logo.png +``` + +## Using `config.staticFiles.add()` + +We instruct our javalin app to serve files from `public` using `config.staticFiles.add()`. + +```java{6-9} +public class App { + public Javalin app; + + public App() { + app = Javalin.create( + config -> { + config.staticFiles.add("/public", Location.CLASSPATH); + }); + } +} +``` + +## Requesting static files + +Now, if we visit `/logo.png` or `/logs/hello.txt` we will get the corresponding +files. Note that `/public/` is _not_ part of the url. diff --git a/src/javalin/template-partials.md b/src/javalin/template-partials.md new file mode 100644 index 0000000..3d874be --- /dev/null +++ b/src/javalin/template-partials.md @@ -0,0 +1,180 @@ +# Template partials + +## The problem + +Many pages in a website might share the same content. For example, our views +might look like this (the repeated content has been highlighted): + +::: code-group + +```html{1-9,12-13} [index.jte] +@import bleeter.models.Page +@param Page page + + + + + + + Bleeter | ${page.title} + + + +

Welcome to Bleeter

+

Follow the herd

+ + +``` + +```html{1-9,20-21} [bleets.jte] +@import java.util.List +@import bleeter.models.Page +@import bleeter.models.Bleet +@param Page page +@param List bleets + + + + + + + Bleeter | ${page.title} + + + +

Bleets

+
    + @for(var bleet : bleets) + +
  1. + ${bleet.content.substring(0, 10)}... + Read more +
  2. + @endfor +
+ + +``` + +```html{1-9,13-14} [bleet.jte] +@import bleeter.models.Page +@import bleeter.models.Bleet +@param Page page +@param Bleet bleet + + + + + + + Bleeter | ${page.title} + + + +

A bleet

+

${bleet.content}

+

${bleet.createdAt}

+ + +``` + +::: + +This is a problem, because if we need to make a change to the shared parts of +each view, we need to update each view individually, which is extra work and an +opportunity for mistakes. + +## Template calls + +We can create partial templates and use them in our views. + +The directory structure might look like this: + +```txt +templates/ +├── bleet.ejs +├── bleets.ejs +├── index.ejs +└── partials + ├── footer.ejs + └── header.ejs +``` + +We will put the repeated HTML into the partial templates. + +::: code-group + +```html [header.ejs] + + + + + + Bleeter | <%= title %> + + + +``` + +```html [footer.ejs] + + +``` + +::: + +## Using partials + +To use the partial templates in our views, we use the EJS `include` function. + +::: code-group + +```html [index.ejs] +<%- include('partials/header') %> + +

Welcome to Bleeter

+

Follow the herd

+ +<%- include('partials/footer') %> +``` + +```html [bleets.ejs] +<%- include('partials/header') %> + +

Bleets

+
    + <% for (let bleet of bleets) { %> + +
  1. + <%= bleet.content.slice(0, 10) + '...' %> + Read more +
  2. + <% } %> +
+ +<%- include('partials/footer') %> +``` + +```html [bleet.ejs] +<%- include('partials/header') %> + +

A bleet

+

<%= bleet.content %>

+

<%= bleet.createdAt %>

+ +<%- include('partials/footer') %> +``` + +::: + +::: danger + +When rendering untrusted content such as user input (including input stored in the database), always use `<%= %>` and not `<%- %>`. The ensures that the HTML content is escaped and prevents [XSS attacks](https://learn.snyk.io/lesson/xss/). + +::: + +::: info + +Partials have access to the data of their parent! So, for example, the `title` which is passed to the `index.ejs` template is available in the `header.ejs` partial. + +::: \ No newline at end of file diff --git a/src/javalin/url-params.md b/src/javalin/url-params.md new file mode 100644 index 0000000..c1d43af --- /dev/null +++ b/src/javalin/url-params.md @@ -0,0 +1,65 @@ +# URL params + +## URL structure + +To make parts of the url dynamic in Javalin, we can use curly braces like so: + +```java {1} +app.get("/users/{x}/{y}/{z}", ctx -> { + var x = ctx.pathParam("x"); + var y = ctx.pathParam("y"); + var z = ctx.pathParam("z"); + + var hashMap = new HashMap(); + + hashMap.put("x", x); + hashMap.put("y", y); + hashMap.put("z", z); + + ctx.json(hashMap); +}) +``` + +Now, a `GET` request to `/users/3/awesome/true` would respond with: + +``` +{ + "x": "3", + "y": "awesome", + "z": "true" +} +``` + +## Using URL params + +A practical application of URL params is to create dynamic endpoints, where users of the API can request particular rows from a table. + +::: code-group + +```java{2-3} [server] +app.get("/users/{userId}", ctx -> { + var id = Integer.parseInt(ctx.pathParam("userId")); + var user = UserRepository.findById(id); + ctx.json(user); +}) +``` + +```bash{1} [client] +> GET /users/13 HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/8.5.0 +> Accept: */* +> +< HTTP/1.1 200 OK +< Date: Thu, 18 Jul 2024 10:52:45 GMT +< Content-Type: application/json +< Content-Length: 212 +< +{ + "id": 13, + "username": "EdShearing", + "verified": 1 +} +``` + +::: diff --git a/src/javalin/user-input.md b/src/javalin/user-input.md new file mode 100644 index 0000000..3603276 --- /dev/null +++ b/src/javalin/user-input.md @@ -0,0 +1,78 @@ +# User input + + + +## Creating a form + +We create a view which displays a form to the user. + +```js +
+ + + + + + + + +
+``` + +::: info + +The `name` attribute of the inputs determines the key once the form data is +parsed into a javascript object on the server. This form would result in an +object with keys of `userId` and `content`. + +::: + +## Rendering the form + +We need to create a controller which renders the form for the user to complete. + +```js +web.get('/bleets/new', (req, res) => { + res.render('bleets/new', { title: 'Create a bleet' }) +}) +``` + +::: warning + +This controller must be placed _above_ the `/bleets/:id` endpoint controller. If +not, then `/bleets/new` will match the `bleets/:id` pattern, and the string +`new` will be interpreted as the `id` of a bleet and passed to the wrong +controller! + +::: + +## Parsing form data + +We must configure the app to properly parse form data into a javascript object +and make it available on the `req.body`. + +```js [app.js] +app.use(express.urlencoded({ extended: true })) +``` + +::: warning + +This must occur _before_ any of the web routes are attached to the `app`. + +::: + +## Handling the post request + +The form submit a `POST` request to the `/bleets` endpoint. We must create a +controller for this: + +```js +app.post('/bleets', async (req, res) => { + const { content, userId } = req.body + await Bleet.create(content, userId) + res.redirect('/bleets') +}) +``` + +The `res.redirect('/bleets')` sends to browser to the `/bleets` page on +successful submission. diff --git a/src/javalin/using-loops.md b/src/javalin/using-loops.md new file mode 100644 index 0000000..9a3a06c --- /dev/null +++ b/src/javalin/using-loops.md @@ -0,0 +1,55 @@ +# Using loops + + + +## Passing an array + +It is a very common scenario to pass an array of objects to our template. + +```js +app.get('/bleets', async (req, res) => { + const bleets = await Bleet.findAll() // bleets is an array + res.render('bleets', { bleets }) +}) +``` + +This renders the `bleets.ejs` template with access to the array of `bleets` from +the database. + +## Rendering lists + +In our view, we can use EJS to render the list using a `for`/`of` loop. + +```html +
    + <% for (let bleet of bleets) { %> +
  1. + <%= bleet.content.slice(0, 10) + '...' %> + Read more +
  2. + <% } %> +
+``` + +::: tip + +It is not necessary to use `
    ` or `
      ` when rendering lists - you can use +this looping pattern to render any html you like. + +::: + +::: info + +The `<% %>` tags interpolate javascript into the template without displaying +anything in the view. This is often used to start and end loops, or to do +conditional rendering such as + +```html +<% if (condition) { %> +

      Condition is true!

      +<% } else { %> +

      Condition is false!

      +<% } %> +``` + +::: diff --git a/src/javalin/views-and-static.md b/src/javalin/views-and-static.md new file mode 100644 index 0000000..65cd0e7 --- /dev/null +++ b/src/javalin/views-and-static.md @@ -0,0 +1,85 @@ +# Views with EJS + +So far, we have used express to send JSON. But it can do much more than that. +Dynamic web applications often require the ability to generate HTML content +dynamically, based on the server-side data. EJS (Embedded JavaScript) templates +allow us to do just that in an Express application. In this section, we will +learn how to set up and use EJS as our templating engine to render dynamic +views. + +## Adding a view engine + +A view engine allows us to send this html to the user. + +First, we install `ejs`. + +```bash +npm install ejs +``` + +The, in `npm/app.js`, we register EJS as our view engine. + +```js +// server/app.js +app.set('view engine', 'ejs') +``` + +## Creating views + +Let's make a folder for our views, and create our first view. + +```bash +mkdir views +touch views/index.ejs +``` + +A `.ejs` file is basically HTML with some added javascript to make it dynamic. +In `index.ejs`, let's add the following: + +```html + + + + + + Bleeter + + +
      Bleeter
      +
      +

      Users

      +
      +
      Copyright
      + + +``` + +Now, in `app.js`, we'll add + +```js +app.get('/', (req, res) => { + res.render('index') +}) +``` + +The `.render` method will look for `index.js` in the `views` folder and send it +to the browser. + +## Including static files + +If we would like to include some `css` without our view, it means sending an +additional "static" file. Fortunately, sending static files in express is quite +easy. We add this line to `app.js` + +```js +app.use(express.static('public')) +``` + +Now, we can put our `style.css` in the `public` folder and add the following +line to the `` of our `index.js` + +```html + +``` + +We have created our first web server! diff --git a/src/javalin/views-and-templates.md b/src/javalin/views-and-templates.md new file mode 100644 index 0000000..119fc9c --- /dev/null +++ b/src/javalin/views-and-templates.md @@ -0,0 +1,55 @@ +# Views and templates + + + +## Adding a template engine + +First, we install `ejs` in our project + +```bash +npm install ejs +``` + +Then, we configure our app to render `.ejs` files found in the `views` folder to +`.html` before sending them to the browser. + +```js +app.set('view engine', 'ejs') +``` + +## Adding an endpoint + +We configure endpoints to determine which URLs serve which views. + +```js +app.get('/', async (req, res) => { + const bleet = await Bleet.findById(3) + + res.render('index', { + bleet + }) +}) +``` + +Notice we are using `res.render` rather than `res.send` or `res.json`. + +## Creating an EJS file + +Finally, in the `views` directory, we create our `index.ejs`. + +```html + + + + + + Bleeter + + + +

      Welcome to Bleeter!

      +

      Read some bleets...

      +

      <%= bleet.content %>

      + + +```