diff --git a/docs/asciidoc/body.adoc b/docs/asciidoc/body.adoc index bbb0ceb00c..de1fb3a1b4 100644 --- a/docs/asciidoc/body.adoc +++ b/docs/asciidoc/body.adoc @@ -1,6 +1,6 @@ -=== Request Body +==== Request Body -Raw `request body` is available via javadoc:Context[body] method: +The raw request body is available via the javadoc:Context[body] method: .Java [source,java,role="primary"] @@ -8,17 +8,17 @@ Raw `request body` is available via javadoc:Context[body] method: { post("/string", ctx -> { String body = ctx.body().value(); // <1> - ... + // ... }); post("/bytes", ctx -> { byte[] body = ctx.body().bytes(); // <2> - ... + // ... }); post("/stream", ctx -> { InputStream body = ctx.body().stream(); // <3> - ... + // ... }); } ---- @@ -29,57 +29,52 @@ Raw `request body` is available via javadoc:Context[body] method: { post("/string") { val body = ctx.body().value() // <1> - ... + // ... } post("/bytes") { val body = ctx.body().bytes() // <2> - ... + // ... } post("/stream") { val body = ctx.body().stream() // <3> - ... + // ... } } ---- -<1> `HTTP Body` as `String` -<2> `HTTP Body` as `byte array` -<3> `HTTP Body` as `InputStream` +<1> Reads the HTTP body as a `String`. +<2> Reads the HTTP body as a `byte array`. +<3> Reads the HTTP body as an `InputStream`. -This gives us the `raw body`. +===== Message Decoder -==== Message Decoder - -Request body parsing is achieved using the javadoc:MessageDecoder[] functional interface. +Request body parsing (converting the raw body into a specific object) is handled by the javadoc:MessageDecoder[] functional interface. [source, java] ---- public interface MessageDecoder { - T decode(Context ctx, Type type) throws Exception; } ---- -javadoc:MessageDecoder[] has a single `decode` method that takes two input arguments: `(context, type)` -and returns a single result of the given type. +The javadoc:MessageDecoder[] has a single `decode` method that takes the request context and the target type, returning the parsed result. -.JSON example: +.JSON Decoder Example: [source, java, role="primary"] ---- { FavoriteJson lib = new FavoriteJson(); // <1> - decoder(MediaType.json, (ctx, type) -> { // <2> - + decoder(MediaType.json, (ctx, type) -> { // <2> byte[] body = ctx.body().bytes(); // <3> - return lib.fromJson(body, type); // <4> }); post("/", ctx -> { MyObject myObject = ctx.body(MyObject.class); // <5> + return myObject; }); } ---- @@ -90,40 +85,37 @@ and returns a single result of the given type. { val lib = FavoriteJson() // <1> - decoder(MediaType.json) { ctx, type -> // <2> - + decoder(MediaType.json) { ctx, type -> // <2> val body = ctx.body().bytes() // <3> - lib.fromJson(body, type) // <4> } post("/") { val myObject = ctx.body() // <5> + myObject } } ---- -<1> Choose your favorite `json` library -<2> Check if the `Content-Type` header matches `application/json` -<3> Read the body as `byte[]` -<4> Parse the `body` and use the requested type -<5> Route handler now call the `body(Type)` function to trigger the decoder function +<1> Initialize your favorite JSON library. +<2> Register the decoder to trigger when the `Content-Type` header matches `application/json`. +<3> Read the raw body as a `byte[]`. +<4> Parse the payload into the requested type. +<5> Inside the route, calling `ctx.body(Type)` automatically triggers the registered decoder. -=== Response Body +==== Response Body -Response body is generated from `handler` function: +The response body is generated by the route handler. -.Response body +.Response Body Example [source, java,role="primary"] ---- { get("/", ctx -> { - ctx.setResponseCode(200); // <1> - + ctx.setResponseCode(200); // <1> ctx.setResponseType(MediaType.text); // <2> - ctx.setResponseHeader("Date", new Date()); // <3> - + return "Response"; // <4> }); } @@ -135,56 +127,46 @@ Response body is generated from `handler` function: { get("/") { ctx.responseCode = 200 // <1> - ctx.responseType = MediaType.text // <2> - ctx.setResponseHeader("Date", Date()) // <3> - + "Response" // <4> } } ---- -<1> Set `status code` to `OK(200)`. This is the default `status code` -<2> Set `content-type` to `text/plain`. This is the default `content-type` -<3> Set the `date` header -<4> Send a `Response` string to the client +<1> Set the status code to `200 OK` (this is the default). +<2> Set the `Content-Type` to `text/plain` (this is the default for strings). +<3> Set a custom response header. +<4> Return the response body to the client. -==== Message Encoder +===== Message Encoder -Response encoding is achieved using the javadoc:MessageEncoder[] functional interface. +Response encoding (converting an object into a raw HTTP response) is handled by the javadoc:MessageEncoder[] functional interface. [source, java] ---- public interface MessageEncoder { - Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception; } ---- -MessageEncoder has a single `encode` method that accepts two input arguments: `(context, value)` and -produces a result. +The javadoc:MessageEncoder[] has a single `encode` method that accepts the context and the value returned by the handler, producing an output. (Internally, javadoc:output.Output[] works like a `java.nio.ByteBuffer` for performance reasons). -The javadoc:output.Output[] works like `java.nio.ByteBuffer` and it is used internally -for performance reason. - -.JSON example: +.JSON Encoder Example: [source, java, role="primary"] ---- { FavoriteJson lib = new FavoriteJson(); // <1> - encoder(MediaType.json, (ctx, result) -> { // <2> - + encoder(MediaType.json, (ctx, result) -> { // <2> String json = lib.toJson(result); // <3> - ctx.setDefaultResponseType(MediaType.json); // <4> - return json; // <5> }); get("/item", ctx -> { - MyObject myObject = ...; + MyObject myObject = new MyObject(); return myObject; // <6> }); } @@ -196,25 +178,22 @@ for performance reason. { val lib = FavoriteJson() // <1> - encoder(MediaType.json) { ctx, result -> // <2> - + encoder(MediaType.json) { ctx, result -> // <2> val json = lib.toJson(result) // <3> - ctx.defaultResponseType = MediaType.json // <4> - json // <5> } get("/item") { - val myObject = ...; + val myObject = MyObject() myObject // <6> } } ---- -<1> Choose your favorite `json` library -<2> Check if the `Accept` header matches `application/json` -<3> Convert `result` to `JSON` -<4> Set default `Content-Type` to `application/json` -<5> Produces JSON response -<6> Route handler returns a user defined type +<1> Initialize your favorite JSON library. +<2> Register the encoder to trigger when the client's `Accept` header matches `application/json`. +<3> Convert the route's result into JSON. +<4> Set the `Content-Type` header to `application/json`. +<5> Return the encoded JSON payload. +<6> The route handler returns a user-defined POJO, which is automatically intercepted and encoded. diff --git a/docs/asciidoc/configuration.adoc b/docs/asciidoc/configuration.adoc index a8cfda1809..9e637bcc60 100644 --- a/docs/asciidoc/configuration.adoc +++ b/docs/asciidoc/configuration.adoc @@ -1,19 +1,16 @@ -== Configuration -Application configuration is based on https://github.com/lightbend/config[config] library. Configuration -can by default be provided in either Java properties, JSON, and https://github.com/lightbend/config/blob/master/HOCON.md[HOCON] files. +=== Configuration -Jooby allows overriding any property via system properties or environment variables. +Application configuration is built on the https://github.com/lightbend/config[Typesafe Config] library. By default, Jooby supports configuration provided in Java properties, JSON, or https://github.com/lightbend/config/blob/master/HOCON.md[HOCON] format. -=== Environment +Jooby allows you to override any property via system properties, environment variables, or program arguments. -The application environment is available via the javadoc:Environment[Environment] class, which allows specifying one -or many unique environment names. +==== Environment -The active environment names serve the purpose of allowing loading different configuration files -depending on the environment. Also, javadoc:Extension[] modules might configure application -services differently depending on the environment too. For example: turn on/off caches, reload files, etc. +The javadoc:Environment[Environment] class manages your application's configuration and active environment names (e.g., `dev`, `prod`, `test`). -.Initializing the Environment +Environment names allow you to load different configuration files or toggle features (like caching or file reloading) depending on the deployment stage. + +.Accessing the Environment [source, java, role = "primary"] ---- { @@ -29,85 +26,42 @@ services differently depending on the environment too. For example: turn on/off } ---- -The active environment names property is set in one of this way: - -- As program argument: `java -jar myapp.jar application.env=foo,bar`; or just `java -jar myapp.jar foo,bar` - -NOTE: This method works as long you start the application using one of the `runApp` methods - -- As system property: `java -Dapplication.env=foo,bar -jar myapp.jar` - -- As environment variable: `application.env=foo,bar` - - -The javadoc:Jooby[getEnvironment] loads the default environment. - -=== Default Environment - -The default environment is available via javadoc:Environment[loadEnvironment, io.jooby.EnvironmentOptions] method. - -This method search for an `application.conf` file in three location (first-listed are higher priority): - -- `${user.dir}/conf`. This is a file system location, useful is you want to externalize configuration (outside of jar file) -- `${user.dir}`. This is a file system location, useful is you want to externalize configuration (outside of jar file) -- `classpath://` (root of classpath). No external configuration, configuration file lives inside the jar file - -NOTE: We use `$user.dir` to reference `System.getProperty("user.dir")`. This system property is set -by the JVM at application startup time. It represent the current directory from where the JVM was -launch it. - -.File system loading -[source,bash] ----- -└── conf - └── application.conf -└── myapp.jar ----- - -A call to: +You can set active environment names in several ways: -[source] ----- - Environment env = getEnvironment(); ----- +* **Program Argument:** `java -jar myapp.jar prod,cloud` (This works when using Jooby's `runApp` methods). +* **System Property:** `java -Dapplication.env=prod -jar myapp.jar` +* **Environment Variable:** `application.env=prod` -Loads the `application.conf` from `conf` directory. You get the same thing if you -move the `application.conf` to `myapp.jar` directory. +==== Default Loading and Precedence -.Classpath loading -[source,bash] ----- -└── myapp.jar - └── application.conf (file inside jar) ----- +When you call `getEnvironment()`, Jooby searches for an `application.conf` file in the following order of priority: -WARNING: Jooby favors file system property loading over classpath property loading. So, if there -is a property file either in the current directory or conf directory it hides the same file -available in the classpath. +1. `${user.dir}/conf/application.conf` (External file system) +2. `${user.dir}/application.conf` (External file system) +3. `classpath://application.conf` (Internal jar resource) -=== Overrides +[NOTE] +==== +`${user.dir}` refers to the directory from which the JVM was launched. Jooby **favors file system files** over classpath files, allowing you to easily externalize configuration without rebuilding your jar. +==== -Property overrides is done in multiple ways (first-listed are higher priority): +==== Overrides -- Program arguments -- System properties -- Environment variables -- Environment property file -- Property file +Properties are resolved using the following precedence (highest priority first): -.application.conf -[source, properties] ----- -foo = foo ----- +1. Program arguments (e.g., `java -jar app.jar foo=bar`) +2. System properties (e.g., `-Dfoo=bar`) +3. Environment variables (e.g., `foo=bar java -jar app.jar`) +4. Environment-specific property file (e.g., `application.prod.conf`) +5. Default property file (`application.conf`) -.Property access +.Accessing Properties [source, java, role="primary"] ---- { - Environment env = getEnvironment(); <1> - Config conf = env.getConfig(); <2> - System.out.println(conf.getString("foo")); <3> + Environment env = getEnvironment(); // <1> + Config conf = env.getConfig(); // <2> + System.out.println(conf.getString("foo")); // <3> } ---- @@ -115,101 +69,41 @@ foo = foo [source, kotlin, role="secondary"] ---- { - val env = environment <1> - val conf = env.config <2> - println(conf.getString("foo")) <3> + val env = environment // <1> + val conf = env.config // <2> + println(conf.getString("foo")) // <3> } ---- -<1> Get environment -<2> Get configuration -<3> Get `foo` property and prints `foo` - -At runtime you can override properties using: - -.Program argument -[source, bash] ----- -java -jar myapp.jar foo=argument ----- - -Example prints: `argument` - -.System property -[source, bash] ----- -java -Dfoo=sysprop -jar myapp.jar ----- - -Prints: `syspro` +<1> Retrieve the current environment. +<2> Access the underlying `Config` object. +<3> Extract the value for the key `foo`. -.Environment variable -[source, bash] ----- -foo=envar java -jar myapp.jar ----- - -Prints: `envar` +==== Multi-Environment Configuration -If you have multiple properties to override, it is probably better to collect all them into a new file -and use active environment name to select them. +It is best practice to keep common settings in `application.conf` and override environment-specific values in separate files named `application.[env].conf`. -.Environment property file -[source, bash] +.Example Structure ---- -└── application.conf -└── application.prod.conf +└── application.conf (foo = "default", bar = "base") +└── application.prod.conf (foo = "production") ---- -.application.conf -[source, properties] ----- -foo = foo -bar = devbar ----- +Running with `java -jar myapp.jar prod` results in: +* `foo`: `"production"` (overridden) +* `bar`: `"base"` (inherited from default) -.application.prod.conf -[source, properties] ----- -bar = prodbar ----- +To activate multiple environments, separate them with commas: `java -jar app.jar prod,cloud`. -.Run with `prod` environment ----- -java -jar my.app application.env=prod ----- +==== Custom Configuration -Or just ----- -java -jar my.app prod ----- +If you want to bypass Jooby's default loading logic, you can provide custom options or instantiate the environment manually. -TIP: You only need to override the properties that changes between environment not all the properties. - -The `application.conf` defines two properties : `foo` and `bar`, while the environment property file -defines only `bar`. - -For Multiple environment activation you need to separate them with `,` (comma): - -.Run with `prod` and `cloud` environment ----- - java -jar my.app application.env=prod,cloud ----- - -=== Custom environment - -Custom configuration and environment are available too using: - -- The javadoc:EnvironmentOptions[] class, or -- Direct instantiation of the javadoc:Environment[] class - -.Environment options +.Using Environment Options [source,java,role="primary"] ---- { - Environment env = setEnvironmentOptions( - new EnvironmentOptions().setFilename("myapp.conf")); <1> - ) + setEnvironmentOptions(new EnvironmentOptions().setFilename("myapp.conf")); // <1> } ---- @@ -217,26 +111,21 @@ Custom configuration and environment are available too using: [source,kotlin,role="secondary"] ---- { - val env = environmentOptions { <1> - filename = "myapp.conf" + environmentOptions { + filename = "myapp.conf" // <1> } } ---- -<1> Load `myapp.conf` using the loading and precedence mechanism described before - -The javadoc:Jooby[setEnvironmentOptions, io.jooby.EnvironmentOptions] method loads, set and returns -the environment. - -To skip/ignore Jooby loading and precedence mechanism, just instantiate and set the environment: +<1> Loads `myapp.conf` instead of the default `application.conf` while maintaining standard precedence rules. -.Direct instantiation +.Direct Instantiation [source,java,role="primary"] ---- { - Config conf = ConfigFactory.load("myapp.conf"); <1> - Environment env = new Environment(getClassLoader(), conf); <2> - setEnvironment(env); <3> + Config conf = ConfigFactory.load("custom.conf"); // <1> + Environment env = new Environment(getClassLoader(), conf); // <2> + setEnvironment(env); // <3> } ---- @@ -244,149 +133,49 @@ To skip/ignore Jooby loading and precedence mechanism, just instantiate and set [source,kotlin,role="secondary"] ---- { - val conf = ConfigFactory.load("myapp.conf") <1> - val env = Environment(classLoader, conf) <2> - environment = env <3> + val conf = ConfigFactory.load("custom.conf") // <1> + val env = Environment(classLoader, conf) // <2> + environment = env // <3> } ---- -<1> Loads and parses configuration -<2> Create a new environment with configuration and (optionally) active names -<3> Set environment on Jooby instance +<1> Manually load a configuration file. +<2> Wrap it in a Jooby Environment. +<3> Assign it to the application before startup. -IMPORTANT: Custom configuration is very flexible. You can reuse Jooby mechanism or provide your own. -The only thing to keep in mind is that environment setting must be done at very early stage, before -starting the application. +==== Logging -=== Logging +Jooby uses **SLF4J**, allowing you to plug in your preferred logging framework. -Jooby uses https://www.slf4j.org[Slf4j] for logging which give you some flexibility for choosing -the logging framework. +===== Logback -==== Logback +1. **Add Dependency:** `logback-classic`. +2. **Configure:** Place `logback.xml` in your `conf` directory or classpath root. -The https://logback.qos.ch/manual/index.html[Logback] is probably the first alternative for -https://www.slf4j.org[Slf4j] due its natively implements the SLF4J API. Follow the next steps to use -logback in your project: +===== Log4j2 -1) Add dependency +1. **Add Dependencies:** `log4j-slf4j-impl` and `log4j-core`. +2. **Configure:** Place `log4j2.xml` in your `conf` directory or classpath root. -[dependency, artifactId="logback-classic"] +===== Environment-Aware Logging -2) Creates a `logback.xml` file in the `conf` directory: - -.logback.xml -[source, xml] ----- - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - ----- - -That's all! https://www.slf4j.org[Slf4j] is going to redirect log message to logback. - -==== Log4j2 - -The https://logging.apache.org/log4j[Log4j2] project is another good alternative for logging. Follow -the next steps to use logback in your project: - -1) Add dependencies - -[dependency, artifactId="log4j-slf4j-impl, log4j-core"] - -2) Creates a `log4j.xml` file in the `conf` directory: - -.log4j.xml -[source, xml] ----- - - - - - - - - - - - - - ----- - -All these extensions are considered valid: `.xml`, `.propertines`, `.yaml` and `.json`. As well as `log4j2` for file name. - -==== Environment logging - -Logging is integrated with the environment names. So it is possible to have a file name: - -- `logback[.name].xml` (for loggback) -- `log4j[.name].xml` (for log4j2) - -Jooby favors the environment specific logging configuration file over regular/normal logging configuration file. - -.Example -[source, bash] ----- -conf -└── logback.conf -└── logback.prod.conf ----- - -To use `logback.prod.conf`, start your application like: - -`java -jar myapp.jar application.env=prod` +Logging is also environment-aware. Jooby will look for `logback.[env].xml` or `log4j2.[env].xml` and favor them over the default files. [IMPORTANT] ==== -The logging configuration file per environment works as long you don't use *static* loggers -before application has been start it. The next example won't work: - -[source, java] ----- -public class App extends Jooby { - private static final Logger log = ... - - public static void main(String[] args) { - runApp(args, App::new); - } -} ----- - -The `runApp` method is the one who configures the logging framework. Adding a static logger force -the logging framework to configure without taking care the environment setup. - -There are a couple of solution is for this: - -- use an instance logger -- use the getLog() method of Jooby +To ensure environment-specific logging works correctly, avoid using **static** loggers in your main App class before `runApp` is called. Static loggers force the logging framework to initialize before Jooby can apply the environment-specific configuration. Use an instance logger or Jooby's `getLog()` method instead. ==== -=== Application Properties +==== Application Properties -These are the application properties that Jooby uses: - -[options="header"] |=== -|Property name | Description | Default value -|application.charset | Charset used by your application. Used by template engine, HTTP encoding/decoding, database driver, etc. | `UTF-8` -|application.env | The active <> names. Use to identify `dev` vs `non-dev` application deployment. Jooby applies some optimizations for `non-dev`environments | `dev` -|application.lang | The languages your application supports. Used by `Context.locale()` | A single locale provided by `Locale.getDefault()`. -|application.logfile | The logging configuration file your application uses. You don't need to set this property, see <>. | -|application.package | The base package of your application. | -|application.pid | JVM process ID. | The native process ID assigned by the operating system. -|application.startupSummary | The level of information logged during startup. | -|application.tmpdir | Temporary directory used by your application. | `tmp` +|Property | Description | Default + +|`application.charset` | Charset for encoding/decoding and templates. | `UTF-8` +|`application.env` | Active environment names. Jooby optimizes performance for non-`dev` environments. | `dev` +|`application.lang` | Supported languages for `Context.locale()`. | `Locale.getDefault()` +|`application.tmpdir` | Temporary directory for the application. | `tmp` +|`application.pid` | The JVM process ID. | System assigned |=== -See javadoc:AvailableSettings[] for more details. +See javadoc:AvailableSettings[] for a complete reference. diff --git a/docs/asciidoc/context.adoc b/docs/asciidoc/context.adoc index 47f5921248..45a12a64a6 100644 --- a/docs/asciidoc/context.adoc +++ b/docs/asciidoc/context.adoc @@ -1,14 +1,14 @@ -== Context +=== Context -A javadoc:Context[Context] allows you to interact with the HTTP Request and manipulate the HTTP Response. +A javadoc:Context[Context] allows you to interact with the HTTP request and manipulate the HTTP response. -In most of the cases you can access the context object as a parameter of your route handler: +In most cases, you access the context object as a parameter of your route handler: .Java [source, java, role="primary"] ---- { - get("/", ctx -> { /* do important stuff with variable 'ctx'. */ }); + get("/", ctx -> { /* do important stuff with the 'ctx' variable */ }); } ---- @@ -16,23 +16,15 @@ In most of the cases you can access the context object as a parameter of your ro [source, kotlin, role="secondary"] ---- { - get("/") { /* variable 'it' holds the context now. */ } + get("/") { /* the 'it' variable (or implicit ctx) holds the context */ } } ---- -javadoc:Context[Context] also provides derived information about the current request such as a -matching locale (or locales) based on the `Accept-Language` header (if presents). You may use -the result of javadoc:Context[locale] or javadoc:Context[locales] to present content matching to -the user's language preference. +javadoc:Context[Context] also provides derived information about the current request, such as matching locales based on the `Accept-Language` header. You can use javadoc:Context[locale] or javadoc:Context[locales] to present content matching the user's language preference. -The above methods use `Locale.lookup(...)` and `Locale.filter(...)` respectively to perform the -language tag matching. See their overloads if you need to plug in your own matching strategy. +These methods use `Locale.lookup(...)` and `Locale.filter(...)` to perform language tag matching. (See their overloads if you need to plug in a custom matching strategy). -To leverage language matching however, you need to tell Jooby which languages your application -supports. This can be done by either setting the `application.lang` configuration property -to a value compatible with the -https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language[Accept-Language] -header: +To leverage language matching, you must tell Jooby which languages your application supports. Set the `application.lang` configuration property to a value compatible with the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language[Accept-Language] header: .application.conf [source, properties] @@ -40,8 +32,7 @@ header: application.lang = en, en-GB, de ---- -or calling the javadoc:Jooby[setLocales, java.util.List] -or javadoc:Jooby[setLocales, java.util.Locale...] method at runtime: +Or configure it programmatically using javadoc:Jooby[setLocales, java.util.List]: .Java [source, java, role="primary"] @@ -59,24 +50,17 @@ or javadoc:Jooby[setLocales, java.util.Locale...] method at runtime: } ---- -If you don't set the supported locales explicitly, Jooby uses a single locale provided by -`Locale.getDefault()`. +If you don't explicitly set the supported locales, Jooby falls back to a single locale provided by `Locale.getDefault()`. -=== Parameters +==== Parameters -There are several parameter types: `header`, `cookie`, `path`, `query`, `form`, `multipart`, -`session` and `flash`. All them share a unified/type-safe API for accessing and manipulating their values. +There are several parameter types: `header`, `cookie`, `path`, `query`, `form`, `multipart`, `session`, and `flash`. They all share a unified, type-safe API for accessing and manipulating their values. -We are going to describe them briefly in the next sections, then go into specific features of the -<>. +This section covers how to extract raw parameters. The next section covers how to convert them into complex objects using the <>. -There is also a <> feature by which you can access a parameter from any combination -of the above types with well-defined priority. +===== Header -==== Header - -HTTP headers allow the client and the server to pass additional information with the request or the -response. +HTTP headers allow the client and server to pass additional information. .Java [source, java, role="primary"] @@ -84,13 +68,10 @@ response. { get("/", ctx -> { String token = ctx.header("token").value(); // <1> - Value headers = ctx.headers(); // <2> - Map headerMap = ctx.headerMap(); // <3> - ... + // ... }); - } ---- @@ -100,24 +81,20 @@ response. { get("/") { ctx -> val token = ctx.header("token").value() // <1> - val headers = ctx.headers() // <2> - - val headerMap = ctx.headerMap(); // <3> - ... + val headerMap = ctx.headerMap() // <3> + // ... } - } ---- -<1> Header variable `token` -<2> All headers as javadoc:value.Value[] -<3> All headers as map +<1> Retrieves the header `token`. +<2> Retrieves all headers as a javadoc:value.Value[]. +<3> Retrieves all headers as a Map. -==== Cookie +===== Cookie -Request cookies are send to the server using the `Cookie` header, but we do provide a simple -`key/value` access to them: +Request cookies are sent to the server via the `Cookie` header. Jooby provides simple key/value access: .Cookies [source, java, role="primary"] @@ -125,11 +102,9 @@ Request cookies are send to the server using the `Cookie` header, but we do prov { get("/", ctx -> { String token = ctx.cookie("token").value(); // <1> - Map cookieMap = ctx.cookieMap(); // <2> - ... + // ... }); - } ---- @@ -139,34 +114,28 @@ Request cookies are send to the server using the `Cookie` header, but we do prov { get("/") { ctx -> val token = ctx.cookie("token").value() // <1> - val cookieMap = ctx.cookieMap() // <2> - ... + // ... } - } ---- -<1> Cookie variable `token` -<2> All cookies as map +<1> Retrieves the cookie named `token`. +<2> Retrieves all cookies as a Map. -==== Path +===== Path -Path parameter are part of the `URI`. To define a path variable you need to use the `{identifier}` notation. +Path parameters are part of the URI. Use the `{identifier}` notation to define a path variable. .Syntax: [source,java,role="primary"] ---- { - get("/{id}" ctx -> ctx.path("id").value()); // <1> - - get("/@{id}" ctx -> ctx.path("id").value()); // <2> - - get("/file/{name}.{ext}", ctx -> cxt.path("name") + "." + ctx.path("ext")); // <3> - - get("/file/*", ctx -> ctx.path("*")) // <4> - - get("/{id:[0-9]+}", ctx -> ctx.path("id)) // <5> + get("/{id}", ctx -> ctx.path("id").value()); // <1> + get("/@{id}", ctx -> ctx.path("id").value()); // <2> + get("/file/{name}.{ext}", ctx -> ctx.path("name") + "." + ctx.path("ext")); // <3> + get("/file/*", ctx -> ctx.path("*")); // <4> + get("/{id:[0-9]+}", ctx -> ctx.path("id")); // <5> } ---- @@ -175,37 +144,29 @@ Path parameter are part of the `URI`. To define a path variable you need to use ---- { get("/{id}") { ctx -> ctx.path("id").value() } // <1> - get("/@{id}") { ctx -> ctx.path("id").value() } // <2> - - get("/file/{name}.{ext}") { ctx -> cxt.path("name") + "." + ctx.path("ext") } // <3> - + get("/file/{name}.{ext}") { ctx -> ctx.path("name") + "." + ctx.path("ext") } // <3> get("/file/*") { ctx -> ctx.path("*") } // <4> - - get("/{id:[0-9]+}") { ctx -> ctx.path("id) } // <5> + get("/{id:[0-9]+}") { ctx -> ctx.path("id") } // <5> } ---- -<1> Path variable `id` -<2> Path variable `id` prefixed with `@` -<3> Multiple variables `name` and `ext` -<4> Unnamed catchall path variable -<5> Path variable with a regular expression +<1> Standard path variable `id`. +<2> Path variable `id` prefixed with `@`. +<3> Multiple variables: `name` and `ext`. +<4> Unnamed catchall path variable. +<5> Path variable strictly matching a regular expression. -.Java +.Accessing Path Variables [source, java, role="primary"] ---- { get("/{name}", ctx -> { String pathString = ctx.getRequestPath(); // <1> - Value path = ctx.path(); // <2> - Map pathMap = ctx.pathMap(); // <3> - String name = ctx.path("name").value(); // <4> - - ... + // ... }); } ---- @@ -216,45 +177,22 @@ Path parameter are part of the `URI`. To define a path variable you need to use { get("/{name}") { ctx -> val pathString = ctx.getRequestPath() // <1> - val path = ctx.path() // <2> - val pathMap = ctx.pathMap() // <3> - val name = ctx.path("name").value() // <4> - - ... + // ... } } ---- -<1> Access to the `raw` path string: - -- `/a+b` => `/a+b` -- `/a b` => `/a%20b` (not decoded) -- `/%2F%2B` => `/%2F%2B` (not decoded) - -<2> Path as javadoc:value.Value[] object: - -- `/a+b` => `{name=a+b}` -- `/a b` => `{name=a b}` (decoded) -- `/%2F%2B` => `{name=/+}` (decoded) - -<3> Path as `Map` object: - -- `/a+b` => `{name=a+b}` -- `/a b` => `{name=a b}` (decoded) -- `/%2F%2B` => `{name=/+}` (decoded) +<1> Access the `raw` path string (e.g., `/a b` returns `/a%20b`). +<2> Path as a javadoc:value.Value[] object (decoded). +<3> Path as a `Map` (decoded). +<4> Specific path variable `name` as a `String` (decoded). -<4> Path variable `name` as `String`: +===== Query -- `/a+b` => `a+b` -- `/a b` => `a b` (decoded) -- `/%2F%2B` => `/+` (decoded) - -==== Query - -Query String is part of the `URI` that start after the `?` character. +The query string is the part of the URI that starts after the `?` character. .Java [source, java, role="primary"] @@ -262,26 +200,17 @@ Query String is part of the `URI` that start after the `?` character. { get("/search", ctx -> { String queryString = ctx.queryString(); // <1> - QueryString query = ctx.query(); // <2> - Map> queryMap = ctx.queryMultimap(); // <3> - String q = ctx.query("q").value(); // <4> - SearchQuery searchQuery = ctx.query(SearchQuery.class); // <5> - - ... + // ... }); } class SearchQuery { - public final String q; - - public SearchQuery(String q) { - this.q = q; - } + public SearchQuery(String q) { this.q = q; } } ---- @@ -291,57 +220,26 @@ class SearchQuery { { get("/search") { ctx -> val queryString = ctx.queryString() // <1> - val query = ctx.query() // <2> - val queryMap = ctx.queryMultimap() // <3> - val q = ctx.query("q").value() // <4> - val searchQuery = ctx.query() // <5> - - ... + // ... } } -data class SearchQuery (val q: String) +data class SearchQuery(val q: String) ---- -<1> Access to `raw` queryString: - -- `/search` => `""` (empty) -- `/search?q=a+b` => `?q=a+b` -- `/search?q=a b` => `?q=a%20b` (not decoded) +<1> Access the `raw` query string (e.g., `?q=a%20b`). +<2> Query string as a javadoc:QueryString[] object (e.g., `{q=a b}`). +<3> Query string as a multi-value map (e.g., `{q=[a b]}`). +<4> Access decoded variable `q`. Throws a `400 Bad Request` if missing. +<5> Binds the query string directly to a `SearchQuery` object. -<2> Query String as javadoc:QueryString[] object: +===== Formdata -- `/search` => `{}` (empty) -- `/search?q=a+b` => `{q=a+b}` -- `/search?q=a b` => `{q=a b}` (decoded) - -<3> Query string as `multi-value map` - -- `/search` => `{}` (empty) -- `/search?q=a+b` => `{q=[a+b]}` -- `/search?q=a b` => `{q=[a b]}` (decoded) - -<4> Access to decoded variable `q`: - -- `/search` => `Bad Request (400). Missing value: "q"` -- `/search?q=a+b` => `a+b` -- `/search?q=a b` => `a b` (decoded) - -<5> Query string as `SearchQuery` - -- `/search` => `Bad Request (400). Missing value: "q"` -- `/search?q=a+b` => `SearchQuery(q="a+b")` -- `/search?q=a b` => `SearchQuery(q="a b")` (decoded) - -==== Formdata - -Formdata is expected to be in HTTP body, or for as part of the `URI` for `GET` requests. - -Data is expected to be encoded as `application/x-www-form-urlencoded`. +Form data is sent in the HTTP body (or as part of the URI for `GET` requests) and is encoded as `application/x-www-form-urlencoded`. .Java [source, java, role="primary"] @@ -349,25 +247,17 @@ Data is expected to be encoded as `application/x-www-form-urlencoded`. { post("/user", ctx -> { Formdata form = ctx.form(); // <1> - Map> formMap = ctx.formMultimap(); // <2> - String userId = ctx.form("id").value(); // <3> - String pass = ctx.form("pass").value(); // <4> - User user = ctx.form(User.class); // <5> - - ... + // ... }); } class User { - public final String id; - public final String pass; - public User(String id, String pass) { this.id = id; this.pass = pass; @@ -381,36 +271,26 @@ class User { { post("/user") { ctx -> val form = ctx.form() // <1> - val formMap = ctx.formMultimap() // <2> - val userId = ctx.form("id").value() // <3> - val pass = ctx.form("pass").value() // <4> - val user = ctx.form() // <5> - - ... + // ... } } -data class User (val id: String, val pass: String) - +data class User(val id: String, val pass: String) ---- ----- -curl -d "id=root&pass=pwd" -X POST http://localhost:8080/user ----- +<1> Form as javadoc:Formdata[]. +<2> Form as a multi-value map. +<3> Specific form variable `id`. +<4> Specific form variable `pass`. +<5> Form automatically bound to a `User` object. -<1> Form as javadoc:Formdata[] => `{id=root, pass=pwd}` -<2> Form as `multi-value map` => `{id=root, pass=[pwd]}` -<3> Form variable `id` => `root` -<4> Form variable `pass` => `pwd` -<5> Form as `User` object => `User(id=root, pass=pwd)` +===== Multipart & File Uploads -==== Multipart - -Form-data must be present in the HTTP body and encoded as `multipart/form-data`: +Multipart data is sent in the HTTP body and encoded as `multipart/form-data`. It is required for file uploads. .Java [source, java, role="primary"] @@ -418,29 +298,19 @@ Form-data must be present in the HTTP body and encoded as `multipart/form-data`: { post("/user", ctx -> { Multipart multipart = ctx.multipart(); // <1> - - Map multipartMap = ctx.multipartMultimap(); // <2> - + Map> multipartMap = ctx.multipartMultimap(); // <2> String userId = ctx.multipart("id").value(); // <3> - String pass = ctx.multipart("pass").value(); // <4> - FileUpload pic = ctx.file("pic"); // <5> - User user = ctx.multipart(User.class); // <6> - - ... + // ... }); } class User { - public final String id; - public final String pass; - public final FileUpload pic; - public User(String id, String pass, FileUpload pic) { this.id = id; this.pass = pass; @@ -455,48 +325,35 @@ class User { { post("/user") { ctx -> val multipart = ctx.multipart() // <1> - val multipartMap = ctx.multipartMultimap() // <2> - val userId = ctx.multipart("id").value() // <3> - val pass = ctx.multipart("pass").value() // <4> - val pic = ctx.file("pic") // <5> - val user = ctx.multipart() // <6> - - ... + // ... } } -data class User (val id: String, val pass: String, val pic: FileUpload) ----- - ----- -curl -F id=root -F pass=root -F pic=@/path/to/local/file/profile.png http://localhost:8080/user +data class User(val id: String, val pass: String, val pic: FileUpload) ---- -<1> Form as javadoc:Multipart[] => `{id=root, pass=pwd, pic=profile.png}` -<2> Form as `multi-value map` => `{id=root, pass=[pwd]}` -<3> Form variable `id` => `root` -<4> Form variable `pass` => `pwd` -<5> javadoc:FileUpload[] variable `pic` -<6> Form as `User` object => `User(id=root, pass=pwd, pic=profile.png)` +<1> Form as javadoc:Multipart[]. +<2> Form as a multi-value map. +<3> Specific multipart text variable `id`. +<4> Specific multipart text variable `pass`. +<5> Single file upload named `pic`. +<6> Multipart form bound to a `User` object (including the file). [NOTE] .File Upload ==== - -File upload are available ONLY for multipart requests. +File uploads are **only** available for multipart requests. .Java [source,java,role="primary"] ---- FileUpload pic = ctx.file("pic"); // <1> - - List pic = ctx.files("pic"); // <2> - + List pics = ctx.files("pic"); // <2> List files = ctx.files(); // <3> ---- @@ -504,58 +361,50 @@ File upload are available ONLY for multipart requests. [source,kotlin,role="secondary"] ---- val pic = ctx.file("pic") // <1> - - val pic = ctx.files("pic") // <2> - + val pics = ctx.files("pic") // <2> val files = ctx.files() // <3> ---- <1> Single file upload named `pic` <2> Multiple file uploads named `pic` <3> All file uploads - ==== -==== Session +===== Session -Session parameters are available via javadoc:Context[session] or javadoc:Context[sessionOrNull] -methods. HTTP Session is covered in his own <>, but here is a quick look: +Session parameters are available via javadoc:Context[session] or javadoc:Context[sessionOrNull]. (See the full <> for details). .Java [source,java,role="primary"] ---- Session session = ctx.session(); // <1> - String attribute = ctx.session("attribute").value(); // <2> - ---- .Kotlin [source,kotlin,role="secondary"] ---- val session = ctx.session() // <1> - val attribute = session.attribute("attribute").value() // <2> ---- -<1> Find an existing Session or create one -<2> Get a session attribute +<1> Finds an existing Session or creates a new one. +<2> Gets a specific session attribute. -==== Flash +===== Flash -Flash parameters are designed to transport success/error messages between requests. It is similar to -a javadoc:Session[] but the lifecycle is shorter: *data is kept for only one request*. +Flash parameters transport success/error messages between requests. They are similar to a session, but their lifecycle is shorter: **data is kept for only one request**. .Java [source,java,role="primary"] ---- get("/", ctx -> { - return ctx.flash("success").value("Welcome!"); <3> + return ctx.flash("success").value("Welcome!"); // <3> }); post("/save", ctx -> { - ctx.flash().put("success", "Item created"); <1> - return ctx.sendRedirect("/"); <2> + ctx.flash().put("success", "Item created"); // <1> + return ctx.sendRedirect("/"); // <2> }); ---- @@ -563,28 +412,26 @@ a javadoc:Session[] but the lifecycle is shorter: *data is kept for only one req [source,kotlin,role="secondary"] ---- get("/") { ctx -> - ctx.flash("success").value("Welcome!") <3> + ctx.flash("success").value("Welcome!") // <3> } post("/save") { ctx -> - ctx.flash().put("success", "Item created") <1> - ctx.sendRedirect("/") <2> + ctx.flash().put("success", "Item created") // <1> + ctx.sendRedirect("/") // <2> } ---- -<1> Set a flash attribute: `success` -<2> Redirect to home page -<3> Display an existing flash attribute `success` or shows `Welcome!` +<1> Sets a flash attribute: `success`. +<2> Redirects to the home page. +<3> Displays the flash attribute `success` (if it exists) or defaults to `Welcome!`. -Flash attributes are implemented using an `HTTP Cookie`. To customize the cookie -(its name defaults to `jooby.flash`) use the javadoc:Router[setFlashCookie, io.jooby.Cookie] method: +Flash attributes are implemented using an HTTP Cookie. To customize the cookie (the default name is `jooby.flash`), use javadoc:Router[setFlashCookie, io.jooby.Cookie]. .Java [source,java,role="primary"] ---- { setFlashCookie(new Cookie("myflash").setHttpOnly(true)); - // or if you're fine with the default name getFlashCookie().setHttpOnly(true); } @@ -595,16 +442,14 @@ Flash attributes are implemented using an `HTTP Cookie`. To customize the cookie ---- { flashCookie = Cookie("myflash").setHttpOnly(true) - // or if you're fine with the default name flashCookie.isHttpOnly = true } ---- -==== Parameter Lookup +===== Parameter Lookup -You can search for parameters in multiple sources with an explicitly defined priority using the -javadoc:Context[lookup] or javadoc:Context[lookup, java.lang.String, io.jooby.ParamSource...] method: +You can search for parameters across multiple sources with an explicitly defined priority using javadoc:Context[lookup]. .Java [source,java,role="primary"] @@ -634,21 +479,18 @@ get("/{foo}") { ctx -> } ---- -In case of a request like `/bar?foo=baz`, `foo is: baz` will be returned since the query parameter -takes precedence over the path parameter. +If a request is made to `/bar?foo=baz`, the result will be `foo is: baz` because the query parameter takes precedence over the path parameter. -==== Client Certificates +===== Client Certificates -If mutual TLS is enabled, you can access the client's certificates from the context. The first -certificate in the list is the peer certificate, followed by the ca certificates in the chain -(the order is preserved). +If mutual TLS is enabled, you can access the client's certificates from the context. The first certificate in the list is the peer certificate, followed by the CA certificates in the chain. .Java [source,java,role="primary"] ---- get("/{foo}", ctx -> { - List certificates = ctx.getClientCertificates(); <1> - Certificate peerCertificate = certificates.get(0); <2> + List certificates = ctx.getClientCertificates(); // <1> + Certificate peerCertificate = certificates.get(0); // <2> }); ---- @@ -656,12 +498,12 @@ get("/{foo}", ctx -> { [source,kotlin,role="secondary"] ---- get("/{foo}") { ctx -> - val certificates = ctx.clientCertificates <1> - val peerCertificate = certificates.first() <2> + val certificates = ctx.clientCertificates // <1> + val peerCertificate = certificates.first() // <2> } ---- -<1> Get all of the certificates presented by the client during the SSL handshake. +<1> Get all certificates presented by the client during the SSL handshake. <2> Get only the peer certificate. include::value-api.adoc[] diff --git a/docs/asciidoc/core.adoc b/docs/asciidoc/core.adoc new file mode 100644 index 0000000000..68e2681b84 --- /dev/null +++ b/docs/asciidoc/core.adoc @@ -0,0 +1,13 @@ +== Core + +include::routing.adoc[] + +include::context.adoc[] + +include::responses.adoc[] + +include::handlers.adoc[] + +include::error-handler.adoc[] + +include::execution-model.adoc[] diff --git a/docs/asciidoc/dev-tools.adoc b/docs/asciidoc/dev-tools.adoc index 8fc01fa288..88d39c8038 100644 --- a/docs/asciidoc/dev-tools.adoc +++ b/docs/asciidoc/dev-tools.adoc @@ -1,49 +1,35 @@ -== Development +=== Development -The `jooby run` tool allows to restart your application on code changes without exiting the JVM. +The `jooby run` tool provides a "hot reload" experience by restarting your application automatically whenever code changes are detected, without exiting the JVM. This makes Java and Kotlin development feel as fast and iterative as a scripting language. -This feature is also known as hot reload/swap. Makes you feel like coding against a script -language where you modify your code and changes are visible immediately. - -The tool uses the https://jboss-modules.github.io/jboss-modules/manual[JBoss Modules] library -that effectively reload application classes. +The tool leverages https://jboss-modules.github.io/jboss-modules/manual[JBoss Modules] to efficiently reload application classes. It is available as both a Maven and a Gradle plugin. -For now `jooby run` is available as Maven and Gradle plugins. +==== Usage -=== Usage - -1) Add build plugin: +1) Add the build plugin: .pom.xml [source, xml, role = "primary", subs="verbatim,attributes"] ---- - ... io.jooby jooby-maven-plugin {joobyVersion} - ... ---- .build.gradle [source, gradle, role = "secondary", subs="verbatim,attributes"] ---- -buildscript { - ext { - joobyVersion = "{joobyVersion}" - } -} - plugins { id "application" id "io.jooby.run" version "{joobyVersion}" } ---- -2) Set main class +2) Configure the Main Class: .pom.xml [source, xml, role = "primary"] @@ -59,7 +45,7 @@ plugins { mainClassName = "myapp.App" ---- -3) Run application +3) Launch the Application: .Maven [source, bash, role = "primary"] @@ -73,77 +59,57 @@ mvn jooby:run ./gradlew joobyRun ---- -=== Compilation & Restart - -Changing a `java` or `kt` file triggers a compilation request. Compilation is executed by -Maven/Gradle using an incremental build process. - -If compilation succeed, application is restarted. - -Compilation errors are printed to the console by Maven/Gradle. +==== Compilation and Restart -Changing a `.conf`, `.properties` file triggers just an application restart request. They don't trigger -a compilation request. +* **Source Files (`.java`, `.kt`):** Changing a source file triggers an incremental compilation request. If the compilation succeeds, the application restarts automatically. +* **Configuration Files (`.conf`, `.properties`):** Changes to these files trigger an immediate application restart without a compilation step. +* **Compilation Errors:** Any errors during the build process are printed directly to the console by Maven or Gradle. -Compiler is enabled by default, except for Eclipse users. Plugin checks for `.classpath` file in -project directory, when found plugin compiler is OFF and let Eclipse compiles the code. +[NOTE] +==== +For **Eclipse** users: The plugin detects the `.classpath` file in your project directory. If found, the plugin's internal compiler is disabled, letting Eclipse handle the compilation while the plugin focuses on the restart logic. +==== -=== Options +==== Options -The next example shows all the available options with their default values: +Below are the available configuration options with their default values: .pom.xml [source, xml, role = "primary", subs="verbatim,attributes"] ---- - - ... - - io.jooby - jooby-maven-plugin - {joobyVersion} - - ${application.class} <1> - conf,properties,class <2> - java,kt <3> - 8080 <4> - 500 <5> - false <6> - - - ... - + + ${application.class} + conf,properties,class + java,kt + 8080 + 500 + false + ---- .build.gradle [source, gradle, role = "secondary", subs="verbatim,attributes"] ---- -buildscript { - ext { - joobyVersion = "{joobyVersion}" - } -} - -plugins { - id "application" - id "io.jooby.run" version "{joobyVersion}" -} - joobyRun { - mainClassName = "${mainClassName}" <1> - restartExtensions = ["conf", "properties", "class"] <2> - compileExtensions = ["java", "kt"] <3> - port = 8080 <4> - waitTimeBeforeRestart = 500 <5> - useSingleClassLoader = false <6> + mainClassName = "${mainClassName}" // <1> + restartExtensions = ["conf", "properties", "class"] // <2> + compileExtensions = ["java", "kt"] // <3> + port = 8080 // <4> + waitTimeBeforeRestart = 500 // <5> + useSingleClassLoader = false // <6> } ---- -<1> Application main class -<2> Restart extensions. A change on these files trigger a restart request. -<3> Source extensions. A change on these files trigger a compilation request, followed by a restart request. -<4> Application port -<5> How long to wait after last file change to restart. Default is: `500` milliseconds. -<6> Use a single/fat class loader to run your application. This is required on complex project classpath where you start seeing weird reflection errors. This was the default mode in Jooby 2.x. The new model since 3.x uses a modular classloader which improves restart times and memory usage making it faster. Default is: `false`. +<1> The application's entry point (Main class). +<2> Extensions that trigger an immediate restart. +<3> Extensions that trigger a compilation followed by a restart. +<4> The local development port. +<5> The delay (in milliseconds) to wait after the last file change before restarting. Default is `500ms`. +<6> If `true`, Jooby uses a single "fat" classloader. Set this to `true` if you encounter strange reflection or class-loading errors in complex projects. Since 3.x, Jooby uses a modular classloader by default for faster restarts and lower memory usage. + +==== Testing with Classpath + +To run the application while including the `test` scope/source set in the classpath, use the following commands: -For Maven and Gradle there are two variant `mvn jooby:testRun` and `./gradlew joobyTestRun` they work -by expanding the classpath to uses the `test` scope or source set. +* **Maven:** `mvn jooby:testRun` +* **Gradle:** `./gradlew joobyTestRun` diff --git a/docs/asciidoc/docinfo-footer.html b/docs/asciidoc/docinfo-footer.html index 52f25902ee..0305aa8497 100644 --- a/docs/asciidoc/docinfo-footer.html +++ b/docs/asciidoc/docinfo-footer.html @@ -12,43 +12,7 @@ }); }); - - - - +