cookie = Optional.empty();
+
+ private String method = "*";
+
+ private String path = "*";
+
+ /**
+ * Creates a new {@link FlashScope} and customize the flash cookie.
+ *
+ * @param cookie Cookie template for flash scope.
+ */
+ public FlashScope(final Cookie.Definition cookie) {
+ this.cookie = Optional.of(requireNonNull(cookie, "Cookie required."));
+ }
+
+ /**
+ * Creates a new {@link FlashScope}.
+ */
+ public FlashScope() {
+ }
+
+ @Override
+ public void configure(final Env env, final Config conf, final Binder binder) {
+ Config $cookie = conf.getConfig("flash.cookie");
+ String cpath = $cookie.getString("path");
+ boolean chttp = $cookie.getBoolean("httpOnly");
+ boolean csecure = $cookie.getBoolean("secure");
+ Cookie.Definition cookie = this.cookie
+ .orElseGet(() -> new Cookie.Definition($cookie.getString("name")));
+
+ // uses user provided or fallback to defaults
+ cookie.path(cookie.path().orElse(cpath))
+ .httpOnly(cookie.httpOnly().orElse(chttp))
+ .secure(cookie.secure().orElse(csecure));
+
+ env.router()
+ .use(method, path, new FlashScopeHandler(cookie, decoder, encoder))
+ .name("flash-scope");
+ }
+
+}
diff --git a/jooby/src/main/java/org/jooby/Jooby.java b/jooby/src/main/java/org/jooby/Jooby.java
new file mode 100644
index 00000000..0abfb177
--- /dev/null
+++ b/jooby/src/main/java/org/jooby/Jooby.java
@@ -0,0 +1,3360 @@
+/*
+ * Copyright 2026 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jooby;
+
+import com.google.common.base.Joiner;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.escape.Escaper;
+import com.google.common.html.HtmlEscapers;
+import com.google.common.net.UrlEscapers;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.Stage;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.ProviderMethodsModule;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+import com.google.inject.util.Types;
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigObject;
+import com.typesafe.config.ConfigValue;
+import static com.typesafe.config.ConfigValueFactory.fromAnyRef;
+import static java.util.Objects.requireNonNull;
+import static org.jooby.Route.CONNECT;
+import static org.jooby.Route.DELETE;
+import org.jooby.Route.Definition;
+import static org.jooby.Route.GET;
+import static org.jooby.Route.HEAD;
+import org.jooby.Route.Mapper;
+import static org.jooby.Route.OPTIONS;
+import static org.jooby.Route.PATCH;
+import static org.jooby.Route.POST;
+import static org.jooby.Route.PUT;
+import static org.jooby.Route.TRACE;
+import org.jooby.Session.Store;
+import org.jooby.funzy.Throwing;
+import org.jooby.funzy.Try;
+import org.jooby.handlers.AssetHandler;
+import org.jooby.internal.AppPrinter;
+import org.jooby.internal.BuiltinParser;
+import org.jooby.internal.BuiltinRenderer;
+import org.jooby.internal.CookieSessionManager;
+import org.jooby.internal.DefaulErrRenderer;
+import org.jooby.internal.HttpHandlerImpl;
+import org.jooby.internal.JvmInfo;
+import org.jooby.internal.LocaleUtils;
+import org.jooby.internal.ParameterNameProvider;
+import org.jooby.internal.RequestScope;
+import org.jooby.internal.RouteMetadata;
+import org.jooby.internal.ServerExecutorProvider;
+import org.jooby.internal.ServerLookup;
+import org.jooby.internal.ServerSessionManager;
+import org.jooby.internal.SessionManager;
+import org.jooby.internal.SourceProvider;
+import org.jooby.internal.TypeConverters;
+import org.jooby.internal.handlers.HeadHandler;
+import org.jooby.internal.handlers.OptionsHandler;
+import org.jooby.internal.handlers.TraceHandler;
+import org.jooby.internal.mvc.MvcRoutes;
+import org.jooby.internal.mvc.MvcWebSocket;
+import org.jooby.internal.parser.BeanParser;
+import org.jooby.internal.parser.DateParser;
+import org.jooby.internal.parser.LocalDateParser;
+import org.jooby.internal.parser.LocaleParser;
+import org.jooby.internal.parser.ParserExecutor;
+import org.jooby.internal.parser.StaticMethodParser;
+import org.jooby.internal.parser.StringConstructorParser;
+import org.jooby.internal.parser.ZonedDateTimeParser;
+import org.jooby.internal.ssl.SslContextProvider;
+import org.jooby.mvc.Consumes;
+import org.jooby.mvc.Produces;
+import org.jooby.scope.Providers;
+import org.jooby.scope.RequestScoped;
+import org.jooby.spi.HttpHandler;
+import org.jooby.spi.Server;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.inject.Singleton;
+import javax.net.ssl.SSLContext;
+import java.io.File;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * jooby
+ * getting started
+ *
+ *
+ * public class MyApp extends Jooby {
+ *
+ * {
+ * use(new Jackson()); // 1. JSON serializer.
+ *
+ * // 2. Define a route
+ * get("/", req {@literal ->} {
+ * Map{@literal <}String, Object{@literal >} model = ...;
+ * return model;
+ * }
+ * }
+ *
+ * public static void main(String[] args) {
+ * run(MyApp::new, args); // 3. Done!
+ * }
+ * }
+ *
+ *
+ * application.conf
+ *
+ * Jooby delegate configuration management to TypeSafe Config .
+ *
+ *
+ *
+ * By default Jooby looks for an application.conf. If
+ * you want to specify a different file or location, you can do it with {@link #conf(String)}.
+ *
+ *
+ *
+ * TypeSafe Config uses a hierarchical model to
+ * define and override properties.
+ *
+ *
+ * A {@link Jooby.Module} might provides his own set of properties through the
+ * {@link Jooby.Module#config()} method. By default, this method returns an empty config object.
+ *
+ * For example:
+ *
+ *
+ * use(new M1());
+ * use(new M2());
+ * use(new M3());
+ *
+ *
+ * Previous example had the following order (first-listed are higher priority):
+ *
+ * arguments properties
+ * System properties
+ * application.conf
+ * M3 properties
+ * M2 properties
+ * M1 properties
+ *
+ *
+ * Command line argmuents or system properties takes precedence over any application specific
+ * property.
+ *
+ *
+ * env
+ *
+ * Jooby defines one mode or environment: dev . In Jooby, dev
+ * is special and some modules could apply special settings while running in dev .
+ * Any other env is usually considered a prod like env. But that depends on module
+ * implementor.
+ *
+ *
+ * An environment can be defined in your .conf file using the
+ * application.env property. If missing, Jooby set the env for you to
+ * dev .
+ *
+ *
+ * There is more at {@link Env} please read the {@link Env} javadoc.
+ *
+ *
+ * modules: the jump to full-stack framework
+ *
+ * {@link Jooby.Module Modules} are quite similar to a Guice modules except that the configure
+ * callback has been complementing with {@link Env} and {@link Config}.
+ *
+ *
+ *
+ * public class MyModule implements Jooby.Module {
+ * public void configure(Env env, Config config, Binder binder) {
+ * }
+ * }
+ *
+ *
+ * From the configure callback you can bind your services as you usually do in a Guice app.
+ *
+ * There is more at {@link Jooby.Module} so please read the {@link Jooby.Module} javadoc.
+ *
+ *
+ * path patterns
+ *
+ * Jooby supports Ant-style path patterns:
+ *
+ *
+ * Some examples:
+ *
+ *
+ * {@code com/t?st.html} - matches {@code com/test.html} but also {@code com/tast.html} or
+ * {@code com/txst.html}
+ * {@code com/*.html} - matches all {@code .html} files in the {@code com} directory
+ * com/{@literal **}/test.html - matches all {@code test.html} files underneath the
+ * {@code com} path
+ * {@code **}/{@code *} - matches any path at any level.
+ * {@code *} - matches any path at any level, shorthand for {@code **}/{@code *}.
+ *
+ *
+ * variables
+ *
+ * Jooby supports path parameters too:
+ *
+ *
+ * Some examples:
+ *
+ *
+ * /user/{id} - /user/* and give you access to the id var.
+ * /user/:id - /user/* and give you access to the id var.
+ * /user/{id:\\d+} - /user/[digits] and give you access to the numeric
+ * id var.
+ *
+ *
+ * routes
+ *
+ * Routes perform actions in response to a server HTTP request.
+ *
+ *
+ * Routes are executed in the order they are defined, for example:
+ *
+ *
+ *
+ * get("/", (req, rsp) {@literal ->} {
+ * log.info("first"); // start here and go to second
+ * });
+ *
+ * get("/", (req, rsp) {@literal ->} {
+ * log.info("second"); // execute after first and go to final
+ * });
+ *
+ * get("/", (req, rsp) {@literal ->} {
+ * rsp.send("final"); // done!
+ * });
+ *
+ *
+ * Previous example can be rewritten using {@link Route.Filter}:
+ *
+ *
+ * get("/", (req, rsp, chain) {@literal ->} {
+ * log.info("first"); // start here and go to second
+ * chain.next(req, rsp);
+ * });
+ *
+ * get("/", (req, rsp, chain) {@literal ->} {
+ * log.info("second"); // execute after first and go to final
+ * chain.next(req, rsp);
+ * });
+ *
+ * get("/", (req, rsp) {@literal ->} {
+ * rsp.send("final"); // done!
+ * });
+ *
+ *
+ * Due to the use of lambdas a route is a singleton and you should NOT use global variables. For
+ * example this is a bad practice:
+ *
+ *
+ * List{@literal <}String{@literal >} names = new ArrayList{@literal <}{@literal >}(); // names produces side effects
+ * get("/", (req, rsp) {@literal ->} {
+ * names.add(req.param("name").value();
+ * // response will be different between calls.
+ * rsp.send(names);
+ * });
+ *
+ *
+ * mvc routes
+ *
+ * A Mvc route use annotations to define routes:
+ *
+ *
+ *
+ * use(MyRoute.class);
+ * ...
+ *
+ * // MyRoute.java
+ * {@literal @}Path("/")
+ * public class MyRoute {
+ *
+ * {@literal @}GET
+ * public String hello() {
+ * return "Hello Jooby";
+ * }
+ * }
+ *
+ *
+ * Programming model is quite similar to JAX-RS/Jersey with some minor differences and/or
+ * simplifications.
+ *
+ *
+ *
+ * To learn more about Mvc Routes, please check {@link org.jooby.mvc.Path},
+ * {@link org.jooby.mvc.Produces} {@link org.jooby.mvc.Consumes} javadoc.
+ *
+ *
+ * static files
+ *
+ * Static files, like: *.js, *.css, ..., etc... can be served with:
+ *
+ *
+ *
+ * assets("assets/**");
+ *
+ *
+ * Classpath resources under the /assets folder will be accessible from client/browser.
+ *
+ *
+ * lifecyle
+ *
+ * We do provide {@link #onStart(Throwing.Consumer)} and {@link #onStop(Throwing.Consumer)} callbacks.
+ * These callbacks are executed are application startup or shutdown time:
+ *
+ *
+ * {@code
+ * {
+ * onStart(() -> {
+ * log.info("Welcome!");
+ * });
+ *
+ * onStop(() -> {
+ * log.info("Bye!");
+ * });
+ * }
+ * }
+ *
+ *
+ * From life cycle callbacks you can access to application services:
+ *
+ *
+ * {@code
+ * {
+ * onStart(registry -> {
+ * MyDatabase db = registry.require(MyDatabase.class);
+ * // do something with databse:
+ * });
+ * }
+ * }
+ *
+ * @author edgar
+ * @see Jooby.Module
+ * @since 0.1.0
+ */
+public class Jooby implements Router, LifeCycle, Registry {
+
+ /**
+ * {@code
+ * {
+ * on("dev", () -> {
+ * // run something on dev
+ * }).orElse(() -> {
+ * // run something on prod
+ * });
+ * }
+ * }
+ */
+ public interface EnvPredicate {
+
+ /**
+ * {@code
+ * {
+ * on("dev", () -> {
+ * // run something on dev
+ * }).orElse(() -> {
+ * // run something on prod
+ * });
+ * }
+ * }
+ *
+ * @param callback Env callback.
+ */
+ default void orElse(final Runnable callback) {
+ orElse(conf -> callback.run());
+ }
+
+ /**
+ * {@code
+ * {
+ * on("dev", () -> {
+ * // run something on dev
+ * }).orElse(conf -> {
+ * // run something on prod
+ * });
+ * }
+ * }
+ *
+ * @param callback Env callback.
+ */
+ void orElse(Consumer callback);
+
+ }
+
+ /**
+ * A module can publish or produces: {@link Route.Definition routes}, {@link Parser},
+ * {@link Renderer}, and any other application specific service or contract of your choice.
+ *
+ * It is similar to {@link com.google.inject.Module} except for the callback method receives a
+ * {@link Env}, {@link Config} and {@link Binder}.
+ *
+ *
+ *
+ * A module can provide his own set of properties through the {@link #config()} method. By
+ * default, this method returns an empty config object.
+ *
+ * For example:
+ *
+ *
+ * use(new M1());
+ * use(new M2());
+ * use(new M3());
+ *
+ *
+ * Previous example had the following order (first-listed are higher priority):
+ *
+ * System properties
+ * application.conf
+ * M3 properties
+ * M2 properties
+ * M1 properties
+ *
+ *
+ *
+ * A module can provide start/stop methods in order to start or close resources.
+ *
+ *
+ * @author edgar
+ * @see Jooby#use(Jooby.Module)
+ * @since 0.1.0
+ */
+ public interface Module {
+
+ /**
+ * @return Produces a module config object (when need it). By default a module doesn't produce
+ * any configuration object.
+ */
+ @Nonnull
+ default Config config() {
+ return ConfigFactory.empty();
+ }
+
+ /**
+ * Configure and produces bindings for the underlying application. A module can optimize or
+ * customize a service by checking current the {@link Env application env} and/or the current
+ * application properties available from {@link Config}.
+ *
+ * @param env The current application's env. Not null.
+ * @param conf The current config object. Not null.
+ * @param binder A guice binder. Not null.
+ * @throws Throwable If something goes wrong.
+ */
+ void configure(Env env, Config conf, Binder binder) throws Throwable;
+
+ }
+
+ static class MvcClass implements Route.Props {
+ Class> routeClass;
+
+ String path;
+
+ ImmutableMap.Builder attrs = ImmutableMap.builder();
+
+ private List consumes;
+
+ private String name;
+
+ private List produces;
+
+ private List excludes;
+
+ private Mapper> mapper;
+
+ private String prefix;
+
+ private String renderer;
+
+ public MvcClass(final Class> routeClass, final String path, final String prefix) {
+ this.routeClass = routeClass;
+ this.path = path;
+ this.prefix = prefix;
+ }
+
+ @Override
+ public MvcClass attr(final String name, final Object value) {
+ attrs.put(name, value);
+ return this;
+ }
+
+ @Override
+ public MvcClass name(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public MvcClass consumes(final List consumes) {
+ this.consumes = consumes;
+ return this;
+ }
+
+ @Override
+ public MvcClass produces(final List produces) {
+ this.produces = produces;
+ return this;
+ }
+
+ @Override
+ public MvcClass excludes(final List excludes) {
+ this.excludes = excludes;
+ return this;
+ }
+
+ @Override
+ public MvcClass map(final Mapper> mapper) {
+ this.mapper = mapper;
+ return this;
+ }
+
+ @Override
+ public String renderer() {
+ return renderer;
+ }
+
+ @Override
+ public MvcClass renderer(final String name) {
+ this.renderer = name;
+ return this;
+ }
+
+ public Route.Definition apply(final Route.Definition route) {
+ attrs.build().forEach(route::attr);
+ if (name != null) {
+ route.name(name);
+ }
+ if (prefix != null) {
+ route.name(prefix + "/" + route.name());
+ }
+ if (consumes != null) {
+ route.consumes(consumes);
+ }
+ if (produces != null) {
+ route.produces(produces);
+ }
+ if (excludes != null) {
+ route.excludes(excludes);
+ }
+ if (mapper != null) {
+ route.map(mapper);
+ }
+ if (renderer != null) {
+ route.renderer(renderer);
+ }
+ return route;
+ }
+ }
+
+ private static class EnvDep {
+ Predicate predicate;
+
+ Consumer callback;
+
+ public EnvDep(final Predicate predicate, final Consumer callback) {
+ this.predicate = predicate;
+ this.callback = callback;
+ }
+ }
+
+ static {
+ // set pid as system property
+ String pid = System.getProperty("pid", JvmInfo.pid() + "");
+ System.setProperty("pid", pid);
+ }
+
+ /**
+ * Keep track of routes.
+ */
+ private transient Set bag = new LinkedHashSet<>();
+
+ /**
+ * The override config. Optional.
+ */
+ private transient Config srcconf;
+
+ private final transient AtomicBoolean started = new AtomicBoolean(false);
+
+ /** Keep the global injector instance. */
+ private transient Injector injector;
+
+ /** Session store. */
+ private transient Session.Definition session = new Session.Definition(Session.Mem.class);
+
+ /** Env builder. */
+ private transient Env.Builder env = Env.DEFAULT;
+
+ /** Route's prefix. */
+ private transient String prefix;
+
+ /** startup callback . */
+ private transient List> onStart = new ArrayList<>();
+ private transient List> onStarted = new ArrayList<>();
+
+ /** stop callback . */
+ private transient List> onStop = new ArrayList<>();
+
+ /** Mappers . */
+ @SuppressWarnings("rawtypes")
+ private transient Mapper mapper;
+
+ /** Don't add same mapper twice . */
+ private transient Set mappers = new HashSet<>();
+
+ /** Bean parser . */
+ private transient Optional beanParser = Optional.empty();
+
+ private transient ServerLookup server = new ServerLookup();
+
+ private transient String dateFormat;
+
+ private transient Charset charset;
+
+ private transient String[] languages;
+
+ private transient ZoneId zoneId;
+
+ private transient Integer port;
+
+ private transient Integer securePort;
+
+ private transient String numberFormat;
+
+ private transient boolean http2;
+
+ private transient List> executors = new ArrayList<>();
+
+ private transient boolean defaultExecSet;
+
+ private boolean throwBootstrapException;
+
+ /**
+ * creates the injector
+ */
+ private transient BiFunction injectorFactory = Guice::createInjector;
+
+ private transient List apprefs;
+
+ private transient LinkedList path = new LinkedList<>();
+
+ private transient String confname;
+
+ private transient boolean caseSensitiveRouting = true;
+
+ private transient String classname;
+
+ /**
+ * Creates a new {@link Jooby} application.
+ */
+ public Jooby() {
+ this(null);
+ }
+
+ /**
+ * Creates a new application and prefix all the names of the routes with the given prefix. Useful,
+ * for dynamic/advanced routing. See {@link Route.Chain#next(String, Request, Response)}.
+ *
+ * @param prefix Route name prefix.
+ */
+ public Jooby(final String prefix) {
+ this.prefix = prefix;
+ use(server);
+ this.classname = classname(getClass().getName());
+ }
+
+ @Override
+ public Route.Collection path(String path, Runnable action) {
+ this.path.addLast(Route.normalize(path));
+ Route.Collection collection = with(action);
+ this.path.removeLast();
+ return collection;
+ }
+
+ @Override
+ public Jooby use(final Jooby app) {
+ return use(prefixPath(null), app);
+ }
+
+ private Optional prefixPath(@Nullable String tail) {
+ return path.size() == 0
+ ? tail == null ? Optional.empty() : Optional.of(Route.normalize(tail))
+ : Optional.of(path.stream()
+ .collect(Collectors.joining("", "", tail == null
+ ? "" : Route.normalize(tail))));
+ }
+
+ @Override
+ public Jooby use(final String path, final Jooby app) {
+ return use(prefixPath(path), app);
+ }
+
+ /**
+ * Use the provided HTTP server.
+ *
+ * @param server Server.
+ * @return This jooby instance.
+ */
+ public Jooby server(final Class extends Server> server) {
+ requireNonNull(server, "Server required.");
+ // remove server lookup
+ List tmp = bag.stream()
+ .skip(1)
+ .collect(Collectors.toList());
+ tmp.add(0,
+ (Module) (env, conf, binder) -> binder.bind(Server.class).to(server).asEagerSingleton());
+ bag.clear();
+ bag.addAll(tmp);
+ return this;
+ }
+
+ private Jooby use(final Optional path, final Jooby app) {
+ requireNonNull(app, "App is required.");
+
+ Function rewrite = r -> {
+ return path.map(p -> {
+ Route.Definition result = new Route.Definition(r.method(), p + r.pattern(), r.filter());
+ result.consumes(r.consumes());
+ result.produces(r.produces());
+ result.excludes(r.excludes());
+ return result;
+ }).orElse(r);
+ };
+
+ app.bag.forEach(it -> {
+ if (it instanceof Route.Definition) {
+ this.bag.add(rewrite.apply((Definition) it));
+ } else if (it instanceof MvcClass) {
+ Object routes = path.map(p -> new MvcClass(((MvcClass) it).routeClass, p, prefix))
+ .orElse(it);
+ this.bag.add(routes);
+ } else {
+ // everything else
+ this.bag.add(it);
+ }
+ });
+ // start/stop callback
+ app.onStart.forEach(this.onStart::add);
+ app.onStarted.forEach(this.onStarted::add);
+ app.onStop.forEach(this.onStop::add);
+ // mapper
+ if (app.mapper != null) {
+ this.map(app.mapper);
+ }
+ if (apprefs == null) {
+ apprefs = new ArrayList<>();
+ }
+ apprefs.add(app);
+ return this;
+ }
+
+ /**
+ * Set a custom {@link Env.Builder} to use.
+ *
+ * @param env A custom env builder.
+ * @return This jooby instance.
+ */
+ public Jooby env(final Env.Builder env) {
+ this.env = requireNonNull(env, "Env builder is required.");
+ return this;
+ }
+
+ @Override
+ public Jooby onStart(final Throwing.Runnable callback) {
+ LifeCycle.super.onStart(callback);
+ return this;
+ }
+
+ @Override
+ public Jooby onStart(final Throwing.Consumer callback) {
+ requireNonNull(callback, "Callback is required.");
+ onStart.add(callback);
+ return this;
+ }
+
+ @Override
+ public Jooby onStarted(final Throwing.Runnable callback) {
+ LifeCycle.super.onStarted(callback);
+ return this;
+ }
+
+ @Override
+ public Jooby onStarted(final Throwing.Consumer callback) {
+ requireNonNull(callback, "Callback is required.");
+ onStarted.add(callback);
+ return this;
+ }
+
+ @Override
+ public Jooby onStop(final Throwing.Runnable callback) {
+ LifeCycle.super.onStop(callback);
+ return this;
+ }
+
+ @Override
+ public Jooby onStop(final Throwing.Consumer callback) {
+ requireNonNull(callback, "Callback is required.");
+ onStop.add(callback);
+ return this;
+ }
+
+ /**
+ * Run the given callback if and only if, application runs in the given environment.
+ *
+ *
+ * {
+ * on("dev", () {@literal ->} {
+ * use(new DevModule());
+ * });
+ * }
+ *
+ *
+ * There is an else clause which is the opposite version of the env predicate:
+ *
+ *
+ * {
+ * on("dev", () {@literal ->} {
+ * use(new DevModule());
+ * }).orElse(() {@literal ->} {
+ * use(new RealModule());
+ * });
+ * }
+ *
+ *
+ * @param env Environment where we want to run the callback.
+ * @param callback An env callback.
+ * @return This jooby instance.
+ */
+ public EnvPredicate on(final String env, final Runnable callback) {
+ requireNonNull(env, "Env is required.");
+ return on(envpredicate(env), callback);
+ }
+
+ /**
+ * Run the given callback if and only if, application runs in the given environment.
+ *
+ *
+ * {
+ * on("dev", () {@literal ->} {
+ * use(new DevModule());
+ * });
+ * }
+ *
+ *
+ * There is an else clause which is the opposite version of the env predicate:
+ *
+ *
+ * {
+ * on("dev", conf {@literal ->} {
+ * use(new DevModule());
+ * }).orElse(conf {@literal ->} {
+ * use(new RealModule());
+ * });
+ * }
+ *
+ *
+ * @param env Environment where we want to run the callback.
+ * @param callback An env callback.
+ * @return This jooby instance.
+ */
+ public EnvPredicate on(final String env, final Consumer callback) {
+ requireNonNull(env, "Env is required.");
+ return on(envpredicate(env), callback);
+ }
+
+ /**
+ * Run the given callback if and only if, application runs in the given envirobment.
+ *
+ *
+ * {
+ * on("dev", "test", () {@literal ->} {
+ * use(new DevModule());
+ * });
+ * }
+ *
+ *
+ * There is an else clause which is the opposite version of the env predicate:
+ *
+ *
+ * {
+ * on(env {@literal ->} env.equals("dev"), () {@literal ->} {
+ * use(new DevModule());
+ * }).orElse(() {@literal ->} {
+ * use(new RealModule());
+ * });
+ * }
+ *
+ *
+ * @param predicate Predicate to check the environment.
+ * @param callback An env callback.
+ * @return This jooby instance.
+ */
+ public EnvPredicate on(final Predicate predicate, final Runnable callback) {
+ requireNonNull(predicate, "Predicate is required.");
+ requireNonNull(callback, "Callback is required.");
+
+ return on(predicate, conf -> callback.run());
+ }
+
+ /**
+ * Run the given callback if and only if, application runs in the given environment.
+ *
+ *
+ * {
+ * on(env {@literal ->} env.equals("dev"), conf {@literal ->} {
+ * use(new DevModule());
+ * });
+ * }
+ *
+ *
+ * @param predicate Predicate to check the environment.
+ * @param callback An env callback.
+ * @return This jooby instance.
+ */
+ public EnvPredicate on(final Predicate predicate, final Consumer callback) {
+ requireNonNull(predicate, "Predicate is required.");
+ requireNonNull(callback, "Callback is required.");
+ this.bag.add(new EnvDep(predicate, callback));
+
+ return otherwise -> this.bag.add(new EnvDep(predicate.negate(), otherwise));
+ }
+
+ /**
+ * Run the given callback if and only if, application runs in the given environment.
+ *
+ *
+ * {
+ * on("dev", "test", "mock", () {@literal ->} {
+ * use(new DevModule());
+ * });
+ * }
+ *
+ *
+ * @param env1 Environment where we want to run the callback.
+ * @param env2 Environment where we want to run the callback.
+ * @param env3 Environment where we want to run the callback.
+ * @param callback An env callback.
+ * @return This jooby instance.
+ */
+ public Jooby on(final String env1, final String env2, final String env3,
+ final Runnable callback) {
+ on(envpredicate(env1).or(envpredicate(env2)).or(envpredicate(env3)), callback);
+ return this;
+ }
+
+ @Override
+ public T require(final Key type) {
+ checkState(injector != null,
+ "Registry is not ready. Require calls are available at application startup time, see http://jooby.org/doc/#application-life-cycle");
+ try {
+ return injector.getInstance(type);
+ } catch (ProvisionException x) {
+ Throwable cause = x.getCause();
+ if (cause instanceof Err) {
+ throw (Err) cause;
+ }
+ throw x;
+ }
+ }
+
+ @Override
+ public Route.OneArgHandler promise(final Deferred.Initializer initializer) {
+ return req -> {
+ return new Deferred(initializer);
+ };
+ }
+
+ @Override
+ public Route.OneArgHandler promise(final String executor,
+ final Deferred.Initializer initializer) {
+ return req -> new Deferred(executor, initializer);
+ }
+
+ @Override
+ public Route.OneArgHandler promise(final Deferred.Initializer0 initializer) {
+ return req -> {
+ return new Deferred(initializer);
+ };
+ }
+
+ @Override
+ public Route.OneArgHandler promise(final String executor,
+ final Deferred.Initializer0 initializer) {
+ return req -> new Deferred(executor, initializer);
+ }
+
+ /**
+ * Setup a session store to use. Useful if you want/need to persist sessions between shutdowns,
+ * apply timeout policies, etc...
+ *
+ * Jooby comes with a dozen of {@link Session.Store}, checkout the
+ * session modules .
+ *
+ * This method returns a {@link Session.Definition} objects that let you customize the session
+ * cookie.
+ *
+ * @param store A session store.
+ * @return A session store definition.
+ */
+ public Session.Definition session(final Class extends Session.Store> store) {
+ this.session = new Session.Definition(requireNonNull(store, "A session store is required."));
+ return this.session;
+ }
+
+ /**
+ * Setup a session store that saves data in a the session cookie. It makes the application
+ * stateless, which help to scale easily. Keep in mind that a cookie has a limited size (up to
+ * 4kb) so you must pay attention to what you put in the session object (don't use as cache).
+ *
+ * Cookie session signed data using the application.secret property, so you must
+ * provide an application.secret value. On dev environment you can set it in your
+ * .conf file. In prod is probably better to provide as command line argument and/or
+ * environment variable. Just make sure to keep it private.
+ *
+ * Please note {@link Session#id()}, {@link Session#accessedAt()}, etc.. make no sense for cookie
+ * sessions, just the {@link Session#attributes()}.
+ *
+ * This method returns a {@link Session.Definition} objects that let you customize the session
+ * cookie.
+ *
+ * @return A session definition/configuration object.
+ */
+ public Session.Definition cookieSession() {
+ this.session = new Session.Definition();
+ return this.session;
+ }
+
+ /**
+ * Setup a session store to use. Useful if you want/need to persist sessions between shutdowns,
+ * apply timeout policies, etc...
+ *
+ * Jooby comes with a dozen of {@link Session.Store}, checkout the
+ * session modules .
+ *
+ * This method returns a {@link Session.Definition} objects that let you customize the session
+ * cookie.
+ *
+ * @param store A session store.
+ * @return A session store definition.
+ */
+ public Session.Definition session(final Session.Store store) {
+ this.session = new Session.Definition(requireNonNull(store, "A session store is required."));
+ return this.session;
+ }
+
+ /**
+ * Register a new param/body converter. See {@link Parser} for more details.
+ *
+ * @param parser A parser.
+ * @return This jooby instance.
+ */
+ public Jooby parser(final Parser parser) {
+ if (parser instanceof BeanParser) {
+ beanParser = Optional.of(parser);
+ } else {
+ bag.add(requireNonNull(parser, "A parser is required."));
+ }
+ return this;
+ }
+
+ /**
+ * Append a response {@link Renderer} for write HTTP messages.
+ *
+ * @param renderer A renderer renderer.
+ * @return This jooby instance.
+ */
+ public Jooby renderer(final Renderer renderer) {
+ this.bag.add(requireNonNull(renderer, "A renderer is required."));
+ return this;
+ }
+
+ @Override
+ public Route.Definition before(final String method, final String pattern,
+ final Route.Before handler) {
+ return appendDefinition(method, pattern, handler);
+ }
+
+ @Override
+ public Route.Definition after(final String method, final String pattern,
+ final Route.After handler) {
+ return appendDefinition(method, pattern, handler);
+ }
+
+ @Override
+ public Route.Definition complete(final String method, final String pattern,
+ final Route.Complete handler) {
+ return appendDefinition(method, pattern, handler);
+ }
+
+ @Override
+ public Route.Definition use(final String path, final Route.Filter filter) {
+ return appendDefinition("*", path, filter);
+ }
+
+ @Override
+ public Route.Definition use(final String verb, final String path, final Route.Filter filter) {
+ return appendDefinition(verb, path, filter);
+ }
+
+ @Override
+ public Route.Definition use(final String verb, final String path, final Route.Handler handler) {
+ return appendDefinition(verb, path, handler);
+ }
+
+ @Override
+ public Route.Definition use(final String path, final Route.Handler handler) {
+ return appendDefinition("*", path, handler);
+ }
+
+ @Override
+ public Route.Definition use(final String path, final Route.OneArgHandler handler) {
+ return appendDefinition("*", path, handler);
+ }
+
+ @Override
+ public Route.Definition get(final String path, final Route.Handler handler) {
+ if (handler instanceof AssetHandler) {
+ return assets(path, (AssetHandler) handler);
+ } else {
+ return appendDefinition(GET, path, handler);
+ }
+ }
+
+ @Override
+ public Route.Collection get(final String path1, final String path2, final Route.Handler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{get(path1, handler), get(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection get(final String path1, final String path2, final String path3,
+ final Route.Handler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{get(path1, handler), get(path2, handler), get(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition get(final String path, final Route.OneArgHandler handler) {
+ return appendDefinition(GET, path, handler);
+ }
+
+ @Override
+ public Route.Collection get(final String path1, final String path2,
+ final Route.OneArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{get(path1, handler), get(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection get(final String path1, final String path2,
+ final String path3, final Route.OneArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{get(path1, handler), get(path2, handler), get(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition get(final String path, final Route.ZeroArgHandler handler) {
+ return appendDefinition(GET, path, handler);
+ }
+
+ @Override
+ public Route.Collection get(final String path1, final String path2,
+ final Route.ZeroArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{get(path1, handler), get(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection get(final String path1, final String path2,
+ final String path3, final Route.ZeroArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{get(path1, handler), get(path2, handler), get(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition get(final String path, final Route.Filter filter) {
+ return appendDefinition(GET, path, filter);
+ }
+
+ @Override
+ public Route.Collection get(final String path1, final String path2, final Route.Filter filter) {
+ return new Route.Collection(new Route.Definition[]{get(path1, filter), get(path2, filter)});
+ }
+
+ @Override
+ public Route.Collection get(final String path1, final String path2,
+ final String path3, final Route.Filter filter) {
+ return new Route.Collection(
+ new Route.Definition[]{get(path1, filter), get(path2, filter), get(path3, filter)});
+ }
+
+ @Override
+ public Route.Definition post(final String path, final Route.Handler handler) {
+ return appendDefinition(POST, path, handler);
+ }
+
+ @Override
+ public Route.Collection post(final String path1, final String path2,
+ final Route.Handler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{post(path1, handler), post(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection post(final String path1, final String path2,
+ final String path3, final Route.Handler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{post(path1, handler), post(path2, handler), post(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition post(final String path, final Route.OneArgHandler handler) {
+ return appendDefinition(POST, path, handler);
+ }
+
+ @Override
+ public Route.Collection post(final String path1, final String path2,
+ final Route.OneArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{post(path1, handler), post(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection post(final String path1, final String path2,
+ final String path3, final Route.OneArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{post(path1, handler), post(path2, handler), post(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition post(final String path, final Route.ZeroArgHandler handler) {
+ return appendDefinition(POST, path, handler);
+ }
+
+ @Override
+ public Route.Collection post(final String path1, final String path2,
+ final Route.ZeroArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{post(path1, handler), post(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection post(final String path1, final String path2,
+ final String path3, final Route.ZeroArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{post(path1, handler), post(path2, handler), post(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition post(final String path, final Route.Filter filter) {
+ return appendDefinition(POST, path, filter);
+ }
+
+ @Override
+ public Route.Collection post(final String path1, final String path2,
+ final Route.Filter filter) {
+ return new Route.Collection(
+ new Route.Definition[]{post(path1, filter), post(path2, filter)});
+ }
+
+ @Override
+ public Route.Collection post(final String path1, final String path2,
+ final String path3, final Route.Filter filter) {
+ return new Route.Collection(
+ new Route.Definition[]{post(path1, filter), post(path2, filter), post(path3, filter)});
+ }
+
+ @Override
+ public Route.Definition head(final String path, final Route.Handler handler) {
+ return appendDefinition(HEAD, path, handler);
+ }
+
+ @Override
+ public Route.Definition head(final String path,
+ final Route.OneArgHandler handler) {
+ return appendDefinition(HEAD, path, handler);
+ }
+
+ @Override
+ public Route.Definition head(final String path, final Route.ZeroArgHandler handler) {
+ return appendDefinition(HEAD, path, handler);
+ }
+
+ @Override
+ public Route.Definition head(final String path, final Route.Filter filter) {
+ return appendDefinition(HEAD, path, filter);
+ }
+
+ @Override
+ public Route.Definition head() {
+ return appendDefinition(HEAD, "*", filter(HeadHandler.class)).name("*.head");
+ }
+
+ @Override
+ public Route.Definition options(final String path, final Route.Handler handler) {
+ return appendDefinition(OPTIONS, path, handler);
+ }
+
+ @Override
+ public Route.Definition options(final String path,
+ final Route.OneArgHandler handler) {
+ return appendDefinition(OPTIONS, path, handler);
+ }
+
+ @Override
+ public Route.Definition options(final String path,
+ final Route.ZeroArgHandler handler) {
+ return appendDefinition(OPTIONS, path, handler);
+ }
+
+ @Override
+ public Route.Definition options(final String path,
+ final Route.Filter filter) {
+ return appendDefinition(OPTIONS, path, filter);
+ }
+
+ @Override
+ public Route.Definition options() {
+ return appendDefinition(OPTIONS, "*", handler(OptionsHandler.class)).name("*.options");
+ }
+
+ @Override
+ public Route.Definition put(final String path,
+ final Route.Handler handler) {
+ return appendDefinition(PUT, path, handler);
+ }
+
+ @Override
+ public Route.Collection put(final String path1, final String path2,
+ final Route.Handler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{put(path1, handler), put(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection put(final String path1, final String path2,
+ final String path3, final Route.Handler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{put(path1, handler), put(path2, handler), put(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition put(final String path,
+ final Route.OneArgHandler handler) {
+ return appendDefinition(PUT, path, handler);
+ }
+
+ @Override
+ public Route.Collection put(final String path1, final String path2,
+ final Route.OneArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{put(path1, handler), put(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection put(final String path1, final String path2,
+ final String path3, final Route.OneArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{put(path1, handler), put(path2, handler), put(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition put(final String path,
+ final Route.ZeroArgHandler handler) {
+ return appendDefinition(PUT, path, handler);
+ }
+
+ @Override
+ public Route.Collection put(final String path1, final String path2,
+ final Route.ZeroArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{put(path1, handler), put(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection put(final String path1, final String path2,
+ final String path3, final Route.ZeroArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{put(path1, handler), put(path2, handler), put(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition put(final String path,
+ final Route.Filter filter) {
+ return appendDefinition(PUT, path, filter);
+ }
+
+ @Override
+ public Route.Collection put(final String path1, final String path2,
+ final Route.Filter filter) {
+ return new Route.Collection(
+ new Route.Definition[]{put(path1, filter), put(path2, filter)});
+ }
+
+ @Override
+ public Route.Collection put(final String path1, final String path2,
+ final String path3, final Route.Filter filter) {
+ return new Route.Collection(
+ new Route.Definition[]{put(path1, filter), put(path2, filter), put(path3, filter)});
+ }
+
+ @Override
+ public Route.Definition patch(final String path, final Route.Handler handler) {
+ return appendDefinition(PATCH, path, handler);
+ }
+
+ @Override
+ public Route.Collection patch(final String path1, final String path2,
+ final Route.Handler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{patch(path1, handler), patch(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection patch(final String path1, final String path2,
+ final String path3, final Route.Handler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{patch(path1, handler), patch(path2, handler),
+ patch(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition patch(final String path, final Route.OneArgHandler handler) {
+ return appendDefinition(PATCH, path, handler);
+ }
+
+ @Override
+ public Route.Collection patch(final String path1, final String path2,
+ final Route.OneArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{patch(path1, handler), patch(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection patch(final String path1, final String path2,
+ final String path3, final Route.OneArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{patch(path1, handler), patch(path2, handler),
+ patch(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition patch(final String path, final Route.ZeroArgHandler handler) {
+ return appendDefinition(PATCH, path, handler);
+ }
+
+ @Override
+ public Route.Collection patch(final String path1, final String path2,
+ final Route.ZeroArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{patch(path1, handler), patch(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection patch(final String path1, final String path2,
+ final String path3, final Route.ZeroArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{patch(path1, handler), patch(path2, handler),
+ patch(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition patch(final String path,
+ final Route.Filter filter) {
+ return appendDefinition(PATCH, path, filter);
+ }
+
+ @Override
+ public Route.Collection patch(final String path1, final String path2,
+ final Route.Filter filter) {
+ return new Route.Collection(
+ new Route.Definition[]{patch(path1, filter), patch(path2, filter)});
+ }
+
+ @Override
+ public Route.Collection patch(final String path1, final String path2,
+ final String path3, final Route.Filter filter) {
+ return new Route.Collection(
+ new Route.Definition[]{patch(path1, filter), patch(path2, filter),
+ patch(path3, filter)});
+ }
+
+ @Override
+ public Route.Definition delete(final String path, final Route.Handler handler) {
+ return appendDefinition(DELETE, path, handler);
+ }
+
+ @Override
+ public Route.Collection delete(final String path1, final String path2,
+ final Route.Handler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{delete(path1, handler), delete(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection delete(final String path1, final String path2, final String path3,
+ final Route.Handler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{delete(path1, handler), delete(path2, handler),
+ delete(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition delete(final String path, final Route.OneArgHandler handler) {
+ return appendDefinition(DELETE, path, handler);
+ }
+
+ @Override
+ public Route.Collection delete(final String path1, final String path2,
+ final Route.OneArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{delete(path1, handler), delete(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection delete(final String path1, final String path2, final String path3,
+ final Route.OneArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{delete(path1, handler), delete(path2, handler),
+ delete(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition delete(final String path,
+ final Route.ZeroArgHandler handler) {
+ return appendDefinition(DELETE, path, handler);
+ }
+
+ @Override
+ public Route.Collection delete(final String path1,
+ final String path2, final Route.ZeroArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{delete(path1, handler), delete(path2, handler)});
+ }
+
+ @Override
+ public Route.Collection delete(final String path1, final String path2, final String path3,
+ final Route.ZeroArgHandler handler) {
+ return new Route.Collection(
+ new Route.Definition[]{delete(path1, handler), delete(path2, handler),
+ delete(path3, handler)});
+ }
+
+ @Override
+ public Route.Definition delete(final String path, final Route.Filter filter) {
+ return appendDefinition(DELETE, path, filter);
+ }
+
+ @Override
+ public Route.Collection delete(final String path1, final String path2,
+ final Route.Filter filter) {
+ return new Route.Collection(
+ new Route.Definition[]{delete(path1, filter), delete(path2, filter)});
+ }
+
+ @Override
+ public Route.Collection delete(final String path1, final String path2, final String path3,
+ final Route.Filter filter) {
+ return new Route.Collection(
+ new Route.Definition[]{delete(path1, filter), delete(path2, filter),
+ delete(path3, filter)});
+ }
+
+ @Override
+ public Route.Definition trace(final String path, final Route.Handler handler) {
+ return appendDefinition(TRACE, path, handler);
+ }
+
+ @Override
+ public Route.Definition trace(final String path, final Route.OneArgHandler handler) {
+ return appendDefinition(TRACE, path, handler);
+ }
+
+ @Override
+ public Route.Definition trace(final String path, final Route.ZeroArgHandler handler) {
+ return appendDefinition(TRACE, path, handler);
+ }
+
+ @Override
+ public Route.Definition trace(final String path, final Route.Filter filter) {
+ return appendDefinition(TRACE, path, filter);
+ }
+
+ @Override
+ public Route.Definition trace() {
+ return appendDefinition(TRACE, "*", handler(TraceHandler.class)).name("*.trace");
+ }
+
+ @Override
+ public Route.Definition connect(final String path, final Route.Handler handler) {
+ return appendDefinition(CONNECT, path, handler);
+ }
+
+ @Override
+ public Route.Definition connect(final String path, final Route.OneArgHandler handler) {
+ return appendDefinition(CONNECT, path, handler);
+ }
+
+ @Override
+ public Route.Definition connect(final String path, final Route.ZeroArgHandler handler) {
+ return appendDefinition(CONNECT, path, handler);
+ }
+
+ @Override
+ public Route.Definition connect(final String path, final Route.Filter filter) {
+ return appendDefinition(CONNECT, path, filter);
+ }
+
+ /**
+ * Creates a new {@link Route.Handler} that delegate the execution to the given handler. This is
+ * useful when the target handler requires some dependencies.
+ *
+ *
+ * public class MyHandler implements Route.Handler {
+ * @Inject
+ * public MyHandler(Dependency d) {
+ * }
+ *
+ * public void handle(Request req, Response rsp) throws Exception {
+ * // do something
+ * }
+ * }
+ * ...
+ * // external route
+ * get("/", handler(MyHandler.class));
+ *
+ * // inline version route
+ * get("/", (req, rsp) {@literal ->} {
+ * Dependency d = req.getInstance(Dependency.class);
+ * // do something
+ * });
+ *
+ *
+ * You can access to a dependency from a in-line route too, so the use of external route it is
+ * more or less a matter of taste.
+ *
+ * @param handler The external handler class.
+ * @return A new inline route handler.
+ */
+ private Route.Handler handler(final Class extends Route.Handler> handler) {
+ requireNonNull(handler, "Route handler is required.");
+ return (req, rsp) -> req.require(handler).handle(req, rsp);
+ }
+
+ /**
+ * Creates a new {@link Route.Filter} that delegate the execution to the given filter. This is
+ * useful when the target handler requires some dependencies.
+ *
+ *
+ * public class MyFilter implements Filter {
+ * @Inject
+ * public MyFilter(Dependency d) {
+ * }
+ *
+ * public void handle(Request req, Response rsp, Route.Chain chain) throws Exception {
+ * // do something
+ * }
+ * }
+ * ...
+ * // external filter
+ * get("/", filter(MyFilter.class));
+ *
+ * // inline version route
+ * get("/", (req, rsp, chain) {@literal ->} {
+ * Dependency d = req.getInstance(Dependency.class);
+ * // do something
+ * });
+ *
+ *
+ * You can access to a dependency from a in-line route too, so the use of external filter it is
+ * more or less a matter of taste.
+ *
+ * @param filter The external filter class.
+ * @return A new inline route.
+ */
+ private Route.Filter filter(final Class extends Route.Filter> filter) {
+ requireNonNull(filter, "Filter is required.");
+ return (req, rsp, chain) -> req.require(filter).handle(req, rsp, chain);
+ }
+
+ @Override
+ public Route.AssetDefinition assets(final String path, final Path basedir) {
+ return assets(path, new AssetHandler(basedir));
+ }
+
+ @Override
+ public Route.AssetDefinition assets(final String path, final String location) {
+ return assets(path, new AssetHandler(location));
+ }
+
+ @Override
+ public Route.AssetDefinition assets(final String path, final AssetHandler handler) {
+ Route.AssetDefinition route = appendDefinition(GET, path, handler, Route.AssetDefinition::new);
+ return configureAssetHandler(route);
+ }
+
+ @Override
+ public Route.Collection use(final Class> routeClass) {
+ return use("", routeClass);
+ }
+
+ @Override
+ public Route.Collection use(final String path, final Class> routeClass) {
+ requireNonNull(routeClass, "Route class is required.");
+ requireNonNull(path, "Path is required");
+ MvcClass mvc = new MvcClass(routeClass, path, prefix);
+ bag.add(mvc);
+ return new Route.Collection(mvc);
+ }
+
+ /**
+ * Keep track of routes in the order user define them.
+ *
+ * @param method Route method.
+ * @param pattern Route pattern.
+ * @param filter Route filter.
+ * @return The same route definition.
+ */
+ private Route.Definition appendDefinition(String method, String pattern, Route.Filter filter) {
+ return appendDefinition(method, pattern, filter, Route.Definition::new);
+ }
+
+ /**
+ * Keep track of routes in the order user define them.
+ *
+ * @param method Route method.
+ * @param pattern Route pattern.
+ * @param filter Route filter.
+ * @param creator Route creator.
+ * @return The same route definition.
+ */
+ private T appendDefinition(String method, String pattern,
+ Route.Filter filter, Throwing.Function4 creator) {
+ String pathPattern = prefixPath(pattern).orElse(pattern);
+ T route = creator.apply(method, pathPattern, filter, caseSensitiveRouting);
+ if (prefix != null) {
+ route.prefix = prefix;
+ // reset name will update the name if prefix != null
+ route.name(route.name());
+ }
+ bag.add(route);
+ return route;
+ }
+
+ /**
+ * Import an application {@link Module}.
+ *
+ * @param module The module to import.
+ * @return This jooby instance.
+ * @see Jooby.Module
+ */
+ public Jooby use(final Jooby.Module module) {
+ requireNonNull(module, "A module is required.");
+ bag.add(module);
+ return this;
+ }
+
+ /**
+ * Set/specify a custom .conf file, useful when you don't want a application.conf
+ * file.
+ *
+ * @param path Classpath location.
+ * @return This jooby instance.
+ */
+ public Jooby conf(final String path) {
+ this.confname = path;
+ use(ConfigFactory.parseResources(path));
+ return this;
+ }
+
+ /**
+ * Set/specify a custom .conf file, useful when you don't want a application.conf
+ * file.
+ *
+ * @param path File system location.
+ * @return This jooby instance.
+ */
+ public Jooby conf(final File path) {
+ this.confname = path.getName();
+ use(ConfigFactory.parseFile(path));
+ return this;
+ }
+
+ /**
+ * Set the application configuration object. You must call this method when the default file
+ * name: application.conf doesn't work for you or when you need/want to register two
+ * or more files.
+ *
+ * @param config The application configuration object.
+ * @return This jooby instance.
+ * @see Config
+ */
+ public Jooby use(final Config config) {
+ this.srcconf = requireNonNull(config, "Config required.");
+ return this;
+ }
+
+ @Override
+ public Jooby err(final Err.Handler err) {
+ this.bag.add(requireNonNull(err, "An err handler is required."));
+ return this;
+ }
+
+ @Override
+ public WebSocket.Definition ws(final String path, final WebSocket.OnOpen handler) {
+ WebSocket.Definition ws = new WebSocket.Definition(path, handler);
+ checkArgument(bag.add(ws), "Duplicated path: '%s'", path);
+ return ws;
+ }
+
+ @Override
+ public WebSocket.Definition ws(final String path,
+ final Class extends WebSocket.OnMessage> handler) {
+ String fpath = Optional.ofNullable(handler.getAnnotation(org.jooby.mvc.Path.class))
+ .map(it -> path + "/" + it.value()[0])
+ .orElse(path);
+
+ WebSocket.Definition ws = ws(fpath, MvcWebSocket.newWebSocket(handler));
+
+ Optional.ofNullable(handler.getAnnotation(Consumes.class))
+ .ifPresent(consumes -> Arrays.asList(consumes.value()).forEach(ws::consumes));
+ Optional.ofNullable(handler.getAnnotation(Produces.class))
+ .ifPresent(produces -> Arrays.asList(produces.value()).forEach(ws::produces));
+ return ws;
+ }
+
+ @Override
+ public Route.Definition sse(final String path, final Sse.Handler handler) {
+ return appendDefinition(GET, path, handler).consumes(MediaType.sse);
+ }
+
+ @Override
+ public Route.Definition sse(final String path, final Sse.Handler1 handler) {
+ return appendDefinition(GET, path, handler).consumes(MediaType.sse);
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Route.Collection with(final Runnable callback) {
+ // hacky way of doing what we want... but we do simplify developer life
+ int size = this.bag.size();
+ callback.run();
+ // collect latest routes and apply route props
+ List local = this.bag.stream()
+ .skip(size)
+ .filter(Route.Props.class::isInstance)
+ .map(Route.Props.class::cast)
+ .collect(Collectors.toList());
+ return new Route.Collection(local.toArray(new Route.Props[local.size()]));
+ }
+
+ /**
+ * Prepare and startup a {@link Jooby} application.
+ *
+ * @param app Application supplier.
+ * @param args Application arguments.
+ */
+ public static void run(final Supplier extends Jooby> app, final String... args) {
+ Config conf = ConfigFactory.systemProperties()
+ .withFallback(args(args));
+ System.setProperty("logback.configurationFile", logback(conf));
+ app.get().start(args);
+ }
+
+ /**
+ * Prepare and startup a {@link Jooby} application.
+ *
+ * @param app Application supplier.
+ * @param args Application arguments.
+ */
+ public static void run(final Class extends Jooby> app, final String... args) {
+ run(() -> Try.apply(() -> app.newInstance()).get(), args);
+ }
+
+ /**
+ * Export configuration from an application. Useful for tooling, testing, debugging, etc...
+ *
+ * @param app Application to extract/collect configuration.
+ * @return Application conf or empty conf on error.
+ */
+ public static Config exportConf(final Jooby app) {
+ AtomicReference conf = new AtomicReference<>(ConfigFactory.empty());
+ app.on("*", c -> {
+ conf.set(c);
+ });
+ exportRoutes(app);
+ return conf.get();
+ }
+
+ /**
+ * Export routes from an application. Useful for route analysis, testing, debugging, etc...
+ *
+ * @param app Application to extract/collect routes.
+ * @return Application routes.
+ */
+ public static List exportRoutes(final Jooby app) {
+ @SuppressWarnings("serial") class Success extends RuntimeException {
+ List routes;
+
+ Success(final List routes) {
+ this.routes = routes;
+ }
+ }
+ List routes = Collections.emptyList();
+ try {
+ app.start(new String[0], r -> {
+ throw new Success(r);
+ });
+ } catch (Success success) {
+ routes = success.routes;
+ } catch (Throwable x) {
+ logger(app).debug("Failed bootstrap: {}", app, x);
+ }
+ return routes;
+ }
+
+ /**
+ * Start an application. Fire the {@link #onStart(Throwing.Runnable)} event and the
+ * {@link #onStarted(Throwing.Runnable)} events.
+ */
+ public void start() {
+ start(new String[0]);
+ }
+
+ /**
+ * Start an application. Fire the {@link #onStart(Throwing.Runnable)} event and the
+ * {@link #onStarted(Throwing.Runnable)} events.
+ *
+ * @param args Application arguments.
+ */
+ public void start(final String... args) {
+ try {
+ start(args, null);
+ } catch (Throwable x) {
+ stop();
+ String msg = "An error occurred while starting the application:";
+ if (throwBootstrapException) {
+ throw new Err(Status.SERVICE_UNAVAILABLE, msg, x);
+ } else {
+ logger(this).error(msg, x);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void start(final String[] args, final Consumer> routes)
+ throws Throwable {
+ long start = System.currentTimeMillis();
+
+ started.set(true);
+
+ this.injector = bootstrap(args(args), routes);
+
+ // shutdown hook
+ Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
+
+ Config conf = injector.getInstance(Config.class);
+
+ Logger log = logger(this);
+
+ // inject class
+ injector.injectMembers(this);
+
+ // onStart callbacks via .conf
+ if (conf.hasPath("jooby.internal.onStart")) {
+ ClassLoader loader = getClass().getClassLoader();
+ Object internalOnStart = loader.loadClass(conf.getString("jooby.internal.onStart"))
+ .newInstance();
+ onStart.add((Throwing.Consumer) internalOnStart);
+ }
+
+ // start services
+ for (Throwing.Consumer onStart : this.onStart) {
+ onStart.accept(this);
+ }
+
+ // route mapper
+ Set routeDefs = injector.getInstance(Route.KEY);
+ Set sockets = injector.getInstance(WebSocket.KEY);
+ if (mapper != null) {
+ routeDefs.forEach(it -> it.map(mapper));
+ }
+
+ AppPrinter printer = new AppPrinter(routeDefs, sockets, conf);
+ printer.printConf(log, conf);
+
+ // Start server
+ Server server = injector.getInstance(Server.class);
+ String serverName = server.getClass().getSimpleName().replace("Server", "").toLowerCase();
+
+ server.start();
+ long end = System.currentTimeMillis();
+
+ log.info("[{}@{}]: Server started in {}ms\n\n{}\n",
+ conf.getString("application.env"),
+ serverName,
+ end - start,
+ printer);
+
+ // started services
+ for (Throwing.Consumer onStarted : this.onStarted) {
+ onStarted.accept(this);
+ }
+
+ boolean join = conf.hasPath("server.join") ? conf.getBoolean("server.join") : true;
+ if (join) {
+ server.join();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Jooby map(final Mapper> mapper) {
+ requireNonNull(mapper, "Mapper is required.");
+ if (mappers.add(mapper.name())) {
+ this.mapper = Optional.ofNullable(this.mapper)
+ .map(next -> Route.Mapper.chain(mapper, next))
+ .orElse((Mapper) mapper);
+ }
+ return this;
+ }
+
+ /**
+ * Use the injection provider to create the Guice injector
+ *
+ * @param injectorFactory the injection provider
+ * @return this instance.
+ */
+
+ public Jooby injector(
+ final BiFunction injectorFactory) {
+ this.injectorFactory = injectorFactory;
+ return this;
+ }
+
+ /**
+ * Bind the provided abstract type to the given implementation:
+ *
+ *
+ * {
+ * bind(MyInterface.class, MyImplementation.class);
+ * }
+ *
+ *
+ * @param type Service interface.
+ * @param implementation Service implementation.
+ * @param Service type.
+ * @return This instance.
+ */
+ public Jooby bind(final Class type, final Class extends T> implementation) {
+ use((env, conf, binder) -> {
+ binder.bind(type).to(implementation);
+ });
+ return this;
+ }
+
+ /**
+ * Bind the provided abstract type to the given implementation:
+ *
+ *
+ * {
+ * bind(MyInterface.class, MyImplementation::new);
+ * }
+ *
+ *
+ * @param type Service interface.
+ * @param implementation Service implementation.
+ * @param Service type.
+ * @return This instance.
+ */
+ public Jooby bind(final Class type, final Supplier implementation) {
+ use((env, conf, binder) -> {
+ binder.bind(type).toInstance(implementation.get());
+ });
+ return this;
+ }
+
+ /**
+ * Bind the provided type:
+ *
+ *
+ * {
+ * bind(MyInterface.class);
+ * }
+ *
+ *
+ * @param type Service interface.
+ * @param Service type.
+ * @return This instance.
+ */
+ public Jooby bind(final Class type) {
+ use((env, conf, binder) -> {
+ binder.bind(type);
+ });
+ return this;
+ }
+
+ /**
+ * Bind the provided type:
+ *
+ *
+ * {
+ * bind(new MyService());
+ * }
+ *
+ *
+ * @param service Service.
+ * @return This instance.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public Jooby bind(final Object service) {
+ use((env, conf, binder) -> {
+ Class type = service.getClass();
+ binder.bind(type).toInstance(service);
+ });
+ return this;
+ }
+
+ /**
+ * Bind the provided type and object that requires some type of configuration:
+ *
+ * {@code
+ * {
+ * bind(MyService.class, conf -> new MyService(conf.getString("service.url")));
+ * }
+ * }
+ *
+ * @param type Service type.
+ * @param provider Service provider.
+ * @param Service type.
+ * @return This instance.
+ */
+ public Jooby bind(final Class type, final Function provider) {
+ use((env, conf, binder) -> {
+ T service = provider.apply(conf);
+ binder.bind(type).toInstance(service);
+ });
+ return this;
+ }
+
+ /**
+ * Bind the provided type and object that requires some type of configuration:
+ *
+ * {@code
+ * {
+ * bind(conf -> new MyService(conf.getString("service.url")));
+ * }
+ * }
+ *
+ * @param provider Service provider.
+ * @param Service type.
+ * @return This instance.
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public Jooby bind(final Function provider) {
+ use((env, conf, binder) -> {
+ Object service = provider.apply(conf);
+ Class type = service.getClass();
+ binder.bind(type).toInstance(service);
+ });
+ return this;
+ }
+
+ /**
+ * Set application date format.
+ *
+ * @param dateFormat A date format.
+ * @return This instance.
+ */
+ public Jooby dateFormat(final String dateFormat) {
+ this.dateFormat = requireNonNull(dateFormat, "DateFormat required.");
+ return this;
+ }
+
+ /**
+ * Set application number format.
+ *
+ * @param numberFormat A number format.
+ * @return This instance.
+ */
+ public Jooby numberFormat(final String numberFormat) {
+ this.numberFormat = requireNonNull(numberFormat, "NumberFormat required.");
+ return this;
+ }
+
+ /**
+ * Set application/default charset.
+ *
+ * @param charset A charset.
+ * @return This instance.
+ */
+ public Jooby charset(final Charset charset) {
+ this.charset = requireNonNull(charset, "Charset required.");
+ return this;
+ }
+
+ /**
+ * Set application locale (first listed are higher priority).
+ *
+ * @param languages List of locale using the language tag format.
+ * @return This instance.
+ */
+ public Jooby lang(final String... languages) {
+ this.languages = languages;
+ return this;
+ }
+
+ /**
+ * Set application time zone.
+ *
+ * @param zoneId ZoneId.
+ * @return This instance.
+ */
+ public Jooby timezone(final ZoneId zoneId) {
+ this.zoneId = requireNonNull(zoneId, "ZoneId required.");
+ return this;
+ }
+
+ /**
+ * Set the HTTP port.
+ *
+ *
+ * Keep in mind this work as a default port and can be reset via application.port
+ * property.
+ *
+ *
+ * @param port HTTP port.
+ * @return This instance.
+ */
+ public Jooby port(final int port) {
+ this.port = port;
+ return this;
+ }
+
+ /**
+ *
+ * Set the HTTPS port to use.
+ *
+ *
+ *
+ * Keep in mind this work as a default port and can be reset via application.port
+ * property.
+ *
+ *
+ * HTTPS
+ *
+ * Jooby comes with a self-signed certificate, useful for development and test. But of course, you
+ * should NEVER use it in the real world.
+ *
+ *
+ *
+ * In order to setup HTTPS with a secure certificate, you need to set these properties:
+ *
+ *
+ *
+ *
+ * ssl.keystore.cert: An X.509 certificate chain file in PEM format. It can be an
+ * absolute path or a classpath resource.
+ *
+ *
+ * ssl.keystore.key: A PKCS#8 private key file in PEM format. It can be an absolute
+ * path or a classpath resource.
+ *
+ *
+ *
+ *
+ * Optionally, you can set these too:
+ *
+ *
+ *
+ *
+ * ssl.keystore.password: Password of the keystore.key (if any). Default is:
+ * null/empty.
+ *
+ *
+ * ssl.trust.cert: Trusted certificates for verifying the remote endpoint’s
+ * certificate. The file should contain an X.509 certificate chain in PEM format. Default uses the
+ * system default.
+ *
+ *
+ * ssl.session.cacheSize: Set the size of the cache used for storing SSL session
+ * objects. 0 to use the default value.
+ *
+ *
+ * ssl.session.timeout: Timeout for the cached SSL session objects, in seconds. 0 to
+ * use the default value.
+ *
+ *
+ *
+ *
+ * As you can see setup is very simple. All you need is your .crt and
+ * .key files.
+ *
+ *
+ * @param port HTTPS port.
+ * @return This instance.
+ */
+ public Jooby securePort(final int port) {
+ this.securePort = port;
+ return this;
+ }
+
+ /**
+ *
+ * Enable HTTP/2 protocol. Some servers require special configuration, others just
+ * works. It is a good idea to check the server documentation about
+ * HTTP/2 .
+ *
+ *
+ *
+ * In order to use HTTP/2 from a browser you must configure HTTPS, see {@link #securePort(int)}
+ * documentation.
+ *
+ *
+ *
+ * If HTTP/2 clear text is supported then you may skip the HTTPS setup, but of course you won't be
+ * able to use HTTP/2 with browsers.
+ *
+ *
+ * @return This instance.
+ */
+ public Jooby http2() {
+ this.http2 = true;
+ return this;
+ }
+
+ /**
+ * Set the default executor to use from {@link Deferred Deferred API}.
+ *
+ * Default executor runs each task in the thread that invokes {@link Executor#execute execute},
+ * that's a Jooby worker thread. A worker thread in Jooby can block.
+ *
+ * The {@link ExecutorService} will automatically shutdown.
+ *
+ * @param executor Executor to use.
+ * @return This jooby instance.
+ */
+ public Jooby executor(final ExecutorService executor) {
+ executor((Executor) executor);
+ onStop(r -> executor.shutdown());
+ return this;
+ }
+
+ /**
+ * Set the default executor to use from {@link Deferred Deferred API}.
+ *
+ * Default executor runs each task in the thread that invokes {@link Executor#execute execute},
+ * that's a Jooby worker thread. A worker thread in Jooby can block.
+ *
+ * The {@link ExecutorService} will automatically shutdown.
+ *
+ * @param executor Executor to use.
+ * @return This jooby instance.
+ */
+ public Jooby executor(final Executor executor) {
+ this.defaultExecSet = true;
+ this.executors.add(binder -> {
+ binder.bind(Key.get(String.class, Names.named("deferred"))).toInstance("deferred");
+ binder.bind(Key.get(Executor.class, Names.named("deferred"))).toInstance(executor);
+ });
+ return this;
+ }
+
+ /**
+ * Set a named executor to use from {@link Deferred Deferred API}. Useful for override the
+ * default/global executor.
+ *
+ * Default executor runs each task in the thread that invokes {@link Executor#execute execute},
+ * that's a Jooby worker thread. A worker thread in Jooby can block.
+ *
+ * The {@link ExecutorService} will automatically shutdown.
+ *
+ * @param name Name of the executor.
+ * @param executor Executor to use.
+ * @return This jooby instance.
+ */
+ public Jooby executor(final String name, final ExecutorService executor) {
+ executor(name, (Executor) executor);
+ onStop(r -> executor.shutdown());
+ return this;
+ }
+
+ /**
+ * Set a named executor to use from {@link Deferred Deferred API}. Useful for override the
+ * default/global executor.
+ *
+ * Default executor runs each task in the thread that invokes {@link Executor#execute execute},
+ * that's a Jooby worker thread. A worker thread in Jooby can block.
+ *
+ * The {@link ExecutorService} will automatically shutdown.
+ *
+ * @param name Name of the executor.
+ * @param executor Executor to use.
+ * @return This jooby instance.
+ */
+ public Jooby executor(final String name, final Executor executor) {
+ this.executors.add(binder -> {
+ binder.bind(Key.get(Executor.class, Names.named(name))).toInstance(executor);
+ });
+ return this;
+ }
+
+ /**
+ * Set the default executor to use from {@link Deferred Deferred API}. This works as reference to
+ * an executor, application directly or via module must provide an named executor.
+ *
+ * Default executor runs each task in the thread that invokes {@link Executor#execute execute},
+ * that's a Jooby worker thread. A worker thread in Jooby can block.
+ *
+ * @param name Executor to use.
+ * @return This jooby instance.
+ */
+ public Jooby executor(final String name) {
+ defaultExecSet = true;
+ this.executors.add(binder -> {
+ binder.bind(Key.get(String.class, Names.named("deferred"))).toInstance(name);
+ });
+ return this;
+ }
+
+ /**
+ * Set a named executor to use from {@link Deferred Deferred API}. Useful for override the
+ * default/global executor.
+ *
+ * Default executor runs each task in the thread that invokes {@link Executor#execute execute},
+ * that's a Jooby worker thread. A worker thread in Jooby can block.
+ *
+ * @param name Name of the executor.
+ * @param provider Provider for the executor.
+ * @return This jooby instance.
+ */
+ private Jooby executor(final String name, final Class extends Provider> provider) {
+ this.executors.add(binder -> {
+ binder.bind(Key.get(Executor.class, Names.named(name))).toProvider(provider)
+ .in(Singleton.class);
+ });
+ return this;
+ }
+
+ /**
+ * If the application fails to start all the services are shutdown. Also, the exception is logged
+ * and usually the application is going to exit.
+ *
+ * This options turn off logging and rethrow the exception as {@link Err}. Here is an example:
+ *
+ *
+ * public class App extends Jooby {
+ * {
+ * throwBootstrapException();
+ * ...
+ * }
+ * }
+ *
+ * App app = new App();
+ *
+ * try {
+ * app.start();
+ * } catch (Err err) {
+ * Throwable cause = err.getCause();
+ * }
+ *
+ *
+ * @return This instance.
+ */
+ public Jooby throwBootstrapException() {
+ this.throwBootstrapException = true;
+ return this;
+ }
+
+ /**
+ * Configure case for routing algorithm. Default is case sensitive.
+ *
+ * @param enabled True for case sensitive, false otherwise.
+ * @return This instance.
+ */
+ public Jooby caseSensitiveRouting(boolean enabled) {
+ this.caseSensitiveRouting = enabled;
+ return this;
+ }
+
+ private static List normalize(final List services, final Env env,
+ final RouteMetadata classInfo, final boolean caseSensitiveRouting) {
+ List result = new ArrayList<>();
+ List snapshot = services;
+ /** modules, routes, parsers, renderers and websockets */
+ snapshot.forEach(candidate -> {
+ if (candidate instanceof Route.Definition) {
+ result.add(candidate);
+ } else if (candidate instanceof MvcClass) {
+ MvcClass mvcRoute = ((MvcClass) candidate);
+ Class> mvcClass = mvcRoute.routeClass;
+ String path = ((MvcClass) candidate).path;
+ MvcRoutes.routes(env, classInfo, path, caseSensitiveRouting, mvcClass)
+ .forEach(route -> result.add(mvcRoute.apply(route)));
+ } else {
+ result.add(candidate);
+ }
+ });
+ return result;
+ }
+
+ private static List processEnvDep(final Set src, final Env env) {
+ List result = new ArrayList<>();
+ List bag = new ArrayList<>(src);
+ bag.forEach(it -> {
+ if (it instanceof EnvDep) {
+ EnvDep envdep = (EnvDep) it;
+ if (envdep.predicate.test(env.name())) {
+ int from = src.size();
+ envdep.callback.accept(env.config());
+ int to = src.size();
+ result.addAll(new ArrayList<>(src).subList(from, to));
+ }
+ } else {
+ result.add(it);
+ }
+ });
+ return result;
+ }
+
+ private Injector bootstrap(final Config args,
+ final Consumer> rcallback) throws Throwable {
+ Config initconf = Optional.ofNullable(srcconf)
+ .orElseGet(() -> ConfigFactory.parseResources("application.conf"));
+ List modconf = modconf(this.bag);
+ Config conf = buildConfig(initconf, args, modconf);
+
+ final List locales = LocaleUtils.parse(conf.getString("application.lang"));
+
+ Env env = this.env.build(conf, this, locales.get(0));
+ String envname = env.name();
+
+ final Charset charset = Charset.forName(conf.getString("application.charset"));
+
+ String dateFormat = conf.getString("application.dateFormat");
+ ZoneId zoneId = ZoneId.of(conf.getString("application.tz"));
+ DateTimeFormatter dateTimeFormatter = DateTimeFormatter
+ .ofPattern(dateFormat, locales.get(0))
+ .withZone(zoneId);
+ DateTimeFormatter zonedDateTimeFormat = DateTimeFormatter
+ .ofPattern(conf.getString("application.zonedDateTimeFormat"));
+
+ DecimalFormat numberFormat = new DecimalFormat(conf.getString("application.numberFormat"));
+
+ // Guice Stage
+ Stage stage = "dev".equals(envname) ? Stage.DEVELOPMENT : Stage.PRODUCTION;
+
+ // expand and normalize bag
+ RouteMetadata rm = new RouteMetadata(env);
+ List realbag = processEnvDep(this.bag, env);
+ List realmodconf = modconf(realbag);
+ List bag = normalize(realbag, env, rm, caseSensitiveRouting);
+
+ // collect routes and fire route callback
+ if (rcallback != null) {
+ List routes = bag.stream()
+ .filter(it -> it instanceof Route.Definition)
+ .map(it -> (Route.Definition) it)
+ .collect(Collectors.toList());
+ rcallback.accept(routes);
+ }
+
+ // final config ? if we add a mod that depends on env
+ Config finalConfig;
+ Env finalEnv;
+ if (modconf.size() != realmodconf.size()) {
+ finalConfig = buildConfig(initconf, args, realmodconf);
+ finalEnv = this.env.build(finalConfig, this, locales.get(0));
+ } else {
+ finalConfig = conf;
+ finalEnv = env;
+ }
+
+ boolean cookieSession = session.store() == null;
+ if (cookieSession && !finalConfig.hasPath("application.secret")) {
+ throw new IllegalStateException("Required property 'application.secret' is missing");
+ }
+
+ /** executors: */
+ if (!defaultExecSet) {
+ // default executor
+ executor(MoreExecutors.directExecutor());
+ }
+ executor("direct", MoreExecutors.directExecutor());
+ executor("server", ServerExecutorProvider.class);
+
+ /** Some basic xss functions. */
+ xss(finalEnv);
+
+ /** dependency injection */
+ @SuppressWarnings("unchecked")
+ com.google.inject.Module joobyModule = binder -> {
+
+ /** type converters */
+ new TypeConverters().configure(binder);
+
+ /** bind config */
+ bindConfig(binder, finalConfig);
+
+ /** bind env */
+ binder.bind(Env.class).toInstance(finalEnv);
+
+ /** bind charset */
+ binder.bind(Charset.class).toInstance(charset);
+
+ /** bind locale */
+ binder.bind(Locale.class).toInstance(locales.get(0));
+ TypeLiteral> localeType = (TypeLiteral>) TypeLiteral
+ .get(Types.listOf(Locale.class));
+ binder.bind(localeType).toInstance(locales);
+
+ /** bind time zone */
+ binder.bind(ZoneId.class).toInstance(zoneId);
+ binder.bind(TimeZone.class).toInstance(TimeZone.getTimeZone(zoneId));
+
+ /** bind date format */
+ binder.bind(DateTimeFormatter.class).toInstance(dateTimeFormatter);
+
+ /** bind number format */
+ binder.bind(NumberFormat.class).toInstance(numberFormat);
+ binder.bind(DecimalFormat.class).toInstance(numberFormat);
+
+ /** bind ssl provider. */
+ binder.bind(SSLContext.class).toProvider(SslContextProvider.class);
+
+ /** routes */
+ Multibinder definitions = Multibinder
+ .newSetBinder(binder, Definition.class);
+
+ /** web sockets */
+ Multibinder sockets = Multibinder
+ .newSetBinder(binder, WebSocket.Definition.class);
+
+ /** tmp dir */
+ File tmpdir = new File(finalConfig.getString("application.tmpdir"));
+ tmpdir.mkdirs();
+ binder.bind(File.class).annotatedWith(Names.named("application.tmpdir"))
+ .toInstance(tmpdir);
+
+ binder.bind(ParameterNameProvider.class).toInstance(rm);
+
+ /** err handler */
+ Multibinder ehandlers = Multibinder
+ .newSetBinder(binder, Err.Handler.class);
+
+ /** parsers & renderers */
+ Multibinder parsers = Multibinder
+ .newSetBinder(binder, Parser.class);
+
+ Multibinder renderers = Multibinder
+ .newSetBinder(binder, Renderer.class);
+
+ /** basic parser */
+ parsers.addBinding().toInstance(BuiltinParser.Basic);
+ parsers.addBinding().toInstance(BuiltinParser.Collection);
+ parsers.addBinding().toInstance(BuiltinParser.Optional);
+ parsers.addBinding().toInstance(BuiltinParser.Enum);
+ parsers.addBinding().toInstance(BuiltinParser.Bytes);
+
+ /** basic render */
+ renderers.addBinding().toInstance(BuiltinRenderer.asset);
+ renderers.addBinding().toInstance(BuiltinRenderer.bytes);
+ renderers.addBinding().toInstance(BuiltinRenderer.byteBuffer);
+ renderers.addBinding().toInstance(BuiltinRenderer.file);
+ renderers.addBinding().toInstance(BuiltinRenderer.charBuffer);
+ renderers.addBinding().toInstance(BuiltinRenderer.stream);
+ renderers.addBinding().toInstance(BuiltinRenderer.reader);
+ renderers.addBinding().toInstance(BuiltinRenderer.fileChannel);
+
+ /** modules, routes, parsers, renderers and websockets */
+ Set routeClasses = new HashSet<>();
+ for (Object it : bag) {
+ Try.run(() -> bindService(
+ logger(this),
+ this.bag,
+ finalConfig,
+ finalEnv,
+ rm,
+ binder,
+ definitions,
+ sockets,
+ ehandlers,
+ parsers,
+ renderers,
+ routeClasses,
+ caseSensitiveRouting)
+ .accept(it))
+ .throwException();
+ }
+
+ parsers.addBinding().toInstance(new DateParser(dateFormat));
+ parsers.addBinding().toInstance(new LocalDateParser(dateTimeFormatter));
+ parsers.addBinding().toInstance(new ZonedDateTimeParser(zonedDateTimeFormat));
+ parsers.addBinding().toInstance(new LocaleParser());
+ parsers.addBinding().toInstance(new StaticMethodParser("valueOf"));
+ parsers.addBinding().toInstance(new StaticMethodParser("fromString"));
+ parsers.addBinding().toInstance(new StaticMethodParser("forName"));
+ parsers.addBinding().toInstance(new StringConstructorParser());
+ parsers.addBinding().toInstance(beanParser.orElseGet(() -> new BeanParser(false)));
+
+ binder.bind(ParserExecutor.class).in(Singleton.class);
+
+ /** override(able) renderer */
+ renderers.addBinding().toInstance(new DefaulErrRenderer());
+ renderers.addBinding().toInstance(BuiltinRenderer.text);
+
+ binder.bind(HttpHandler.class).to(HttpHandlerImpl.class).in(Singleton.class);
+
+ RequestScope requestScope = new RequestScope();
+ binder.bind(RequestScope.class).toInstance(requestScope);
+ binder.bindScope(RequestScoped.class, requestScope);
+
+ /** session manager */
+ binder.bind(Session.Definition.class)
+ .toProvider(session(finalConfig.getConfig("session"), session))
+ .asEagerSingleton();
+ Object sstore = session.store();
+ if (cookieSession) {
+ binder.bind(SessionManager.class).to(CookieSessionManager.class)
+ .asEagerSingleton();
+ } else {
+ binder.bind(SessionManager.class).to(ServerSessionManager.class).asEagerSingleton();
+ if (sstore instanceof Class) {
+ binder.bind(Store.class).to((Class extends Store>) sstore)
+ .asEagerSingleton();
+ } else {
+ binder.bind(Store.class).toInstance((Store) sstore);
+ }
+ }
+
+ binder.bind(Request.class).toProvider(Providers.outOfScope(Request.class))
+ .in(RequestScoped.class);
+ binder.bind(Route.Chain.class).toProvider(Providers.outOfScope(Route.Chain.class))
+ .in(RequestScoped.class);
+ binder.bind(Response.class).toProvider(Providers.outOfScope(Response.class))
+ .in(RequestScoped.class);
+ /** server sent event */
+ binder.bind(Sse.class).toProvider(Providers.outOfScope(Sse.class))
+ .in(RequestScoped.class);
+
+ binder.bind(Session.class).toProvider(Providers.outOfScope(Session.class))
+ .in(RequestScoped.class);
+
+ /** def err */
+ ehandlers.addBinding().toInstance(new Err.DefHandler());
+
+ /** executors. */
+ executors.forEach(it -> it.accept(binder));
+ };
+
+ Injector injector = injectorFactory.apply(stage, joobyModule);
+ if (apprefs != null) {
+ apprefs.forEach(app -> app.injector = injector);
+ apprefs.clear();
+ apprefs = null;
+ }
+
+ onStart.addAll(0, finalEnv.startTasks());
+ onStarted.addAll(0, finalEnv.startedTasks());
+ onStop.addAll(finalEnv.stopTasks());
+
+ // clear bag and freeze it
+ this.bag.clear();
+ this.bag = ImmutableSet.of();
+ this.executors.clear();
+ this.executors = ImmutableList.of();
+
+ return injector;
+ }
+
+ private void xss(final Env env) {
+ Escaper ufe = UrlEscapers.urlFragmentEscaper();
+ Escaper fpe = UrlEscapers.urlFormParameterEscaper();
+ Escaper pse = UrlEscapers.urlPathSegmentEscaper();
+ Escaper html = HtmlEscapers.htmlEscaper();
+
+ env.xss("urlFragment", ufe::escape)
+ .xss("formParam", fpe::escape)
+ .xss("pathSegment", pse::escape)
+ .xss("html", html::escape);
+ }
+
+ private static Provider session(final Config $session,
+ final Session.Definition session) {
+ return () -> {
+ // save interval
+ session.saveInterval(session.saveInterval()
+ .orElse($session.getDuration("saveInterval", TimeUnit.MILLISECONDS)));
+
+ // build cookie
+ Cookie.Definition source = session.cookie();
+
+ source.name(source.name()
+ .orElse($session.getString("cookie.name")));
+
+ if (!source.comment().isPresent() && $session.hasPath("cookie.comment")) {
+ source.comment($session.getString("cookie.comment"));
+ }
+ if (!source.domain().isPresent() && $session.hasPath("cookie.domain")) {
+ source.domain($session.getString("cookie.domain"));
+ }
+ source.httpOnly(source.httpOnly()
+ .orElse($session.getBoolean("cookie.httpOnly")));
+
+ Object maxAge = $session.getAnyRef("cookie.maxAge");
+ if (maxAge instanceof String) {
+ maxAge = $session.getDuration("cookie.maxAge", TimeUnit.SECONDS);
+ }
+ source.maxAge(source.maxAge()
+ .orElse(((Number) maxAge).intValue()));
+
+ source.path(source.path()
+ .orElse($session.getString("cookie.path")));
+
+ source.secure(source.secure()
+ .orElse($session.getBoolean("cookie.secure")));
+
+ return session;
+ };
+ }
+
+ private static Throwing.Consumer super Object> bindService(Logger log,
+ final Set src,
+ final Config conf,
+ final Env env,
+ final RouteMetadata rm,
+ final Binder binder,
+ final Multibinder definitions,
+ final Multibinder sockets,
+ final Multibinder ehandlers,
+ final Multibinder parsers,
+ final Multibinder renderers,
+ final Set routeClasses,
+ final boolean caseSensitiveRouting) {
+ return it -> {
+ if (it instanceof Jooby.Module) {
+ int from = src.size();
+ install(log, (Jooby.Module) it, env, conf, binder);
+ int to = src.size();
+ // collect any route a module might add
+ if (to > from) {
+ List elements = normalize(new ArrayList<>(src).subList(from, to), env, rm,
+ caseSensitiveRouting);
+ for (Object e : elements) {
+ bindService(log, src,
+ conf,
+ env,
+ rm,
+ binder,
+ definitions,
+ sockets,
+ ehandlers,
+ parsers,
+ renderers,
+ routeClasses, caseSensitiveRouting).accept(e);
+ }
+ }
+ } else if (it instanceof Route.Definition) {
+ Route.Definition rdef = (Definition) it;
+ Route.Filter h = rdef.filter();
+ if (h instanceof Route.MethodHandler) {
+ Class> routeClass = ((Route.MethodHandler) h).implementingClass();
+ if (routeClasses.add(routeClass)) {
+ binder.bind(routeClass);
+ }
+ definitions.addBinding().toInstance(rdef);
+ } else {
+ definitions.addBinding().toInstance(rdef);
+ }
+ } else if (it instanceof WebSocket.Definition) {
+ sockets.addBinding().toInstance((WebSocket.Definition) it);
+ } else if (it instanceof Parser) {
+ parsers.addBinding().toInstance((Parser) it);
+ } else if (it instanceof Renderer) {
+ renderers.addBinding().toInstance((Renderer) it);
+ } else {
+ ehandlers.addBinding().toInstance((Err.Handler) it);
+ }
+ };
+ }
+
+ private static List modconf(final Collection bag) {
+ return bag.stream()
+ .filter(it -> it instanceof Jooby.Module)
+ .map(it -> ((Jooby.Module) it).config())
+ .filter(c -> !c.isEmpty())
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Test if the application is up and running.
+ *
+ * @return True if the application is up and running.
+ */
+ public boolean isStarted() {
+ return started.get();
+ }
+
+ /**
+ * Stop the application, fire the {@link #onStop(Throwing.Runnable)} event and shutdown the
+ * web server.
+ *
+ * Stop listeners run in the order they were added:
+ *
+ * {@code
+ * {
+ *
+ * onStop(() -> System.out.println("first"));
+ *
+ * onStop(() -> System.out.println("second"));
+ *
+ * ...
+ * }
+ * }
+ *
+ *
+ */
+ public void stop() {
+ if (started.compareAndSet(true, false)) {
+ Logger log = logger(this);
+
+ fireStop(this, log, onStop);
+ if (injector != null) {
+ try {
+ injector.getInstance(Server.class).stop();
+ } catch (Throwable ex) {
+ log.debug("server.stop() resulted in exception", ex);
+ }
+ }
+ injector = null;
+
+ log.info("Stopped");
+ }
+ }
+
+ private static void fireStop(final Jooby app, final Logger log,
+ final List> onStop) {
+ // stop services
+ onStop.forEach(c -> Try.run(() -> c.accept(app))
+ .onFailure(x -> log.error("shutdown of {} resulted in error", c, x)));
+ }
+
+ /**
+ * Build configuration properties, it configure system, app and modules properties.
+ *
+ * @param source Source config to use.
+ * @param args Args conf.
+ * @param modules List of modules.
+ * @return A configuration properties ready to use.
+ */
+ private Config buildConfig(final Config source, final Config args,
+ final List modules) {
+ // normalize tmpdir
+ Config system = ConfigFactory.systemProperties();
+ Config tmpdir = source.hasPath("java.io.tmpdir") ? source : system;
+
+ // system properties
+ system = system
+ // file encoding got corrupted sometimes, override it.
+ .withValue("file.encoding", fromAnyRef(System.getProperty("file.encoding")))
+ .withValue("java.io.tmpdir",
+ fromAnyRef(Paths.get(tmpdir.getString("java.io.tmpdir")).normalize().toString()));
+
+ // set module config
+ Config moduleStack = ConfigFactory.empty();
+ for (Config module : ImmutableList.copyOf(modules).reverse()) {
+ moduleStack = moduleStack.withFallback(module);
+ }
+
+ String env = Arrays.asList(system, args, source).stream()
+ .filter(it -> it.hasPath("application.env"))
+ .findFirst()
+ .map(c -> c.getString("application.env"))
+ .orElse("dev");
+
+ String cpath = Arrays.asList(system, args, source).stream()
+ .filter(it -> it.hasPath("application.path"))
+ .findFirst()
+ .map(c -> c.getString("application.path"))
+ .orElse("/");
+
+ Config envconf = envConf(source, env);
+
+ // application.[env].conf -> application.conf
+ Config conf = envconf.withFallback(source);
+
+ return system
+ .withFallback(args)
+ .withFallback(conf)
+ .withFallback(moduleStack)
+ .withFallback(MediaType.types)
+ .withFallback(defaultConfig(conf, Route.normalize(cpath)))
+ .resolve();
+ }
+
+ /**
+ * Build a conf from arguments.
+ *
+ * @param args Application arguments.
+ * @return A conf.
+ */
+ static Config args(final String[] args) {
+ if (args == null || args.length == 0) {
+ return ConfigFactory.empty();
+ }
+ Map conf = new HashMap<>();
+ for (String arg : args) {
+ String[] values = arg.split("=");
+ String name;
+ String value;
+ if (values.length == 2) {
+ name = values[0];
+ value = values[1];
+ } else {
+ name = "application.env";
+ value = values[0];
+ }
+ if (name.indexOf(".") == -1) {
+ conf.put("application." + name, value);
+ }
+ conf.put(name, value);
+ }
+ return ConfigFactory.parseMap(conf, "args");
+ }
+
+ /**
+ * Build a env config: [application].[env].[conf].
+ * Stack looks like
+ *
+ *
+ * (file://[origin].[env].[conf])?
+ * (cp://[origin].[env].[conf])?
+ * file://application.[env].[conf]
+ * /application.[env].[conf]
+ *
+ *
+ * @param source App source to use.
+ * @param env Application env.
+ * @return A config env.
+ */
+ private Config envConf(final Config source, final String env) {
+ String name = Optional.ofNullable(this.confname).orElse(source.origin().resource());
+ Config result = ConfigFactory.empty();
+ if (name != null) {
+ // load [resource].[env].[ext]
+ int dot = name.lastIndexOf('.');
+ name = name.substring(0, dot);
+ } else {
+ name = "application";
+ }
+ String envconfname = name + "." + env + ".conf";
+ Config envconf = fileConfig(envconfname);
+ Config appconf = fileConfig(name + ".conf");
+ return result
+ // file system:
+ .withFallback(envconf)
+ .withFallback(appconf)
+ // classpath:
+ .withFallback(ConfigFactory.parseResources(envconfname));
+ }
+
+ /**
+ * Config from file system.
+ *
+ * @param fname A file name.
+ * @return A config for the file name.
+ */
+ static Config fileConfig(final String fname) {
+ // TODO: sanitization of arguments
+ File dir = new File(System.getProperty("user.dir"));
+ // TODO: sanitization of arguments
+ File froot = new File(dir, fname);
+ if (froot.exists()) {
+ return ConfigFactory.parseFile(froot);
+ } else {
+ // TODO: sanitization of arguments
+ File fconfig = new File(new File(dir, "conf"), fname);
+ if (fconfig.exists()) {
+ return ConfigFactory.parseFile(fconfig);
+ }
+ }
+ return ConfigFactory.empty();
+ }
+
+ /**
+ * Build default application.* properties.
+ *
+ * @param conf A source config.
+ * @param cpath Application path.
+ * @return default properties.
+ */
+ private Config defaultConfig(final Config conf, final String cpath) {
+ String ns = Optional.ofNullable(getClass().getPackage())
+ .map(Package::getName)
+ .orElse("default." + getClass().getName());
+ String[] parts = ns.split("\\.");
+ String appname = parts[parts.length - 1];
+
+ // locale
+ final List locales;
+ if (!conf.hasPath("application.lang")) {
+ locales = Optional.ofNullable(this.languages)
+ .map(langs -> LocaleUtils.parse(Joiner.on(",").join(langs)))
+ .orElse(ImmutableList.of(Locale.getDefault()));
+ } else {
+ locales = LocaleUtils.parse(conf.getString("application.lang"));
+ }
+ Locale locale = locales.iterator().next();
+ String lang = locale.toLanguageTag();
+
+ // time zone
+ final String tz;
+ if (!conf.hasPath("application.tz")) {
+ tz = Optional.ofNullable(zoneId).orElse(ZoneId.systemDefault()).getId();
+ } else {
+ tz = conf.getString("application.tz");
+ }
+
+ // number format
+ final String nf;
+ if (!conf.hasPath("application.numberFormat")) {
+ nf = Optional.ofNullable(numberFormat)
+ .orElseGet(() -> ((DecimalFormat) DecimalFormat.getInstance(locale)).toPattern());
+ } else {
+ nf = conf.getString("application.numberFormat");
+ }
+
+ int processors = Runtime.getRuntime().availableProcessors();
+ String version = Optional.ofNullable(getClass().getPackage())
+ .map(Package::getImplementationVersion)
+ .filter(Objects::nonNull)
+ .orElse("0.0.0");
+ Config defs = ConfigFactory.parseResources(Jooby.class, "jooby.conf")
+ .withValue("contextPath", fromAnyRef(cpath.equals("/") ? "" : cpath))
+ .withValue("application.name", fromAnyRef(appname))
+ .withValue("application.version", fromAnyRef(version))
+ .withValue("application.class", fromAnyRef(classname))
+ .withValue("application.ns", fromAnyRef(ns))
+ .withValue("application.lang", fromAnyRef(lang))
+ .withValue("application.tz", fromAnyRef(tz))
+ .withValue("application.numberFormat", fromAnyRef(nf))
+ .withValue("server.http2.enabled", fromAnyRef(http2))
+ .withValue("runtime.processors", fromAnyRef(processors))
+ .withValue("runtime.processors-plus1", fromAnyRef(processors + 1))
+ .withValue("runtime.processors-plus2", fromAnyRef(processors + 2))
+ .withValue("runtime.processors-x2", fromAnyRef(processors * 2))
+ .withValue("runtime.processors-x4", fromAnyRef(processors * 4))
+ .withValue("runtime.processors-x8", fromAnyRef(processors * 8))
+ .withValue("runtime.concurrencyLevel", fromAnyRef(Math.max(4, processors)))
+ .withValue("server.threads.Min", fromAnyRef(Math.max(4, processors)))
+ .withValue("server.threads.Max", fromAnyRef(Math.max(32, processors * 8)));
+
+ if (charset != null) {
+ defs = defs.withValue("application.charset", fromAnyRef(charset.name()));
+ }
+ if (port != null) {
+ defs = defs.withValue("application.port", fromAnyRef(port));
+ }
+ if (securePort != null) {
+ defs = defs.withValue("application.securePort", fromAnyRef(securePort));
+ }
+ if (dateFormat != null) {
+ defs = defs.withValue("application.dateFormat", fromAnyRef(dateFormat));
+ }
+ return defs;
+ }
+
+ /**
+ * Install a {@link Jooby.Module}.
+ *
+ * @param log Logger.
+ * @param module The module to install.
+ * @param env Application env.
+ * @param config The configuration object.
+ * @param binder A Guice binder.
+ * @throws Throwable If module bootstrap fails.
+ */
+ private static void install(final Logger log, final Jooby.Module module, final Env env, final Config config,
+ final Binder binder) throws Throwable {
+ module.configure(env, config, binder);
+ try {
+ binder.install(ProviderMethodsModule.forObject(module));
+ } catch (NoClassDefFoundError x) {
+ // Allow dynamic linking of optional dependencies (required by micrometer module), we ignore
+ // missing classes here, if there is a missing class Jooby is going to fails early (not here)
+ log.debug("ignoring class not found from guice provider method", x);
+ }
+ }
+
+ /**
+ * Bind a {@link Config} and make it available for injection. Each property of the config is also
+ * binded it and ready to be injected with {@link javax.inject.Named}.
+ *
+ * @param binder Guice binder.
+ * @param config App config.
+ */
+ @SuppressWarnings("unchecked")
+ private void bindConfig(final Binder binder, final Config config) {
+ // root nodes
+ traverse(binder, "", config.root());
+
+ // terminal nodes
+ for (Entry entry : config.entrySet()) {
+ String name = entry.getKey();
+ Named named = Names.named(name);
+ Object value = entry.getValue().unwrapped();
+ if (value instanceof List) {
+ List values = (List) value;
+ Type listType = values.size() == 0
+ ? String.class
+ : Types.listOf(values.iterator().next().getClass());
+ Key key = (Key) Key.get(listType, Names.named(name));
+ binder.bind(key).toInstance(values);
+ } else {
+ binder.bindConstant().annotatedWith(named).to(value.toString());
+ }
+ }
+ // bind config
+ binder.bind(Config.class).toInstance(config);
+ }
+
+ private static void traverse(final Binder binder, final String p, final ConfigObject root) {
+ root.forEach((n, v) -> {
+ if (v instanceof ConfigObject) {
+ ConfigObject child = (ConfigObject) v;
+ String path = p + n;
+ Named named = Names.named(path);
+ binder.bind(Config.class).annotatedWith(named).toInstance(child.toConfig());
+ traverse(binder, path + ".", child);
+ }
+ });
+ }
+
+ private static Predicate envpredicate(final String candidate) {
+ return env -> env.equalsIgnoreCase(candidate) || candidate.equals("*");
+ }
+
+ static String logback(final Config conf) {
+ // Avoid warning message from logback when multiples files are present
+ String logback;
+ if (conf.hasPath("logback.configurationFile")) {
+ logback = conf.getString("logback.configurationFile");
+ } else {
+ String env = conf.hasPath("application.env") ? conf.getString("application.env") : null;
+ ImmutableList.Builder files = ImmutableList.builder();
+ // TODO: sanitization of arguments
+ File userdir = new File(System.getProperty("user.dir"));
+ File confdir = new File(userdir, "conf");
+ if (env != null) {
+ files.add(new File(userdir, "logback." + env + ".xml"));
+ files.add(new File(confdir, "logback." + env + ".xml"));
+ }
+ files.add(new File(userdir, "logback.xml"));
+ files.add(new File(confdir, "logback.xml"));
+ logback = files.build()
+ .stream()
+ .filter(File::exists)
+ .map(File::getAbsolutePath)
+ .findFirst()
+ .orElseGet(() -> {
+ return Optional.ofNullable(Jooby.class.getResource("/logback." + env + ".xml"))
+ .map(Objects::toString)
+ .orElse("logback.xml");
+ });
+ }
+ return logback;
+ }
+
+ private static Logger logger(final Jooby app) {
+ return LoggerFactory.getLogger(app.getClass());
+ }
+
+ private Route.AssetDefinition configureAssetHandler(final Route.AssetDefinition handler) {
+ onStart(r -> {
+ Config conf = r.require(Config.class);
+ handler
+ .cdn(conf.getString("assets.cdn"))
+ .lastModified(conf.getBoolean("assets.lastModified"))
+ .etag(conf.getBoolean("assets.etag"))
+ .maxAge(conf.getString("assets.cache.maxAge"));
+ });
+ return handler;
+ }
+
+ /**
+ * Class name is this, except for script bootstrap.
+ *
+ * @param name Default classname.
+ * @return Classname.
+ */
+ private String classname(String name) {
+ if (name.equals(Jooby.class.getName()) || name.equals("org.jooby.Kooby")) {
+ return SourceProvider.INSTANCE.get()
+ .map(StackTraceElement::getClassName)
+ .orElse(name);
+ }
+ return name;
+ }
+}
diff --git a/jooby/src/main/java/org/jooby/LifeCycle.java b/jooby/src/main/java/org/jooby/LifeCycle.java
new file mode 100644
index 00000000..b01fd8b2
--- /dev/null
+++ b/jooby/src/main/java/org/jooby/LifeCycle.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2026 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jooby;
+
+import com.typesafe.config.Config;
+import org.jooby.funzy.Throwing;
+import org.jooby.funzy.Try;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Optional;
+
+/**
+ * life cycle
+ *
+ * Listen for application start and stop events. Useful for starting/stopping services.
+ *
+ *
+ * onStart/onStop events
+ *
+ * Start/stop callbacks are accessible via application:
+ *
+ * {@code
+ * {
+ * onStart(() -> {
+ * log.info("starting app");
+ * });
+ *
+ * onStop(() -> {
+ * log.info("stopping app");
+ * });
+ * }
+ * }
+ *
+ *
+ * Or via module:
+ *
+ *
+ * {@code
+ * public class MyModule implements Jooby.Module {
+ *
+ * public void configure(Env env, Config conf, Binder binder) {
+ * env.onStart(() -> {
+ * log.info("starting module");
+ * });
+ *
+ * env.onStop(() -> {
+ * log.info("stopping module");
+ * });
+ * }
+ * }
+ * }
+ *
+ * callbacks order
+ *
+ * Callback order is preserved:
+ *
+ *
+ * {@code
+ * {
+ * onStart(() -> {
+ * log.info("first");
+ * });
+ *
+ * onStart(() -> {
+ * log.info("second");
+ * });
+ *
+ * onStart(() -> {
+ * log.info("third");
+ * });
+ * }
+ * }
+ *
+ * Order is useful for service dependencies, like ServiceB should be started after ServiceA.
+ *
+ *
+ * service registry
+ *
+ * You can also request for a service and start or stop it:
+ *
+ *
+ * {@code
+ * {
+ * onStart(registry -> {
+ * MyService service = registry.require(MyService.class);
+ * service.start();
+ * });
+ *
+ * onStop(registry -> {
+ * MyService service = registry.require(MyService.class);
+ * service.stop();
+ * });
+ * }
+ * }
+ *
+ * PostConstruct/PreDestroy annotations
+ *
+ * If you prefer the annotation way... you can too:
+ *
+ *
+ * {@code
+ *
+ * @Singleton
+ * public class MyService {
+ *
+ * @PostConstruct
+ * public void start() {
+ * }
+ *
+ * @PreDestroy
+ * public void stop() {
+ * }
+ * }
+ *
+ * App.java:
+ *
+ * {
+ * lifeCycle(MyService.class);
+ * }
+ *
+ * }
+ *
+ *
+ * It works as expected just make sure MyService is a Singleton
+ * object.
+ *
+ *
+ * @author edgar
+ * @since 1.0.0.CR3
+ */
+public interface LifeCycle {
+
+ /**
+ * Find a single method annotated with the given annotation in the provided type.
+ *
+ * @param rawType The type to look for a method.
+ * @param annotation Annotation to look for.
+ * @return A callback to the method. Or empty.
+ */
+ static Optional> lifeCycleAnnotation(final Class> rawType,
+ final Class extends Annotation> annotation) {
+ for (Method method : rawType.getDeclaredMethods()) {
+ if (method.getAnnotation(annotation) != null) {
+ int mods = method.getModifiers();
+ if (Modifier.isStatic(mods)) {
+ throw new IllegalArgumentException(annotation.getSimpleName()
+ + " method should not be static: " + method);
+ }
+ if (!Modifier.isPublic(mods)) {
+ throw new IllegalArgumentException(annotation.getSimpleName()
+ + " method must be public: " + method);
+ }
+ if (method.getParameterCount() > 0) {
+ throw new IllegalArgumentException(annotation.getSimpleName()
+ + " method should not accept arguments: " + method);
+ }
+ if (method.getReturnType() != void.class) {
+ throw new IllegalArgumentException(annotation.getSimpleName()
+ + " method should not return anything: " + method);
+ }
+ return Optional.of(owner -> {
+ Try.run(() -> {
+ method.setAccessible(true);
+ method.invoke(owner);
+ }).unwrap(InvocationTargetException.class)
+ .throwException();
+ });
+ }
+ }
+ return Optional.empty();
+ }
+
+ ;
+
+ /**
+ * Add to lifecycle the given service. Any method annotated with {@link PostConstruct} or
+ * {@link PreDestroy} will be executed at application startup or shutdown time.
+ *
+ * The service must be a Singleton object.
+ *
+ * {@code
+ *
+ * @Singleton
+ * public class MyService {
+ *
+ * @PostConstruct
+ * public void start() {
+ * }
+ *
+ * @PreDestroy
+ * public void stop() {
+ * }
+ * }
+ *
+ * App.java:
+ *
+ * {
+ * lifeCycle(MyService.class);
+ * }
+ *
+ * }
+ *
+ * You should ONLY call this method while the application is been initialized or while
+ * {@link Jooby.Module#configure(Env, Config, com.google.inject.Binder)} is been executed.
+ *
+ * The behavior of this method once application has been initialized is undefined.
+ *
+ * @param service Service type. Must be a singleton object.
+ * @return This instance.
+ */
+ @Nonnull
+ default LifeCycle lifeCycle(final Class> service) {
+ lifeCycleAnnotation(service, PostConstruct.class)
+ .ifPresent(it -> onStart(app -> it.accept(app.require(service))));
+
+ lifeCycleAnnotation(service, PreDestroy.class)
+ .ifPresent(it -> onStop(app -> it.accept(app.require(service))));
+ return this;
+ }
+
+ /**
+ * Add a start lifecycle event, useful for initialize and/or start services at startup time.
+ *
+ * You should ONLY call this method while the application is been initialized or while
+ * {@link Jooby.Module#configure(Env, Config, com.google.inject.Binder)}.
+ *
+ * The behavior of this method once application has been initialized is undefined.
+ *
+ * @param task Task to run.
+ * @return This env.
+ */
+ @Nonnull
+ LifeCycle onStart(Throwing.Consumer task);
+
+ /**
+ * Add a started lifecycle event. Started callbacks are executed when the application is ready:
+ * modules and servers has been started.
+ *
+ * You should ONLY call this method while the application is been initialized or while
+ * {@link Jooby.Module#configure(Env, Config, com.google.inject.Binder)}.
+ *
+ * The behavior of this method once application has been initialized is undefined.
+ *
+ * @param task Task to run.
+ * @return This env.
+ */
+ @Nonnull
+ LifeCycle onStarted(Throwing.Consumer task);
+
+ /**
+ * Add a start lifecycle event, useful for initialize and/or start services at startup time.
+ *
+ * You should ONLY call this method while the application is been initialized or from
+ * {@link Jooby.Module#configure(Env, Config, com.google.inject.Binder)}.
+ *
+ * The behavior of this method once application has been initialized is undefined.
+ *
+ * @param task Task to run.
+ * @return This env.
+ */
+ @Nonnull
+ default LifeCycle onStart(final Throwing.Runnable task) {
+ return onStart(app -> task.run());
+ }
+
+ /**
+ * Add a started lifecycle event. Started callbacks are executed when the application is ready:
+ * modules and servers has been started.
+ *
+ * You should ONLY call this method while the application is been initialized or while
+ * {@link Jooby.Module#configure(Env, Config, com.google.inject.Binder)}.
+ *
+ * The behavior of this method once application has been initialized is undefined.
+ *
+ * @param task Task to run.
+ * @return This env.
+ */
+ @Nonnull
+ default LifeCycle onStarted(final Throwing.Runnable task) {
+ return onStarted(app -> task.run());
+ }
+
+ /**
+ * Add a stop lifecycle event, useful for cleanup and/or stop service at stop time.
+ *
+ * You should ONLY call this method while the application is been initialized or from
+ * {@link Jooby.Module#configure(Env, Config, com.google.inject.Binder)}.
+ *
+ * The behavior of this method once application has been initialized is undefined.
+ *
+ * @param task Task to run.
+ * @return This env.
+ */
+ @Nonnull
+ default LifeCycle onStop(final Throwing.Runnable task) {
+ return onStop(app -> task.run());
+ }
+
+ /**
+ * Add a stop lifecycle event, useful for cleanup and/or stop service at stop time.
+ *
+ * You should ONLY call this method while the application is been initialized or from
+ * {@link Jooby.Module#configure(Env, Config, com.google.inject.Binder)}.
+ *
+ * The behaviour of this method once application has been initialized is undefined.
+ *
+ * @param task Task to run.
+ * @return This env.
+ */
+ @Nonnull
+ LifeCycle onStop(Throwing.Consumer task);
+
+}
diff --git a/jooby/src/main/java/org/jooby/MediaType.java b/jooby/src/main/java/org/jooby/MediaType.java
new file mode 100644
index 00000000..c23881b7
--- /dev/null
+++ b/jooby/src/main/java/org/jooby/MediaType.java
@@ -0,0 +1,646 @@
+/*
+ * Copyright 2026 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jooby;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+
+/**
+ * An immutable implementation of HTTP media types (a.k.a mime types).
+ *
+ * @author edgar
+ * @since 0.1.0
+ */
+public class MediaType implements Comparable {
+
+ /**
+ * A media type matcher.
+ *
+ * @see MediaType#matcher(org.jooby.MediaType)
+ * @see MediaType#matcher(java.util.List)
+ */
+ public static class Matcher {
+
+ /**
+ * The source of media types.
+ */
+ private Iterable acceptable;
+
+ /**
+ * Creates a new {@link Matcher}.
+ *
+ * @param acceptable The source to compare with.
+ */
+ Matcher(final Iterable acceptable) {
+ this.acceptable = acceptable;
+ }
+
+ /**
+ * Given:
+ *
+ *
+ * text/html, application/xhtml; {@literal *}/{@literal *}
+ *
+ *
+ *
+ * matches(text/html) // true through text/html
+ * matches(application/json) // true through {@literal *}/{@literal *}
+ *
+ *
+ * @param candidate A candidate media type. Required.
+ * @return True if the matcher matches the given media type.
+ */
+ public boolean matches(final MediaType candidate) {
+ return doFirst(ImmutableList.of(candidate)).isPresent();
+ }
+
+ /**
+ * Given:
+ *
+ *
+ * text/html, application/xhtml; {@literal *}/{@literal *}
+ *
+ *
+ *
+ * matches(text/html) // true through text/html
+ * matches(application/json) // true through {@literal *}/{@literal *}
+ *
+ *
+ * @param candidates One ore more candidates media type. Required.
+ * @return True if the matcher matches the given media type.
+ */
+ public boolean matches(final List candidates) {
+ return filter(candidates).size() > 0;
+ }
+
+ /**
+ * Given:
+ *
+ *
+ * text/html, application/xhtml; {@literal *}/{@literal *}
+ *
+ *
+ *
+ * first(text/html) // returns text/html
+ * first(application/json) // returns application/json
+ *
+ *
+ * @param candidate A candidate media type. Required.
+ * @return A first most relevant media type or an empty optional.
+ */
+ public Optional first(final MediaType candidate) {
+ return first(ImmutableList.of(candidate));
+ }
+
+ /**
+ * Given:
+ *
+ *
+ * text/html, application/xhtml; {@literal *}/{@literal *}
+ *
+ *
+ *
+ * first(text/html) // returns text/html
+ * first(application/json) // returns application/json
+ *
+ *
+ * @param candidates One ore more candidates media type. Required.
+ * @return A first most relevant media type or an empty optional.
+ */
+ public Optional first(final List candidates) {
+ return doFirst(candidates);
+ }
+
+ /**
+ * Filter the accepted types and keep the most specifics media types.
+ *
+ * Given:
+ *
+ *
+ * text/html, application/xhtml; {@literal *}/{@literal *}
+ *
+ *
+ *
+ * filter(text/html) // returns text/html
+ * first(application/json) // returns application/json
+ * filter(text/html, application/json) // returns text/html and application/json
+ *
+ *
+ * @param types A types to filter
+ * @return Filtered types that matches the given types ordered from more specific to less
+ * specific.
+ */
+ public List filter(final List types) {
+ checkArgument(types != null && types.size() > 0, "Media types are required");
+ ImmutableList.Builder result = ImmutableList.builder();
+ final List sortedTypes;
+ if (types.size() == 1) {
+ sortedTypes = ImmutableList.of(types.get(0));
+ } else {
+ sortedTypes = new ArrayList<>(types);
+ Collections.sort(sortedTypes);
+ }
+ for (MediaType accept : acceptable) {
+ for (MediaType candidate : sortedTypes) {
+ if (accept.matches(candidate)) {
+ result.add(candidate);
+ }
+ }
+ }
+ return result.build();
+ }
+
+ /**
+ * Given:
+ *
+ *
+ * text/html, application/xhtml; {@literal *}/{@literal *}
+ *
+ *
+ *
+ * first(text/html) -> returns text/html
+ * first(application/json) -> returns application/json
+ *
+ *
+ * @param candidates One ore more candidates media type. Required.
+ * @return A first most relevant media type or an empty optional.
+ */
+ private Optional doFirst(final List candidates) {
+ List result = filter(candidates);
+ return result.size() == 0 ? Optional.empty() : Optional.of(result.get(0));
+ }
+ }
+
+ /**
+ * Default parameters.
+ */
+ private static final Map DEFAULT_PARAMS = ImmutableMap.of("q", "1");
+
+ /**
+ * A JSON media type.
+ */
+ public static final MediaType json = new MediaType("application", "json");
+
+ private static final MediaType jsonLike = new MediaType("application", "*+json");
+
+ /**
+ * Any text media type.
+ */
+ public static final MediaType text = new MediaType("text", "*");
+
+ /**
+ * Text plain media type.
+ */
+ public static final MediaType plain = new MediaType("text", "plain");
+
+ /**
+ * Stylesheet media type.
+ */
+ public static final MediaType css = new MediaType("text", "css");
+
+ /**
+ * Javascript media types.
+ */
+ public static final MediaType js = new MediaType("application", "javascript");
+
+ /**
+ * HTML media type.
+ */
+ public static final MediaType html = new MediaType("text", "html");
+
+ /**
+ * The default binary media type.
+ */
+ public static final MediaType octetstream = new MediaType("application", "octet-stream");
+
+ /**
+ * Any media type.
+ */
+ public static final MediaType all = new MediaType("*", "*");
+
+ /** Any media type. */
+ public static final List ALL = ImmutableList.of(MediaType.all);
+
+ /** Form multipart-data media type. */
+ public static final MediaType multipart = new MediaType("multipart", "form-data");
+
+ /** Form url encoded. */
+ public static final MediaType form = new MediaType("application", "x-www-form-urlencoded");
+
+ /** Xml media type. */
+ public static final MediaType xml = new MediaType("application", "xml");
+
+ /** Server sent event type. */
+ public static final MediaType sse = new MediaType("text", "event-stream");
+
+ /** Xml like media type. */
+ private static final MediaType xmlLike = new MediaType("application", "*+xml");
+
+ /**
+ * Track the type of this media type.
+ */
+ private final String type;
+
+ /**
+ * Track the subtype of this media type.
+ */
+ private final String subtype;
+
+ /**
+ * Track the media type parameters.
+ */
+ private final Map params;
+
+ /**
+ * True for wild-card types.
+ */
+ private final boolean wildcardType;
+
+ /**
+ * True for wild-card sub-types.
+ */
+ private final boolean wildcardSubtype;
+
+ /** Name . */
+ private String name;
+
+ private int hc;
+
+ /**
+ * Alias for most used types.
+ */
+ private static final ConcurrentHashMap> cache = new ConcurrentHashMap<>();
+
+ static {
+ cache.put("html", ImmutableList.of(html));
+ cache.put("json", ImmutableList.of(json));
+ cache.put("css", ImmutableList.of(css));
+ cache.put("js", ImmutableList.of(js));
+ cache.put("octetstream", ImmutableList.of(octetstream));
+ cache.put("form", ImmutableList.of(form));
+ cache.put("multipart", ImmutableList.of(multipart));
+ cache.put("xml", ImmutableList.of(xml));
+ cache.put("plain", ImmutableList.of(plain));
+ cache.put("*", ALL);
+ }
+
+ static final Config types = ConfigFactory
+ .parseResources("mime.properties")
+ .withFallback(ConfigFactory.parseResources(MediaType.class, "mime.properties"));
+
+ /**
+ * Creates a new {@link MediaType}.
+ *
+ * @param type The primary type. Required.
+ * @param subtype The secondary type. Required.
+ * @param parameters The parameters. Required.
+ */
+ private MediaType(final String type, final String subtype, final Map parameters) {
+ this.type = requireNonNull(type, "A mime type is required.");
+ this.subtype = requireNonNull(subtype, "A mime subtype is required.");
+ this.params = ImmutableMap.copyOf(requireNonNull(parameters, "Parameters are required."));
+ this.wildcardType = "*".equals(type);
+ this.wildcardSubtype = "*".equals(subtype);
+ this.name = type + "/" + subtype;
+
+ hc = 31 + name.hashCode();
+ hc = 31 * hc + params.hashCode();
+ }
+
+ /**
+ * Creates a new {@link MediaType}.
+ *
+ * @param type The primary type. Required.
+ * @param subtype The secondary type. Required.
+ */
+ private MediaType(final String type, final String subtype) {
+ this(type, subtype, DEFAULT_PARAMS);
+ }
+
+ /**
+ * @return The quality of this media type. Default is: 1.
+ */
+ public float quality() {
+ return Float.valueOf(params.get("q"));
+ }
+
+ /**
+ * @return The primary media type.
+ */
+ public String type() {
+ return type;
+ }
+
+ public Map params() {
+ return params;
+ }
+
+ /**
+ * @return The secondary media type.
+ */
+ public String subtype() {
+ return subtype;
+ }
+
+ /**
+ * @return The qualified type {@link #type()}/{@link #subtype()}.
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * @return True, if this type is a well-known text type.
+ */
+ public boolean isText() {
+ if (this.wildcardType) {
+ return false;
+ }
+
+ if (this == text || text.matches(this)) {
+ return true;
+ }
+ if (this == js || js.matches(this)) {
+ return true;
+ }
+ if (jsonLike.matches(this)) {
+ return true;
+ }
+ if (xmlLike.matches(this)) {
+ return true;
+ }
+ if (this.type.equals("application") && this.subtype.equals("hocon")) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int compareTo(final MediaType that) {
+ requireNonNull(that, "A media type is required.");
+ if (this == that) {
+ return 0;
+ }
+ if (this.wildcardType && !that.wildcardType) {
+ return 1;
+ }
+
+ if (that.wildcardType && !this.wildcardType) {
+ return -1;
+ }
+
+ if (this.wildcardSubtype && !that.wildcardSubtype) {
+ return 1;
+ }
+
+ if (that.wildcardSubtype && !this.wildcardSubtype) {
+ return -1;
+ }
+
+ if (!this.type().equals(that.type())) {
+ return 0;
+ }
+
+ int q = Float.compare(that.quality(), this.quality());
+ if (q != 0) {
+ return q;
+ }
+ // param size
+ int paramsSize1 = this.params.size();
+ int paramsSize2 = that.params.size();
+ return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1));
+ }
+
+ /**
+ * @param that A media type to compare to.
+ * @return True, if the given media type matches the current one.
+ */
+ public boolean matches(final MediaType that) {
+ requireNonNull(that, "A media type is required.");
+ if (this == that || this.wildcardType || that.wildcardType) {
+ // same or */*
+ return true;
+ }
+ if (type.equals(that.type)) {
+ if (subtype.equals(that.subtype) || this.wildcardSubtype || that.wildcardSubtype) {
+ return true;
+ }
+ if (subtype.startsWith("*+")) {
+ return that.subtype.endsWith(subtype.substring(2));
+ }
+ if (subtype.startsWith("*")) {
+ return that.subtype.endsWith(subtype.substring(1));
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return True for * / *.
+ */
+ public boolean isAny() {
+ return this.wildcardType && this.wildcardSubtype;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj instanceof MediaType) {
+ MediaType that = (MediaType) obj;
+ return type.equals(that.type) && subtype.equals(that.subtype) && params.equals(that.params);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return hc;
+ }
+
+ @Override
+ public final String toString() {
+ return name;
+ }
+
+ /**
+ * Convert a media type expressed as String into a {@link MediaType}.
+ *
+ * @param type A media type to parse.
+ * @return An immutable {@link MediaType}.
+ * @throws Err.BadMediaType For bad media types.
+ */
+ public static MediaType valueOf(final String type) throws Err.BadMediaType {
+ return parse(type).get(0);
+ }
+
+ private static List parseInternal(final String value) {
+ String[] types = value.split(",");
+ @SuppressWarnings("serial")
+ List result = new ArrayList<>(types.length);
+ for (String type : types) {
+ String[] parts = type.trim().split(";");
+ if (parts[0].equals("*")) {
+ // odd and ugly media type
+ result.add(all);
+ } else {
+ String[] typeAndSubtype = parts[0].split("/");
+ if (typeAndSubtype.length != 2) {
+ throw new Err.BadMediaType(value);
+ }
+ String stype = typeAndSubtype[0].trim();
+ String subtype = typeAndSubtype[1].trim();
+ if ("*".equals(stype) && !"*".equals(subtype)) {
+ throw new Err.BadMediaType(value);
+ }
+ Map parameters = DEFAULT_PARAMS;
+ if (parts.length > 1) {
+ parameters = new LinkedHashMap<>(DEFAULT_PARAMS);
+ for (int i = 1; i < parts.length; i++) {
+ String[] parameter = parts[i].split("=");
+ if (parameter.length > 1) {
+ parameters.put(parameter[0].trim(), parameter[1].trim().toLowerCase());
+ }
+ }
+ }
+ result.add(new MediaType(stype, subtype, parameters));
+ }
+ }
+ if (result.size() > 1) {
+ Collections.sort(result);
+ }
+ return result;
+ }
+
+ /**
+ * Convert one or more media types expressed as String into a {@link MediaType}.
+ *
+ * @param types Media types to parse.
+ * @return An list of immutable {@link MediaType}.
+ * @throws Err.BadMediaType For bad media types.
+ */
+ public static List valueOf(final String... types) throws Err.BadMediaType {
+ requireNonNull(types, "Types are required.");
+ List result = new ArrayList<>();
+ for (String type : types) {
+ result.add(valueOf(type));
+ }
+ return result;
+ }
+
+ /**
+ * Convert a string separated by comma into one or more {@link MediaType}.
+ *
+ * @param value The string separated by commas.
+ * @return One ore more {@link MediaType}.
+ * @throws Err.BadMediaType For bad media types.
+ */
+ public static List parse(final String value) throws Err.BadMediaType {
+ return cache.computeIfAbsent(value, MediaType::parseInternal);
+ }
+
+ /**
+ * Produces a matcher for the given media type.
+ *
+ * @param acceptable The acceptable/target media type.
+ * @return A media type matcher.
+ */
+ public static Matcher matcher(final MediaType acceptable) {
+ return matcher(ImmutableList.of(acceptable));
+ }
+
+ /**
+ * Produces a matcher for the given media types.
+ *
+ * @param acceptable The acceptable/target media types.
+ * @return A media type matcher.
+ */
+ public static Matcher matcher(final List acceptable) {
+ requireNonNull(acceptable, "Acceptables media types are required.");
+ return new Matcher(acceptable);
+ }
+
+ /**
+ * Get a {@link MediaType} for a file.
+ *
+ * @param file A candidate file.
+ * @return A {@link MediaType} or {@link MediaType#octetstream} for unknown file extensions.
+ */
+ public static Optional byFile(final File file) {
+ requireNonNull(file, "A file is required.");
+ return byPath(file.getName());
+ }
+
+ /**
+ * Get a {@link MediaType} for a file path.
+ *
+ * @param path A candidate file path.
+ * @return A {@link MediaType} or empty optional for unknown file extensions.
+ */
+ public static Optional byPath(final Path path) {
+ requireNonNull(path, "A path is required.");
+ return byPath(path.toString());
+ }
+
+ /**
+ * Get a {@link MediaType} for a file path.
+ *
+ * @param path A candidate file path: like myfile.js or /js/myfile.js.
+ * @return A {@link MediaType} or empty optional for unknown file extensions.
+ */
+ public static Optional byPath(final String path) {
+ requireNonNull(path, "A path is required.");
+ int idx = path.lastIndexOf('.');
+ if (idx != -1) {
+ String ext = path.substring(idx + 1);
+ return byExtension(ext);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Get a {@link MediaType} for a file extension.
+ *
+ * @param ext A file extension, like js or css.
+ * @return A {@link MediaType} or empty optional for unknown file extensions.
+ */
+ public static Optional byExtension(final String ext) {
+ requireNonNull(ext, "An ext is required.");
+ String key = "mime." + ext;
+ if (types.hasPath(key)) {
+ return Optional.of(MediaType.valueOf(types.getString("mime." + ext)));
+ }
+ return Optional.empty();
+ }
+
+}
diff --git a/jooby/src/main/java/org/jooby/Mutant.java b/jooby/src/main/java/org/jooby/Mutant.java
new file mode 100644
index 00000000..c51b6d70
--- /dev/null
+++ b/jooby/src/main/java/org/jooby/Mutant.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2026 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jooby;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedSet;
+
+import com.google.common.primitives.Primitives;
+import com.google.inject.TypeLiteral;
+import com.google.inject.util.Types;
+
+import javax.annotation.Nonnull;
+
+/**
+ *
+ * A type safe {@link Mutant} useful for reading parameters/headers/session attributes, etc..
+ *
+ *
+ *
+ * // str param
+ * String value = request.param("str").value();
+ *
+ * // optional str
+ * String value = request.param("str").value("defs");
+ *
+ * // int param
+ * int value = request.param("some").intValue();
+ *
+ * // optional int param
+ * Optional{@literal <}Integer{@literal >} value = request.param("some").toOptional(Integer.class);
+
+ * // list param
+ * List{@literal <}String{@literal >} values = request.param("some").toList(String.class);
+ *
+ * // file upload
+ * Upload upload = request.param("file").to(Upload.class);
+ *
+ *
+ * @author edgar
+ * @since 0.1.0
+ * @see Request#param(String)
+ * @see Request#header(String)
+ */
+public interface Mutant {
+
+ /**
+ * @return Get a boolean when possible.
+ */
+ default boolean booleanValue() {
+ return to(boolean.class);
+ }
+
+ /**
+ * @param value Default value to use.
+ * @return Get a boolean.
+ */
+ default boolean booleanValue(final boolean value) {
+ return toOptional(Boolean.class).orElse(value);
+ }
+
+ /**
+ * @return Get a byte when possible.
+ */
+ default byte byteValue() {
+ return to(byte.class);
+ }
+
+ /**
+ * @param value Default value to use.
+ * @return Get a byte.
+ */
+ default byte byteValue(final byte value) {
+ return toOptional(Byte.class).orElse(value);
+ }
+
+ /**
+ * @return Get a byte when possible.
+ */
+ default char charValue() {
+ return to(char.class);
+ }
+
+ /**
+ * @param value Default value to use.
+ * @return Get a char.
+ */
+ default char charValue(final char value) {
+ return toOptional(Character.class).orElse(value);
+ }
+
+ /**
+ * @return Get a short when possible.
+ */
+ default short shortValue() {
+ return to(short.class);
+ }
+
+ /**
+ * @param value Default value to use.
+ * @return Get a short value.
+ */
+ default short shortValue(final short value) {
+ return toOptional(Short.class).orElse(value);
+ }
+
+ /**
+ * @return Get an integer when possible.
+ */
+ default int intValue() {
+ return to(int.class);
+ }
+
+ /**
+ * @param value Default value to use.
+ * @return Get an integer.
+ */
+ default int intValue(final int value) {
+ return toOptional(Integer.class).orElse(value);
+ }
+
+ /**
+ * @return Get a long when possible.
+ */
+ default long longValue() {
+ return to(long.class);
+ }
+
+ /**
+ * @param value Default value to use.
+ * @return Get a long.
+ */
+ default long longValue(final long value) {
+ return toOptional(Long.class).orElse(value);
+ }
+
+ /**
+ * @return Get a string when possible.
+ */
+ @Nonnull
+ default String value() {
+ return to(String.class);
+ }
+
+ /**
+ * @param value Default value to use.
+ * @return Get a string.
+ */
+ @Nonnull
+ default String value(final String value) {
+ return toOptional().orElse(value);
+ }
+
+ /**
+ * @return Get a float when possible.
+ */
+ default float floatValue() {
+ return to(float.class);
+ }
+
+ /**
+ * @param value Default value to use.
+ * @return Get a float.
+ */
+ default float floatValue(final float value) {
+ return toOptional(Float.class).orElse(value);
+ }
+
+ /**
+ * @return Get a double when possible.
+ */
+ default double doubleValue() {
+ return to(double.class);
+ }
+
+ /**
+ * @param value Default value to use.
+ * @return Get a double.
+ */
+ default double doubleValue(final double value) {
+ return toOptional(Double.class).orElse(value);
+ }
+
+ /**
+ * @param type The enum type.
+ * @param Enum type.
+ * @return Get an enum when possible.
+ */
+ @Nonnull
+ default > T toEnum(final Class type) {
+ return to(type);
+ }
+
+ /**
+ * @param value Default value to use.
+ * @param Enum type.
+ * @return Get an enum.
+ */
+ @SuppressWarnings("unchecked")
+ @Nonnull
+ default > T toEnum(final T value) {
+ Optional optional = (Optional) toOptional(value.getClass());
+ return optional.orElse(value);
+ }
+
+ /**
+ * @param type The element type.
+ * @param List type.
+ * @return Get list of values when possible.
+ */
+ @SuppressWarnings("unchecked")
+ @Nonnull
+ default List toList(final Class type) {
+ return (List) to(TypeLiteral.get(Types.listOf(Primitives.wrap(type))));
+ }
+
+ /**
+ * @return Get list of values when possible.
+ */
+ @Nonnull
+ default List toList() {
+ return toList(String.class);
+ }
+
+ /**
+ * @return Get set of values when possible.
+ */
+ @Nonnull
+ default Set toSet() {
+ return toSet(String.class);
+ }
+
+ /**
+ * @param type The element type.
+ * @param Set type.
+ * @return Get set of values when possible.
+ */
+ @SuppressWarnings("unchecked")
+ @Nonnull
+ default Set