/* * 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.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.slf4j.Logger; import org.civilian.application.AppConfig; import org.civilian.application.ConfigKeys; import org.civilian.content.ContentTypeLookup; import org.civilian.internal.DefaultApp; import org.civilian.internal.Logs; import org.civilian.internal.TempContext; import org.civilian.internal.admin.AdminApp; import org.civilian.provider.ContextProvider; import org.civilian.provider.PathProvider; import org.civilian.resource.Path; import org.civilian.template.TemplateWriter; import org.civilian.util.Check; import org.civilian.util.ClassUtil; import org.civilian.util.FileType; import org.civilian.util.Settings; import org.civilian.util.ResourceLoader; /** * Context represents the environment which hosts and runs Civilian applications. * In servlet terms the context corresponds to a ServletContext. */ public abstract class Context implements ContextProvider, PathProvider { /** * The default name of the Civilian config file. */ public static final String DEFAULT_CONFIG_FILE = "civilian.ini"; /** * The context log. */ private static final Logger log = Logs.CONTEXT; /** * Creates a new context. * The context sets the default line separator to "\n", */ public Context() { TemplateWriter.setDefaultLineSeparator("\n"); } //-------------------------- // civilian.ini //-------------------------- /** * Reads a config or properties file into a settings object. * @param configName the name of the config file. * @return a settings object for the config file or null, if the file could not be read */ public Settings readSettings(String configName) throws IllegalArgumentException, IOException { Check.notNull(configName, "configName"); File configFile = getConfigFile(configName, FileType.EXISTENT_FILE); Settings settings = new Settings(); settings.read(configFile); return settings; } //-------------------------- // init //-------------------------- /** * Initializes the context from the provided settings. */ protected void init(ClassLoader appClassLoader, Settings settings) throws Exception { Check.notNull(settings, "settings"); long time = System.currentTimeMillis(); AppLoader loader = createAppLoader(appClassLoader); // read flags develop_ = settings.getBoolean(ConfigKeys.DEVELOP, false); boolean logInfo = log.isInfoEnabled(); if (logInfo) { log.info("----------------------"); log.info("start, version " + Version.VALUE); } // 1. create applications: this may fail throw any exception ArrayList<AppInfo> appInfos = new ArrayList<>(); createAdminApp(loader, new Settings(settings, ConfigKeys.ADMINAPP_PREFIX), appInfos); createCustomApps(loader, settings, appInfos); // 2. add to context for (AppInfo info : appInfos) addApp(info.app, info.id, info.path, info.settings); time = System.currentTimeMillis() - time; if (logInfo) { log.info("start complete, " + ((time) / 1000.0) + "s"); log.info("----------------------"); } } //-------------------------- // adding apps //-------------------------- /** * Holds the data of an application defined in the Civilian config. */ private static class AppInfo { public AppInfo(String id, Settings settings, String path, Application app) { this.id = id; this.app = app; this.path = Path.norm(path); this.settings = settings; } public final String id; public final Application app; public final String path; public final Settings settings; } private void createAdminApp(AppLoader loader, Settings settings, ArrayList<AppInfo> appInfos) throws Exception { if (AppConfig.isEnabled(settings, develop_, develop_)) { String path = settings.get(ConfigKeys.PATH, ConfigKeys.ADMIN_PATH_DEFAULT); Application app = loader.createAdminApp(); AppInfo appInfo = new AppInfo("civadmin", settings, path, app); appInfos.add(appInfo); } } private String[] findCustomAppIds(Settings settings) { // test if the app ids are explicitly listed if (settings.contains(ConfigKeys.APPLICATIONS, false)) return settings.getList(ConfigKeys.APPLICATIONS); // else collect them ArrayList<String> ids = new ArrayList<>(); for (Iterator<String> keys = settings.keys(); keys.hasNext(); ) { String key = keys.next(); // test the mandatory keys if (key.startsWith(ConfigKeys.APP_PREFIX) && (key.endsWith(ConfigKeys.CLASS) || key.endsWith(ConfigKeys.PACKAGE))) { int p = key.indexOf('.', ConfigKeys.APP_PREFIX.length()); if (p != -1) { String id = key.substring(ConfigKeys.APP_PREFIX.length(), p); if (!ids.contains(id)) ids.add(id); } } } Collections.sort(ids); return ids.toArray(new String[ids.size()]); } // creates and initializes applications private void createCustomApps(AppLoader loader, Settings settings, ArrayList<AppInfo> appInfos) throws Exception { for (String id : findCustomAppIds(settings)) { String appPrefix = ConfigKeys.APP_PREFIX + id + '.'; Settings appSettings = new Settings(settings, appPrefix); if (AppConfig.isEnabled(appSettings, true, develop_)) createCustomApp(loader, appSettings, id, appPrefix, appInfos); } } /** * Creates the application for a certain id. * Even if creation fails and we cannot instantiate the application object * we push a DefaultApp object, which - when invoked - will present * an error message. */ protected void createCustomApp(AppLoader loader, Settings settings, String id, String appPrefix, ArrayList<AppInfo> appInfos) throws Exception { String path = settings.get(ConfigKeys.PATH, ""); Application app; if (settings.contains(ConfigKeys.CLASS)) app = loader.createApplication(settings.get(ConfigKeys.CLASS)); else if (settings.contains(ConfigKeys.PACKAGE)) app = loader.createDefaultApp(settings.get(ConfigKeys.PACKAGE)); else { String msg = "application '" + id + "' must either define an application class (key '" + appPrefix + ConfigKeys.CLASS + "') or set the base package for controller classes (key '" + appPrefix + ConfigKeys.PACKAGE + "')"; throw new IllegalStateException(msg); } AppInfo appInfo = new AppInfo(id, settings, path, app); appInfos.add(appInfo); } /** * Adds an application to the context. * @param app the application. * @param id the id of the application * @param relativePath the path of the application relative to the context * @param settings the settings of the application, can be null. * @return was the application successfully initialized? * @throws IllegalArgumentException if the app is contained in another context * or its path is already used by another application */ protected boolean addApp(Application app, String id, String relativePath, Settings settings) { Check.notNull(app, "app"); Check.notNull(id, "id"); Path relPath = new Path(relativePath); // sanity check 1: app must not be part of a context yet if (app.getContext() != TempContext.INSTANCE) throw new IllegalStateException(app + " already added to context " + app.getContext()); // sanity check 2: path and id may not be used by another app for (Application prevApp : apps_) { if (prevApp.getId().equals(id)) throw new IllegalArgumentException("the app id '" + id + "' is already used by another app"); if (prevApp.getRelativePath().equals(relPath)) throw new IllegalArgumentException("the path '" + relativePath + "' is used by app '" + prevApp.getId() + "' and '" + id + "'"); } // add the app to the application list apps_.add(app); // init the app Application.InitResult initResult = app.init(this, id, relPath, settings); // connect the app: even if the application has status error // it can decide to display some error information to an user if ((settings == null) || initResult.connect) { Object connector = connect(app, initResult.async); app.setConnector(connector); } return initResult.success; } //-------------------------- // connect //-------------------------- /** * Connect the application to the server. This should enable the application * to receive requests. * @param app the app * @param supportAsync should async operations be supported? * @return an connector object which is {@link Application#getConnector() available} in the application. * In an servlet environment, the connector is a servlet * which receives requests and directs them to the application. */ protected abstract Object connect(Application app, boolean supportAsync); /** * Disconnect the application from the server. * Called when the application is closed. */ protected abstract void disconnect(Application app); //-------------------------- // close //-------------------------- /** * Closes all applications of the context. * @see Application#close() */ protected void close() { // close in reverse initialization order for (int i=apps_.size() - 1; i>=0; i--) close(apps_.get(i)); } /** * Closes an application and removes it from the context. */ protected void close(Application app) { if (app == null) return; if (app.getContext() != this) throw new IllegalArgumentException("not my application: " + app); try { if (app.getStatus() == Application.Status.RUNNING) { try { app.runClose(); } catch(Exception e) { Logs.CONTEXT.error("error when closing application " + app, e); } } } finally { apps_.remove(app); } } //-------------------------- // accessors //-------------------------- /** * Implements ContextProvider and returns this. */ @Override public Context getContext() { return this; } /** * Returns an list of all applications of the Context. */ public List<Application> getApplications() { return Collections.unmodifiableList(apps_); } /** * Returns the first application with the given class. */ @SuppressWarnings("unchecked") public <T extends Application> T getApplication(Class<T> appClass) { for (Application app : apps_) { if (appClass.isAssignableFrom(app.getClass())) return (T)app; } return null; } /** * Returns the application with the given id. */ public Application getApplication(String id) { for (Application app : apps_) { if (app.getId().equals(id)) return app; } return null; } /** * Returns the develop flag of the Context. */ public boolean develop() { return develop_; } /** * Returns a ResourceLoader to access Context resources. */ public abstract ResourceLoader getResourceLoader(); /** * Returns the real path corresponding to the given relative request path. * @param path a path relative to the context path. * @return the real path or null if it does not map to a real path. */ public abstract String getRealPath(String path); /** * Returns the real path corresponding to the given request path. * @param path a path relative to the context path * @return the real path or null if it does not map to a real path. */ public String getRealPath(Path path) { return getRealPath(path.toString()); } /** * Returns the file corresponding to the given virtual path. * @throws IllegalArgumentException thrown if the path cannot be mapped to a real path */ public File getRealFile(String path, FileType fileType) throws IllegalArgumentException { String realPath = getRealPath(path); File file = realPath != null ? new File(realPath) : null; if (fileType != null) fileType.check(file, "getRealFile " + path); return file; } /** * Returns the file corresponding to the context's root directory in the local file system. */ public File getRootDir() { return getRealFile("", FileType.EXISTENT_DIR); } /** * Returns if the given path is not valid for a file resource request. * In a servlet environment this is any path pointing into the WEB-INF directory. */ public abstract boolean isProhibitedPath(String path); /** * Returns the real path of a resource located in a context specific config * directory. For a servlet environment, the config directory is the WEB-INF directory * of the web application. For that a call getConfigPath("myconfig.ini"), would return * a file path whose name ends with "/WEB-INF/myconfig.ini". */ public abstract String getConfigPath(String name); /** * Returns a File for a config path. * @param name a name below the {@link #getConfigPath(String) config path}. */ public File getConfigFile(String name, FileType fileType) throws IllegalArgumentException { return getRealFile(getConfigPath(name), fileType); } /** * Returns the implementation dependent server version. */ public abstract String getServerVersion(); /** * Returns the implementation dependent server info. */ public abstract String getServerInfo(); /** * Returns the absolute path of the Context within the server. * It is the root path for all applications deployed in * the context. * Example: If the server is a webserver and listens at http://<host>:<port>/ * then the applications are located below * http://<host>:<port>/<context-path>/ * In that example the context path corresponds to the path * of the associated ServletContext. */ @Override public abstract Path getPath(); /** * Returns {@link #getPath()}. */ @Override public Path getRelativePath() { return getPath(); } /** * Returns a ContentTypeLookup to translate file names into content types. */ public abstract ContentTypeLookup getContentTypeLookup(); /** * Returns an attribute which was previously associated with * the context. * @param name the attribute name * @see #setAttribute(String, Object) */ public abstract Object getAttribute(String name); /** * Returns an iterator of the attribute names stored in the context. */ public abstract Iterator<String> getAttributeNames(); /** * Stores an attribute under the given name in the context. */ public abstract void setAttribute(String name, Object object); /** * Logs a message into the server log file. * In a servlet environment this goes to ServletContext.log() * Note: The Context itself use slf4j for logging. */ public void log(String msg) { log(msg, null); } /** * Logs a message into the server log file. * In a servlet environment this goes to ServletContext.log() * Note: The Context itself use slf4j for logging. */ public abstract void log(String msg, Throwable throwable); /** * Returns the underlying implementation of the context which has the given class * or null, if the implementation has a different class. * In a servlet environment you can access the ServletContext in this way. */ public abstract <T> T unwrap(Class<T> implClass); private AppLoader createAppLoader(ClassLoader cl) throws Exception { if (cl == null) cl = getClass().getClassLoader(); Class<? extends AppLoader> c = ClassUtil.getClass(AppLoader.class.getName() + "Impl", AppLoader.class, cl); c.getConstructor().setAccessible(true); return c.newInstance(); } private static interface AppLoader { public Application createAdminApp(); public Application createDefaultApp(String packageName); public Application createApplication(String className) throws Exception; } // instantiated with Class.forName @SuppressWarnings("unused") private static class AppLoaderImpl implements AppLoader { public AppLoaderImpl() { } @Override public Application createAdminApp() { return new AdminApp(); } @Override public Application createDefaultApp(String packageName) { return new DefaultApp(packageName); } @Override public Application createApplication(String className) throws Exception { return ClassUtil.createObject(className, Application.class, getClass().getClassLoader()); } } /** * The develop flag for the context. */ protected boolean develop_; private ArrayList<Application> apps_ = new ArrayList<>(); }