/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 javaslang.API.$;
import static javaslang.API.Case;
import static javaslang.API.Match;
import static javaslang.Predicates.instanceOf;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Optional;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import com.typesafe.config.Config;
import javaslang.control.Try.CheckedConsumer;
import javaslang.control.Try.CheckedRunnable;
/**
* <h2>life cycle</h2>
* <p>
* Listen for application start and stop events. Useful for starting/stopping services.
* </p>
*
* <h3>onStart/onStop events</h3>
* <p>
* Start/stop callbacks are accessible via application:
* </p>
* <pre>{@code
* {
* onStart(() -> {
* log.info("starting app");
* });
*
* onStop(() -> {
* log.info("stopping app");
* });
* }
* }</pre>
*
* <p>
* Or via module:
* </p>
*
* <pre>{@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");
* });
* }
* }
* }</pre>
*
* <h3>callbacks order</h3>
* <p>
* Callback order is preserved:
* </p>
*
* <pre>{@code
* {
* onStart(() -> {
* log.info("first");
* });
*
* onStart(() -> {
* log.info("second");
* });
*
* onStart(() -> {
* log.info("third");
* });
* }
* }</pre>
* <p>
* Order is useful for service dependencies, like ServiceB should be started after ServiceA.
* </p>
*
* <h3>service registry</h3>
* <p>
* You can also request for a service and start or stop it:
* </p>
*
* <pre>{@code
* {
* onStart(registry -> {
* MyService service = registry.require(MyService.class);
* service.start();
* });
*
* onStop(registry -> {
* MyService service = registry.require(MyService.class);
* service.stop();
* });
* }
* }</pre>
*
* <h3>PostConstruct/PreDestroy annotations</h3>
* <p>
* If you prefer the annotation way... you can too:
* </p>
*
* <pre>{@code
*
* @Singleton
* public class MyService {
*
* @PostConstruct
* public void start() {
* }
*
* @PreDestroy
* public void stop() {
* }
* }
*
* App.java:
*
* {
* lifeCycle(MyService.class);
* }
*
* }</pre>
*
* <p>
* It works as expected just make sure <code>MyService</code> is a <strong>Singleton</strong>
* object.
* </p>
*
* @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<CheckedConsumer<Object>> 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 {
method.setAccessible(true);
method.invoke(owner);
} catch (InvocationTargetException ex) {
throw Match(ex.getTargetException()).of(
Case(instanceOf(RuntimeException.class), x -> x),
Case($(), x -> new IllegalStateException(
"execution of " + annotation + " resulted in error", x)));
}
});
}
}
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.
*
* <pre>{@code
*
* @Singleton
* public class MyService {
*
* @PostConstruct
* public void start() {
* }
*
* @PreDestroy
* public void stop() {
* }
* }
*
* App.java:
*
* {
* lifeCycle(MyService.class);
* }
*
* }</pre>
*
* 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 <code>undefined</code>.
*
* @param service Service type. Must be a singleton object.
* @return This instance.
*/
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 <code>undefined</code>.
*
* @param task Task to run.
* @return This env.
*/
LifeCycle onStart(CheckedConsumer<Registry> 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 <code>undefined</code>.
*
* @param task Task to run.
* @return This env.
*/
LifeCycle onStarted(CheckedConsumer<Registry> 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 <code>undefined</code>.
*
* @param task Task to run.
* @return This env.
*/
default LifeCycle onStart(final CheckedRunnable 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 <code>undefined</code>.
*
* @param task Task to run.
* @return This env.
*/
default LifeCycle onStarted(final CheckedRunnable 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 <code>undefined</code>.
*
* @param task Task to run.
* @return This env.
*/
default LifeCycle onStop(final CheckedRunnable 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 <code>undefined</code>.
*
* @param task Task to run.
* @return This env.
*/
LifeCycle onStop(CheckedConsumer<Registry> task);
}