/* * Copyright (C) 2014 Civilian Framework. * * Licensed under the Civilian License (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.civilian-framework.org/license.txt * * 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.civilian; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.civilian.application.AppConfig; import org.civilian.application.ConfigKeys; import org.civilian.application.UploadConfig; import org.civilian.asset.AssetConfig; import org.civilian.asset.AssetService; import org.civilian.asset.AssetServices; import org.civilian.content.ContentSerializer; import org.civilian.content.ContentType; import org.civilian.content.GsonJsonSerializer; import org.civilian.content.TextSerializer; import org.civilian.controller.ControllerConfig; import org.civilian.controller.ControllerNaming; import org.civilian.controller.ControllerService; import org.civilian.controller.classloader.ReloadConfig; import org.civilian.internal.Logs; import org.civilian.internal.TempContext; import org.civilian.processor.AssetDispatch; import org.civilian.processor.ErrorProcessor; import org.civilian.processor.IpFilter; import org.civilian.processor.ProcessorConfig; import org.civilian.processor.ProcessorList; import org.civilian.processor.ResourceDispatch; import org.civilian.provider.ApplicationProvider; import org.civilian.provider.ContextProvider; import org.civilian.provider.PathProvider; import org.civilian.request.BadRequestException; import org.civilian.resource.Path; import org.civilian.resource.PathParamMap; import org.civilian.resource.ResourceConfig; import org.civilian.resource.scan.ResourceScan; import org.civilian.response.std.ErrorResponse; import org.civilian.response.std.NotFoundResponse; import org.civilian.text.LocaleServiceList; import org.civilian.type.TypeLib; import org.civilian.util.Check; import org.civilian.util.ClassUtil; import org.civilian.util.Iterators; import org.civilian.util.Settings; import org.slf4j.Logger; /** * Application represents a Civilian application. */ public abstract class Application implements ApplicationProvider, ContextProvider, PathProvider { private static final Logger log = Logs.APPLICATION; /** * Lifecycle status of the Application. */ public enum Status { /** * The application was created, but not yet initialized. */ CREATED, /** * The application was successfully initialized and is now running. */ RUNNING, /** * The application threw an error during initialization. */ ERROR, /** * The application was closed. */ CLOSED } /** * Creates a new application. * @param pathParams the path parameters used by the application or null * if no path parameters are used * @param controllerRootPackage the package of the root controller class. All controller classes * are assumed to be in this package or in a subpackage. * If it is null, then the package of the application is used. * If the give name starts with "." then the package of the application * classes suffixed with that name is used. * @param controllerNaming provides naming conventions for controller classes * used during resource scan. If it is null, then the default naming conventions are used. */ public Application(PathParamMap pathParams, String controllerRootPackage, ControllerNaming controllerNaming) { if (controllerRootPackage == null) controllerRootPackage = ClassUtil.getPackageName(getClass()); else if (controllerRootPackage.startsWith(".")) controllerRootPackage = ClassUtil.getPackageName(getClass()) + controllerRootPackage; resourceConfig_ = new ResourceConfig(pathParams); controllerConfig_ = new ControllerConfig(controllerRootPackage, controllerNaming); } /** * Creates a new application. * Shortcut for Application(pathParams, controllerRootPackage, new ControllerNaming()) */ public Application(PathParamMap pathParams, String controllerRootPackage) { this(pathParams, controllerRootPackage, null); } /** * Creates a new application. * Shortcut for Application(pathParams, <application package>, new ControllerNaming()) */ public Application(PathParamMap pathParams) { this(pathParams, null, null); } /** * Creates a new application. * Shortcut for Application(PathParamMap.EMPTY, <application package>, new ControllerNaming()) */ public Application() { this(null, null, null); } /** * Called by the Context. */ void setConnector(Object connector) { connector_ = connector; } /** * Returns the connector of the application. A connector is a service which allows * the application to receive requests. In case of an servlet container, the connector * is a servlet. */ public Object getConnector() { return connector_; } /** * Returns the connector of the application which has the given class * or null, if the connector has a different class. */ public <T> T getConnector(Class<T> connectorClass) { return ClassUtil.unwrap(connector_, connectorClass); } /** * Called by the context when the application is added to the context. * @param context the context * @param id the application id * @param relativePath the relative path of the application within the context * @param settings the application settings */ final InitResult init(Context context, String id, Path relativePath, Settings settings) { context_ = context; id_ = id; relativePath_ = relativePath; path_ = context.getPath().add(relativePath); InitResult result = new InitResult(); if (getStatus() != Status.CREATED) throw new IllegalStateException("already initialized"); if (settings == null) settings = new Settings(); try { initApp(settings, result); initProcessors(settings); status_ = Application.Status.RUNNING; log.info("init {} at {}", getId(), getPath()); result.success = true; } catch(Exception e) { status_ = Application.Status.ERROR; log.error("could not initialize " + this, e); try { close(); } catch(Exception e2) { log.error(toString() + ": error during forced close()", e2); } processors_ = new ProcessorList(new ErrorProcessor( Response.Status.SC503_SERVICE_UNAVAILABLE, this + " encountered an error during initialization", e)); } return result; } private void initApp(Settings settings, InitResult initResult) throws Exception { AppConfig appConfig = createConfig(settings); try { appConfig.init(); // inits unsafe-config settings init(appConfig); } finally { // even if init throws an error we complete // setup of safe application properties // since the error page may rely on them initResult.connect = appConfig.getConnect(); initResult.async = appConfig.getAsync(); encoding_ = appConfig.getEncoding(); version_ = appConfig.getVersion(); typeLib_ = appConfig.getTypeLibrary(); assetService_ = initAssets(appConfig.getAssetConfig()); uploadConfig_ = appConfig.getUploadConfig(); contentSerializers_ = appConfig.getContentSerializers(); localeServices_ = new LocaleServiceList( appConfig.getMsgBundleFactory(), appConfig.allowUnsupportedLocales(), appConfig.getSupportedLocales()); } initDefaultContentSerializers(); // init the controller service controllerService_ = new ControllerService( resourceConfig_.getPathParams(), getTypeLib(), appConfig.getControllerFactory(), appConfig.getReloadConfig()); // init the resource tree rootResource_ = appConfig.getRootResource(); if (rootResource_ == null) rootResource_ = generateResourceTree(appConfig.getReloadConfig()); Resource.Tree tree = rootResource_.getTree(); tree.setAppPath(getPath()); tree.setDefaultExtension(appConfig.getDefaultResExtension()); tree.setControllerService(controllerService_); } static class InitResult { boolean success; boolean connect; boolean async; } /** * Initializes the application. * @param config allows to configure the application. * @throws Exception if an error during initialization occurs. */ protected abstract void init(AppConfig config) throws Exception; /** * Creates the AppConfig during initialization. * Derived implementations may overwrite this method * if they intend to initialize the AppConfig in a different way. */ protected AppConfig createConfig(Settings settings) { return new AppConfig(this, settings); } /** * Adds ContentSerializers for text/plain and application/json * if not done in init(Appconfig). */ private void initDefaultContentSerializers() { if (getContentSerializer(ContentType.TEXT_PLAIN) == null) contentSerializers_.put(ContentType.TEXT_PLAIN, new TextSerializer()); if ((getContentSerializer(ContentType.APPLICATION_JSON) == null) && ClassUtil.getPotentialClass("com.google.gson.Gson", Object.class, null) != null) contentSerializers_.put(ContentType.APPLICATION_JSON, new GsonJsonSerializer()); } /** * Initializes the AssetService of the application. Called after * {@link #init(AppConfig)} finished. * @return the AssetService */ protected AssetService initAssets(AssetConfig config) { AssetService service = AssetServices.combine(Path.ROOT, config.getLocations()); if (config.getLocationCount() > 0) service = AssetServices.makeCaching(service, config.getMaxCachedSize()); service.init(getPath(), getEncoding(), config.getContentTypeLookup()); return service; } private void initProcessors(Settings settings) throws Exception { ProcessorConfig pconfig = new ProcessorConfig(); // an optional IpFilter as first processor String[] ipList = settings.getList(ConfigKeys.IP); if (ipList.length > 0) pconfig.addLast(new IpFilter(ipList)); // resource dispatch as next processor pconfig.addLast(new ResourceDispatch(rootResource_)); // followed by an optional AssetDispatch if (getAssetService().hasAssets()) pconfig.addLast(new AssetDispatch(getAssetService())); // allow derived implementations to tweak the processor list initProcessors(pconfig); // initialize the processors processors_ = new ProcessorList(pconfig.getList()); } /** * Allows derived applications to initialize the processor list. * By default the list contains these processors: * <ol> * <li>IpFilter, if the Civilian config specified a list of allowed ips * <li>AssetDispatch, to access CSS, JS and other static resource of the application, if * the asset config is enabled and contains asset locations * <li>ResourceDispatch, to dispatch requests to resources * </ol> * The process list can be rearranged, reduced or enhanced depending * on the needs of your application. * @throws Exception if an error during initialization occurs. */ protected void initProcessors(ProcessorConfig config) throws Exception { } //-------------------------------- // resource/path-setup //-------------------------------- /** * Generates the resource tree at application startup. * Called during application initialization when no * resource tree was configured. * @see AppConfig#setResourceRoot(Resource) */ public Resource generateResourceTree(ReloadConfig reloadConfig) throws Exception { ClassLoader loader = getClass().getClassLoader(); if (reloadConfig != null) loader = reloadConfig.createClassLoader(); ResourceScan scan = new ResourceScan( getControllerConfig().getRootPackage(), getControllerConfig().getNaming(), getResourceConfig().getPathParams(), loader); return scan.run(); } //-------------------------------- // close //-------------------------------- /** * Closes the application. Called when the context shuts down * or the application is removed from the context. */ void runClose() throws Exception { status_ = Status.CLOSED; try { processors_.close(); } finally { close(); } } /** * Closes the application. Called when the context shuts down * or the application is removed from the context. * The method is also called when {@link #init(AppConfig)} threw an exception. * Therefore you need to take into account that your app resource may * not have been fully initialized. */ protected abstract void close() throws Exception; //-------------------------------- // accessors //-------------------------------- /** * Implements ApplicationProvider and returns this. */ @Override public Application getApplication() { return this; } /** * Returns the application id. * The id is used to identify the application within * the {@link Context}. The id was defined within * <code>civilian.ini</code>. * @see Context#getApplication(String) */ public String getId() { return id_; } /** * Returns the application version. * The optional version can be defined during setup. * @see AppConfig#setVersion(String) */ public String getVersion() { return version_; } /** * Returns the develop flag of the context. * The develop flag is defined in the Civilian config file. * @return if true the application runs in development mode, * else in production mode. * @see Context#develop() */ public boolean develop() { return context_.develop(); } /** * Returns the context in which the application is running. */ @Override public Context getContext() { return context_; } /** * Returns the default encoding for textual content of responses. */ public String getEncoding() { return encoding_; } /** * Returns the path from the server root to the application. */ @Override public Path getPath() { return path_; } /** * Returns the relative path from the context to the application. */ @Override public Path getRelativePath() { return relativePath_; } /** * Returns the application status. */ public Status getStatus() { return status_; } /** * Returns the AssetService used to serve assets. */ public AssetService getAssetService() { return assetService_; } /** * Returns the processor list. * The processor list contains the processors which are used to process requests. */ public ProcessorList getProcessors() { return processors_; } /** * Returns the root resource of the application. */ public Resource getRootResource() { return rootResource_; } /** * Returns the resource config which stores resource related settings. */ public ResourceConfig getResourceConfig() { return resourceConfig_; } /** * Returns the controller service which provides * access to controller types. */ public ControllerService getControllerService() { return controllerService_; } /** * Returns the controller config. */ public ControllerConfig getControllerConfig() { return controllerConfig_; } /** * Returns the the type library used by the application. * The library contains type implementations which are used * to serialize (format and parse) values of these types. */ public TypeLib getTypeLib() { return typeLib_; } /** * Returns the LocaleServiceList. */ public LocaleServiceList getLocaleServices() { return localeServices_; } /** * Returns a ContentSerializer for the content type. * @return the ContentSerializer or null if no suitable serializer is available * By default the application possesses ContentSerializers for text/plain and * application/json (based on GSON). * @see AppConfig#registerContentSerializer(ContentType, ContentSerializer) */ public ContentSerializer getContentSerializer(ContentType contentType) { return contentSerializers_.get(contentType); } /** * Returns an iterator for all ContentTypes with a registered ContentSerializer. */ public Iterator<ContentType> getContentSerializerTypes() { return Iterators.unmodifiable(contentSerializers_.keySet().iterator()); } /** * Returns the UploadConfig which defines upload limits and location. */ public UploadConfig getUploadConfig() { return uploadConfig_; } /** * Stores an attribute under the given name in the application. */ public void setAttribute(String name, Object value) { attributes_.put(name, value); } /** * Returns an attribute which was previously associated with * the application. * @param name the attribute name * @see #setAttribute(String, Object) */ public Object getAttribute(String name) { return attributes_.get(name); } /** * Returns an iterator of the attribute names stored in the application. */ public Iterator<String> getAttributeNames() { return attributes_.keySet().iterator(); } //----------------------------------- // process //----------------------------------- /** * Processes a request. * The default implementation forwards the request to the processor pipeline. * If no processor handled the request, it is forwarded to the {@link #createNotFoundResponse() NotFoundResponse}. * If an exception occurs during request processing {@link #onError(Request, Throwable)} is called */ public void process(Request request) { Check.notNull(request, "request"); if (request.getApplication() != this) throw new IllegalArgumentException("not my request: " + request.getApplication()); try { if (log.isDebugEnabled()) log.debug("{} {}", request.getMethod(), request.getPath()); try { boolean processed = processors_.process(request); if (!processed) createNotFoundResponse().send(request.getResponse()); } finally { if (!request.isAsyncStarted()) request.getResponse().closeContent(); } } catch (Throwable t) { try { onError(request, t); } catch(Exception e) { log.error("exception during exception processing", e); } } } /** * Called when an error during request processing occurs. * The default implementation * <ul> * <li>recognizes a {@link BadRequestException} and calls {@link Response#sendError(int, String, Throwable)} * using the status code, the message and cause of the BadRequestException. * <li>otherwise calls {@link #ignoreError(Throwable)} if the * error should be ignored. * If it should not be ignored, it logs the error and - if the response is not yet committed - * sends the error via {@link Response#sendError(int, String, Throwable)} * </ul> */ protected void onError(Request request, Throwable error) throws Exception { Check.notNull(request, "request"); Check.notNull(error, "error"); Response response = request.getResponse(); if (error instanceof BadRequestException) { if (log.isDebugEnabled()) log.debug("bad request", error.getCause()); BadRequestException e = (BadRequestException)error; if (!response.isCommitted()) response.sendError(e.getStatusCode(), e.getMessage(), e.getCause()); } else if (!ignoreError(error)) { log.error("onError", error); if (!response.isCommitted()) response.sendError(Response.Status.INTERNAL_SERVER_ERROR, null, error); } } /** * Called by onError to decide if a thrown error should be ignored. * A good example would be to ignore socket errors when the * socket connection was aborted by the client. * The default implementation returns false (not to ignore the error). * Derived application classes should override the method and implement * their own strategy. */ public boolean ignoreError(Throwable error) { // String s = error.toString(); // return s.contains("connection abort") || s.contains("ClientAbortException"); return false; } /** * Returns an ErrorResponseobject which is used by the response * to send an error to the client. Applications can * return a different implementation to tweak the error response. * @see Response#sendError(int) * @see Response#sendError(int, String, Throwable) */ public ErrorResponse createErrorResponse() { return new ErrorResponse(); } /** * Returns a NotFoundResponse object which is used by * to send a response to the client, if not processor handled the request. * Applications can return a different implementation to tweak the not-found-response. */ public NotFoundResponse createNotFoundResponse() { return new NotFoundResponse(); } /** * Returns a string representation of the application. */ @Override public String toString() { return "app '" + id_ + "'"; } // properties all have reasonable defaults, initialized again when added to the context private String id_ = "?"; private String encoding_ = ConfigKeys.ENCODING_DEFAULT; private Path relativePath_ = Path.ROOT; private Path path_ = Path.ROOT; private Context context_ = TempContext.INSTANCE; // properties set after init(AppConfig) private ControllerService controllerService_; private ControllerConfig controllerConfig_; private LocaleServiceList localeServices_; private Resource rootResource_; private ResourceConfig resourceConfig_; private TypeLib typeLib_; private AssetService assetService_; private UploadConfig uploadConfig_; private String version_; private Object connector_; private ProcessorList processors_ = ProcessorList.EMPTY; private final HashMap<String, Object> attributes_ = new HashMap<>(); private Map<ContentType,ContentSerializer> contentSerializers_ = Collections.<ContentType,ContentSerializer>emptyMap(); // lifecycle property private Status status_ = Status.CREATED; }