*
* More documentation at jooby.io
*
* @since 2.0.0
* @author edgar
*/
public class Jooby implements Router, Registry {
static final String BASE_PACKAGE = "application.package";
static final String APP_NAME = "___app_name__";
private static final String JOOBY_RUN_HOOK = "___jooby_run_hook__";
private final transient AtomicBoolean started = new AtomicBoolean(true);
private static transient Jooby owner;
private RouterImpl router;
private ExecutionMode mode;
private Path tmpdir;
private List readyCallbacks;
private List startingCallbacks;
private LinkedList stopCallbacks;
private List lateExtensions;
private Environment env;
private RegistryRef registry = new RegistryRef();
private ServerOptions serverOptions;
private EnvironmentOptions environmentOptions;
private List locales;
private boolean lateInit;
private String name;
private String version;
/**
* Creates a new Jooby instance.
*/
public Jooby() {
if (owner == null) {
ClassLoader classLoader = getClass().getClassLoader();
environmentOptions = new EnvironmentOptions().setClassLoader(classLoader);
router = new RouterImpl(classLoader);
stopCallbacks = new LinkedList<>();
startingCallbacks = new ArrayList<>();
readyCallbacks = new ArrayList<>();
lateExtensions = new ArrayList<>();
} else {
copyState(owner, this);
}
}
/**
* Server options or null.
*
* @return Server options or null.
*/
public @Nullable ServerOptions getServerOptions() {
return serverOptions;
}
/**
* Set server options.
*
* @param serverOptions Server options.
* @return This application.
*/
public @Nonnull Jooby setServerOptions(@Nonnull ServerOptions serverOptions) {
this.serverOptions = serverOptions;
return this;
}
@Nonnull @Override public Set getRouterOptions() {
return router.getRouterOptions();
}
@Nonnull @Override public Jooby setRouterOptions(@Nonnull RouterOption... options) {
router.setRouterOptions(options);
return this;
}
/**
* Application environment. If none was set, environment is initialized
* using {@link Environment#loadEnvironment(EnvironmentOptions)}.
*
* @return Application environment.
*/
public @Nonnull Environment getEnvironment() {
if (env == null) {
env = Environment.loadEnvironment(environmentOptions);
}
return env;
}
/**
* Returns the list of supported locales, or
* {@code null} if none set.
*
* @return The supported locales.
*/
@Nullable @Override public List getLocales() {
return locales;
}
/**
* Sets the supported locales.
*
* @param locales The supported locales.
* @return This router.
*/
public Router setLocales(@Nonnull List locales) {
this.locales = requireNonNull(locales);
return this;
}
/**
* Sets the supported locales.
*
* @param locales The supported locales.
* @return This router.
*/
public Router setLocales(Locale... locales) {
return setLocales(Arrays.asList(locales));
}
/**
* Application class loader.
*
* @return Application class loader.
*/
public @Nonnull ClassLoader getClassLoader() {
return env == null ? environmentOptions.getClassLoader() : env.getClassLoader();
}
/**
* Application configuration. It is a shortcut for {@link Environment#getConfig()}.
*
* @return Application config.
*/
public @Nonnull Config getConfig() {
return getEnvironment().getConfig();
}
/**
* Set application environment.
*
* @param environment Application environment.
* @return This application.
*/
public @Nonnull Jooby setEnvironment(@Nonnull Environment environment) {
this.env = environment;
return this;
}
/**
* Set environment options and initialize/overrides the environment.
*
* @param options Environment options.
* @return New environment.
*/
public @Nonnull Environment setEnvironmentOptions(@Nonnull EnvironmentOptions options) {
this.environmentOptions = options;
this.env = Environment.loadEnvironment(
options.setClassLoader(options.getClassLoader(getClass().getClassLoader())));
return this.env;
}
/**
* Event fired before starting router and web-server. Non-lateinit extension are installed at
* this stage.
*
* @param body Start body.
* @return This application.
*/
public @Nonnull Jooby onStarting(@Nonnull SneakyThrows.Runnable body) {
startingCallbacks.add(body);
return this;
}
/**
* Event is fire once all components has been initialized, for example router and web-server
* are up and running, extension installed, etc...
*
* @param body Start body.
* @return This application.
*/
public @Nonnull Jooby onStarted(@Nonnull SneakyThrows.Runnable body) {
readyCallbacks.add(body);
return this;
}
/**
* Stop event is fire at application shutdown time. Useful to execute cleanup task, free
* resources, etc...
*
* @param body Stop body.
* @return This application.
*/
public @Nonnull Jooby onStop(@Nonnull AutoCloseable body) {
stopCallbacks.addFirst(body);
return this;
}
@Nonnull @Override public Jooby setContextPath(@Nonnull String basePath) {
router.setContextPath(basePath);
return this;
}
@Nonnull @Override public String getContextPath() {
return router.getContextPath();
}
/**
* Installs/imports a full application into this one. Applications share services, registry,
* callbacks, etc.
*
* Applications must be instantiated/created lazily via a supplier/factory. This is required due
* to the way an application is usually initialized (constructor initializer).
*
* Working example:
*
*
{@code
*
* install(SubApp::new);
*
* }
*
* Lazy creation configures and setup SubApp correctly, the next example
* won't work:
*
*
{@code
*
* SubApp app = new SubApp();
* install(app); // WONT WORK
*
* }
*
* Note: you must take care of application services across the applications. For example make sure
* you don't configure the same service twice or more in the main and imported applications too.
*
* @param factory Application factory.
* @return This application.
*/
@Nonnull public Jooby install(@Nonnull SneakyThrows.Supplier factory) {
return install("/", factory);
}
/**
* Installs/imports a full application into this one. Applications share services, registry,
* callbacks, etc.
*
* Application must be instantiated/created lazily via a supplier/factory. This is required due
* to the way an application is usually initialized (constructor initializer).
*
* Working example:
*
*
{@code
*
* install("/subapp", SubApp::new);
*
* }
*
* Lazy creation allows to configure and setup SubApp correctly, the next example
* won't work:
*
*
{@code
*
* SubApp app = new SubApp();
* install("/subapp", app); // WONT WORK
*
* }
*
* Note: you must take care of application services across the applications. For example make sure
* you don't configure the same service twice or more in the main and imported applications too.
*
* @param path Path prefix.
* @param factory Application factory.
* @return This application.
*/
@Nonnull
public Jooby install(@Nonnull String path, @Nonnull SneakyThrows.Supplier factory) {
try {
owner = this;
path(path, () -> factory.get());
return this;
} finally {
owner = null;
}
}
/**
* The underlying router.
*
* @return The underlying router.
*/
public @Nonnull Router getRouter() {
return router;
}
@Override public boolean isTrustProxy() {
return router.isTrustProxy();
}
@Nonnull @Override public Jooby setTrustProxy(boolean trustProxy) {
this.router.setTrustProxy(trustProxy);
return this;
}
@Nonnull @Override public Router domain(@Nonnull String domain, @Nonnull Router subrouter) {
this.router.domain(domain, subrouter);
return this;
}
@Nonnull @Override public RouteSet domain(@Nonnull String domain, @Nonnull Runnable body) {
return router.domain(domain, body);
}
@Nonnull @Override
public RouteSet mount(@Nonnull Predicate predicate, @Nonnull Runnable body) {
return router.mount(predicate, body);
}
@Nonnull @Override
public Jooby mount(@Nonnull Predicate predicate, @Nonnull Router subrouter) {
this.router.mount(predicate, subrouter);
return this;
}
@Nonnull @Override public Jooby mount(@Nonnull String path, @Nonnull Router router) {
this.router.mount(path, router);
if (router instanceof Jooby) {
Jooby child = (Jooby) router;
child.registry = this.registry;
}
return this;
}
@Nonnull @Override
public Jooby mount(@Nonnull Router router) {
return mount("/", router);
}
@Nonnull @Override public Jooby mvc(@Nonnull Object router) {
Provider provider = () -> router;
return mvc(router.getClass(), provider);
}
@Nonnull @Override public Jooby mvc(@Nonnull Class router) {
return mvc(router, () -> require(router));
}
@Nonnull @Override
public Jooby mvc(@Nonnull Class router, @Nonnull Provider provider) {
try {
ServiceLoader modules = ServiceLoader.load(MvcFactory.class);
MvcFactory module = stream(modules.spliterator(), false)
.filter(it -> it.supports(router))
.findFirst()
.orElseGet(() ->
/** Make happy IDE incremental build: */
mvcReflectionFallback(router, getClassLoader())
.orElseThrow(() -> Usage.mvcRouterNotFound(router))
);
Extension extension = module.create(provider);
extension.install(this);
return this;
} catch (Exception x) {
throw SneakyThrows.propagate(x);
}
}
@Nonnull @Override
public Route ws(@Nonnull String pattern, @Nonnull WebSocket.Initializer handler) {
return router.ws(pattern, handler);
}
@Nonnull @Override
public Route sse(@Nonnull String pattern, @Nonnull ServerSentEmitter.Handler handler) {
return router.sse(pattern, handler);
}
@Nonnull @Override public List getRoutes() {
return router.getRoutes();
}
@Nonnull @Override public Jooby error(@Nonnull ErrorHandler handler) {
router.error(handler);
return this;
}
@Nonnull @Override public Jooby decorator(@Nonnull Route.Decorator decorator) {
router.decorator(decorator);
return this;
}
@Nonnull @Override public Jooby before(@Nonnull Route.Before before) {
router.before(before);
return this;
}
@Nonnull @Override public Jooby after(@Nonnull Route.After after) {
router.after(after);
return this;
}
@Nonnull @Override public Jooby encoder(@Nonnull MessageEncoder encoder) {
router.encoder(encoder);
return this;
}
@Nonnull @Override public Jooby decoder(@Nonnull MediaType contentType, @Nonnull
MessageDecoder decoder) {
router.decoder(contentType, decoder);
return this;
}
@Nonnull @Override
public Jooby encoder(@Nonnull MediaType contentType, @Nonnull MessageEncoder encoder) {
router.encoder(contentType, encoder);
return this;
}
/**
* Install extension module.
*
* @param extension Extension module.
* @return This application.
*/
@Nonnull public Jooby install(@Nonnull Extension extension) {
if (lateInit || extension.lateinit()) {
lateExtensions.add(extension);
} else {
try {
extension.install(this);
} catch (Exception x) {
throw SneakyThrows.propagate(x);
}
}
return this;
}
@Nonnull @Override public Jooby dispatch(@Nonnull Runnable body) {
router.dispatch(body);
return this;
}
@Nonnull @Override public Jooby dispatch(@Nonnull Executor executor, @Nonnull Runnable action) {
router.dispatch(executor, action);
return this;
}
@Nonnull @Override public RouteSet path(@Nonnull String pattern, @Nonnull Runnable action) {
return router.path(pattern, action);
}
@Nonnull @Override public RouteSet routes(@Nonnull Runnable action) {
return router.routes(action);
}
@Nonnull @Override
public Route route(@Nonnull String method, @Nonnull String pattern,
@Nonnull Route.Handler handler) {
return router.route(method, pattern, handler);
}
@Nonnull @Override public Match match(@Nonnull Context ctx) {
return router.match(ctx);
}
@Override public boolean match(@Nonnull String pattern, @Nonnull String path) {
return router.match(pattern, path);
}
@Nonnull @Override
public Jooby errorCode(@Nonnull Class extends Throwable> type,
@Nonnull StatusCode statusCode) {
router.errorCode(type, statusCode);
return this;
}
@Nonnull @Override public StatusCode errorCode(@Nonnull Throwable cause) {
return router.errorCode(cause);
}
@Nonnull @Override public Executor getWorker() {
return router.getWorker();
}
@Nonnull @Override public Jooby setWorker(@Nonnull Executor worker) {
this.router.setWorker(worker);
if (worker instanceof ExecutorService) {
onStop(((ExecutorService) worker)::shutdown);
}
return this;
}
@Nonnull @Override public Jooby setDefaultWorker(@Nonnull Executor worker) {
this.router.setDefaultWorker(worker);
return this;
}
@Nonnull @Override public Logger getLog() {
return LoggerFactory.getLogger(getClass());
}
@Nonnull @Override public Jooby responseHandler(ResponseHandler handler) {
router.responseHandler(handler);
return this;
}
@Nonnull @Override public ErrorHandler getErrorHandler() {
return router.getErrorHandler();
}
@Nonnull @Override public Path getTmpdir() {
if (tmpdir == null) {
tmpdir = Paths.get(getEnvironment().getConfig().getString("application.tmpdir"))
.toAbsolutePath();
}
return tmpdir;
}
/**
* Set application temporary directory.
*
* @param tmpdir Temp directory.
* @return This application.
*/
public @Nonnull Jooby setTmpdir(@Nonnull Path tmpdir) {
this.tmpdir = tmpdir;
return this;
}
/**
* Application execution mode.
*
* @return Application execution mode.
*/
public @Nonnull ExecutionMode getExecutionMode() {
return mode == null ? ExecutionMode.DEFAULT : mode;
}
/**
* Set application execution mode.
*
* @param mode Application execution mode.
* @return This application.
*/
public @Nonnull Jooby setExecutionMode(@Nonnull ExecutionMode mode) {
this.mode = mode;
return this;
}
@Nonnull @Override public Map getAttributes() {
return router.getAttributes();
}
@Nonnull @Override public Jooby attribute(@Nonnull String key, @Nonnull Object value) {
router.attribute(key, value);
return this;
}
@Nonnull @Override public T attribute(@Nonnull String key) {
return router.attribute(key);
}
@Nonnull @Override public T require(@Nonnull Class type, @Nonnull String name) {
return require(ServiceKey.key(type, name));
}
@Nonnull @Override public T require(@Nonnull Class type) {
return require(ServiceKey.key(type));
}
@Override public @Nonnull T require(@Nonnull ServiceKey key) {
ServiceRegistry services = getServices();
T service = services.getOrNull(key);
if (service == null) {
if (!registry.isSet()) {
throw new RegistryException("Service not found: " + key);
}
String name = key.getName();
return name == null ? registry.get().require(key.getType()) : registry.get().require(key.getType(), name);
}
return service;
}
/**
* Set application registry.
*
* @param registry Application registry.
* @return This application.
*/
@Nonnull public Jooby registry(@Nonnull Registry registry) {
this.registry.set(registry);
return this;
}
@Nonnull @Override public ServiceRegistry getServices() {
return this.router.getServices();
}
/**
* Get base application package. This is the package from where application was initialized
* or the package of a Jooby application sub-class.
*
* @return Base application package.
*/
public @Nullable String getBasePackage() {
return System.getProperty(BASE_PACKAGE,
Optional.ofNullable(getClass().getPackage()).map(Package::getName).orElse(null));
}
@Nonnull @Override public SessionStore getSessionStore() {
return router.getSessionStore();
}
@Nonnull @Override public Jooby setSessionStore(@Nonnull SessionStore store) {
router.setSessionStore(store);
return this;
}
@Nonnull @Override public Jooby executor(@Nonnull String name, @Nonnull Executor executor) {
if (executor instanceof ExecutorService) {
onStop(((ExecutorService) executor)::shutdown);
}
router.executor(name, executor);
return this;
}
@Deprecated @Nonnull @Override public Jooby setFlashCookie(@Nonnull String name) {
router.setFlashCookie(name);
return this;
}
@Nonnull @Override public Cookie getFlashCookie() {
return router.getFlashCookie();
}
@Nonnull @Override public Jooby setFlashCookie(@Nonnull Cookie flashCookie) {
router.setFlashCookie(flashCookie);
return this;
}
@Nonnull @Override public Jooby converter(@Nonnull ValueConverter converter) {
router.converter(converter);
return this;
}
@Nonnull @Override public Jooby converter(@Nonnull BeanConverter converter) {
router.converter(converter);
return this;
}
@Nonnull @Override public List getConverters() {
return router.getConverters();
}
@Nonnull @Override public List getBeanConverters() {
return router.getBeanConverters();
}
@Nonnull @Override public Jooby setHiddenMethod(
@Nonnull Function> provider) {
router.setHiddenMethod(provider);
return this;
}
@Nonnull @Override public Jooby setCurrentUser(
@Nonnull Function provider) {
router.setCurrentUser(provider);
return this;
}
@Nonnull @Override public Jooby setContextAsService(boolean contextAsService) {
router.setContextAsService(contextAsService);
return this;
}
@Nonnull @Override public Jooby setHiddenMethod(@Nonnull String parameterName) {
router.setHiddenMethod(parameterName);
return this;
}
/**
* Start application, find a web server, deploy application, start router, extension modules,
* etc..
*
* @return Server.
*/
public @Nonnull Server start() {
List servers = stream(
spliteratorUnknownSize(
ServiceLoader.load(Server.class).iterator(),
Spliterator.ORDERED),
false)
.collect(Collectors.toList());
if (servers.size() == 0) {
throw new IllegalStateException("Server not found.");
}
if (servers.size() > 1) {
List names = servers.stream()
.map(it -> it.getClass().getSimpleName().toLowerCase())
.collect(Collectors.toList());
getLog().warn("Multiple servers found {}. Using: {}", names, names.get(0));
}
Server server = servers.get(0);
try {
if (serverOptions == null) {
serverOptions = ServerOptions.from(getEnvironment().getConfig()).orElse(null);
}
if (serverOptions != null) {
serverOptions.setServer(server.getClass().getSimpleName().toLowerCase());
server.setOptions(serverOptions);
}
return server.start(this);
} catch (Throwable x) {
Logger log = getLog();
log.error("Application startup resulted in exception", x);
try {
server.stop();
} catch (Throwable stopx) {
log.info("Server stop resulted in exception", stopx);
}
// rethrow
throw x instanceof StartupException
? (StartupException) x
: new StartupException("Application startup resulted in exception", x);
}
}
/**
* Call back method that indicates application was deploy it in the given server.
*
* @param server Server.
* @return This application.
*/
public @Nonnull Jooby start(@Nonnull Server server) {
Path tmpdir = getTmpdir();
ensureTmpdir(tmpdir);
if (mode == null) {
mode = ExecutionMode.DEFAULT;
}
if (locales == null) {
String path = "application.lang";
locales = Optional.of(getConfig())
.filter(c -> c.hasPath(path))
.map(c -> c.getString(path))
.map(
v -> LocaleUtils.parseLocales(v).orElseThrow(() -> new RuntimeException(String.format(
"Invalid value for configuration property '%s'; check the documentation of %s#parse(): %s",
path, Locale.LanguageRange.class.getName(), v))))
.orElseGet(() -> singletonList(Locale.getDefault()));
}
ServiceRegistry services = getServices();
services.put(Environment.class, getEnvironment());
services.put(Config.class, getConfig());
joobyRunHook(getClass().getClassLoader(), server);
for (Extension extension : lateExtensions) {
try {
extension.install(this);
} catch (Throwable e) {
throw SneakyThrows.propagate(e);
}
}
this.lateExtensions.clear();
this.lateExtensions = null;
this.startingCallbacks = fire(this.startingCallbacks);
router.start(this);
return this;
}
/**
* Callback method that indicates application was successfully started it and listening for
* connections.
*
* @param server Server.
* @return This application.
*/
public @Nonnull Jooby ready(@Nonnull Server server) {
Logger log = getLog();
this.serverOptions = server.getOptions();
log.info("{} started with:", getName());
log.info(" PID: {}", System.getProperty("PID"));
log.info(" {}", server.getOptions());
if (log.isDebugEnabled()) {
log.debug(" env: {}", env);
} else {
log.info(" env: {}", env.getActiveNames());
}
log.info(" execution mode: {}", mode.name().toLowerCase());
log.info(" user: {}", System.getProperty("user.name"));
log.info(" app dir: {}", System.getProperty("user.dir"));
log.info(" tmp dir: {}", tmpdir);
StringBuilder buff = new StringBuilder();
buff.append("routes: \n\n{}\n\nlistening on:\n");
ServerOptions options = server.getOptions();
String host = options.getHost().replace("0.0.0.0", "localhost");
List