package org.gcube.smartgears.managers; import static java.util.concurrent.TimeUnit.SECONDS; import static org.gcube.common.events.Observes.Kind.critical; import static org.gcube.smartgears.Constants.context_attribute; import static org.gcube.smartgears.Constants.profile_file_path; import static org.gcube.smartgears.lifecycle.application.ApplicationState.active; import static org.gcube.smartgears.lifecycle.application.ApplicationState.failed; import static org.gcube.smartgears.lifecycle.application.ApplicationState.stopped; import static org.gcube.smartgears.provider.ProviderFactory.provider; import java.io.File; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.util.List; import java.util.Map.Entry; import jakarta.servlet.FilterRegistration; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContextEvent; import jakarta.servlet.ServletContextListener; import jakarta.servlet.ServletRegistration; import org.gcube.common.events.Observes; import org.gcube.smartgears.configuration.application.ApplicationHandlers; import org.gcube.smartgears.context.application.ApplicationContext; import org.gcube.smartgears.context.container.ContainerContext; import org.gcube.smartgears.extensions.ApplicationExtension; import org.gcube.smartgears.extensions.RequestExceptionBarrier; import org.gcube.smartgears.handlers.application.ApplicationLifecycleEvent; import org.gcube.smartgears.handlers.application.ApplicationLifecycleHandler; import org.gcube.smartgears.handlers.application.ApplicationPipeline; import org.gcube.smartgears.handlers.application.RequestHandler; import org.gcube.smartgears.lifecycle.application.ApplicationLifecycle; import org.gcube.smartgears.lifecycle.container.ContainerLifecycle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Coordinates management of an application as a gCube resource. * * @author Fabio Simeoni * */ public class ApplicationManager { private static Logger log = LoggerFactory.getLogger(ApplicationManager.class); private ApplicationPipeline lifecyclePipeline; private ApplicationContext context; private List extensions; /** * Starts application management. * * @param container * @param application the context of the application * @return the context of the application */ public ApplicationContext start(ContainerContext container, ServletContext application) { try { context = provider().contextFor(container, application); for (Entry servlet : application.getServletRegistrations().entrySet()) log.trace("servlet {} : {} {} ", application.getServletContextName(),servlet.getKey(), servlet.getValue().getMappings()); context.configuration().validate(); saveApplicationState(); // make context available to application in case it is gcube-aware shareContextWith(application); // prepare for events as early as possible registerObservers(); ApplicationHandlers handlers = provider().handlersFor(context); List lifecycleHandlers = handlers.lifecycleHandlers(); List requestHandlers = handlers.requestHandlers(); log.trace("managing {} lifecycle with {}", context.name(), lifecycleHandlers); log.trace("managing {} requests with {}", context.name(), requestHandlers); log.trace("extending {} API with {}", context.name(), extensions); extensions = provider().extensionsFor(context); // order is important here: first add APIs registerExtension(extensions); // then intercept them all registerHandlersAsFilter(requestHandlers); // start lifecycle management start(lifecycleHandlers); //adding the context name to the configuration context.configuration().context(application.getContextPath()); // we're in business context.lifecycle().moveTo(active); return context; } catch (RuntimeException e) { if (context != null) { log.error("error starting application {}",context.name(), e); context.lifecycle().moveTo(failed); } else log.error("error starting application with context not initialized",e ); throw e; } } private void saveApplicationState() { File file = context.persistence().file(profile_file_path); try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))){ oos.writeObject(context.id()); }catch (Exception e) { log.error("error serializing application {} state", context.name()); throw new RuntimeException(e); } } /** * Stops application management. * */ public void stop() { if (context == null) return; log.info("stopping {} management", context.name()); try { context.lifecycle().tryMoveTo(stopped); context.events().fire(context, ApplicationLifecycle.stop); if (extensions != null) unregister(extensions); stopLifecycleHandlers(); log.info("stopping application events for {}", context.name()); context.events().stop(); } catch (Exception e) { log.warn("cannot stop {} management (see cause)", context.name(), e); } } private void registerHandlersAsFilter(List rqHandlers) { ServletContext app = context.application(); String appName = app.getContextPath().replace("/", ""); RequestManager requestFilter = new RequestManager(context, appName, rqHandlers); FilterRegistration.Dynamic filter = app.addFilter(appName + "-filter", requestFilter); filter.addMappingForUrlPatterns(null, false, "/*"); } private void registerExtension(List extensions) { ServletContext application = context.application(); for (ApplicationExtension extension : extensions) try { extension.init(context); if (context.configuration().includes().isEmpty()) { //register excludes for extension in case of includes they are excluded by default context.configuration().excludes().addAll(extension.excludes()); } String mapping = extension.mapping(); application.addServlet(context.configuration().name() + "-" + extension.name(), extension) .addMapping(mapping); // adds a filter to map request exceptions onto error responses, // repeating for our extensions what we already do for our filters. // we do not interfere with error management of native application servlets RequestExceptionBarrier barrier = new RequestExceptionBarrier(); FilterRegistration.Dynamic filter = application.addFilter("exception-barrier", barrier); filter.addMappingForUrlPatterns(null, false, mapping); log.info("registered API extension {} @ {}", extension.name(), mapping); } catch (Exception e) { throw new RuntimeException("cannot register API extension " + extension.name(), e); } } private void unregister(List extensions) { for (ApplicationExtension extension : extensions) extension.stop(); } private void start(List handlers) { try { lifecyclePipeline = new ApplicationPipeline(handlers); lifecyclePipeline.forward(new ApplicationLifecycleEvent.Start(context)); } catch (RuntimeException e) { context.lifecycle().tryMoveTo(failed); throw e; } } private void stopLifecycleHandlers() { if (lifecyclePipeline == null) return; // copy pipeline, flip it, and ApplicationPipeline returnPipeline = lifecyclePipeline.reverse(); // start lifetime pipeline in inverse order with stop event returnPipeline.forward(new ApplicationLifecycleEvent.Stop(context)); } private void registerObservers() { Object observer = new Object() { @Observes(value = ContainerLifecycle.stop, kind = critical) void onStopOf(ContainerLifecycle ignore) { if (!context.lifecycle().tryMoveTo(stopped)) log.warn("cannot stop {} after container has stopped", context.name()); } }; context.container().events().subscribe(observer); // we cleanup when container stops context.application().addListener(new ServletContextListener() { @Override public void contextInitialized(ServletContextEvent sce) { log.info("initilizing context {} ",context.name()); context.events().fire(context, ApplicationLifecycle.activation); log.info("webApp {} initialized ",context.name()); } //when the container shuts down we go down @Override public void contextDestroyed(ServletContextEvent sce) { try { log.info("stopping {} on undeployment|shutdown",context.name()); stop(); log.info("suspending undeployment|shutdow to allow {} to stop gracefully",context.name()); SECONDS.sleep(3); log.info("resuming undeployment|shutdow after stopping {}",context.name()); } catch (InterruptedException e) { log.warn(context.name()+" cannot gracefully stop on undeployment|shutdow",e); } } }); } private void shareContextWith(ServletContext application) { application.setAttribute(context_attribute, context); } }