org.rapidcontext.app.ApplicationContext.java Source code

Java tutorial

Introduction

Here is the source code for org.rapidcontext.app.ApplicationContext.java

Source

/*
 * RapidContext <http://www.rapidcontext.com/>
 * Copyright (c) 2007-2015 Per Cederberg. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the BSD license.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the RapidContext LICENSE for more details.
 */

package org.rapidcontext.app;

import java.io.File;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Logger;

import com.eaio.uuid.UUID;

import org.apache.commons.lang.time.DateUtils;
import org.rapidcontext.app.plugin.PluginException;
import org.rapidcontext.app.plugin.PluginManager;
import org.rapidcontext.app.proc.AppListProcedure;
import org.rapidcontext.app.proc.ConnectionListProcedure;
import org.rapidcontext.app.proc.ConnectionValidateProcedure;
import org.rapidcontext.app.proc.PluginInstallProcedure;
import org.rapidcontext.app.proc.PluginListProcedure;
import org.rapidcontext.app.proc.PluginLoadProcedure;
import org.rapidcontext.app.proc.PluginUnloadProcedure;
import org.rapidcontext.app.proc.ProcedureDeleteProcedure;
import org.rapidcontext.app.proc.ProcedureListProcedure;
import org.rapidcontext.app.proc.ProcedureReadProcedure;
import org.rapidcontext.app.proc.ProcedureTypesProcedure;
import org.rapidcontext.app.proc.ProcedureWriteProcedure;
import org.rapidcontext.app.proc.ResetProcedure;
import org.rapidcontext.app.proc.SessionAuthenticateProcedure;
import org.rapidcontext.app.proc.SessionAuthenticateTokenProcedure;
import org.rapidcontext.app.proc.SessionCurrentProcedure;
import org.rapidcontext.app.proc.SessionTerminateProcedure;
import org.rapidcontext.app.proc.StatusProcedure;
import org.rapidcontext.app.proc.StorageCopyProcedure;
import org.rapidcontext.app.proc.StorageDeleteProcedure;
import org.rapidcontext.app.proc.StorageListProcedure;
import org.rapidcontext.app.proc.StorageReadProcedure;
import org.rapidcontext.app.proc.StorageWriteProcedure;
import org.rapidcontext.app.proc.ThreadContextProcedure;
import org.rapidcontext.app.proc.ThreadCreateProcedure;
import org.rapidcontext.app.proc.ThreadInterruptProcedure;
import org.rapidcontext.app.proc.ThreadListProcedure;
import org.rapidcontext.app.proc.TypeListProcedure;
import org.rapidcontext.app.proc.UserAuthenticationTokenProcedure;
import org.rapidcontext.app.proc.UserChangeProcedure;
import org.rapidcontext.app.proc.UserAccessProcedure;
import org.rapidcontext.app.proc.UserListProcedure;
import org.rapidcontext.app.proc.UserPasswordChangeProcedure;
import org.rapidcontext.app.proc.UserSearchProcedure;
import org.rapidcontext.core.data.Array;
import org.rapidcontext.core.data.Dict;
import org.rapidcontext.core.js.JsCompileInterceptor;
import org.rapidcontext.core.js.JsProcedure;
import org.rapidcontext.core.proc.CallContext;
import org.rapidcontext.core.proc.Interceptor;
import org.rapidcontext.core.proc.Library;
import org.rapidcontext.core.proc.ProcedureException;
import org.rapidcontext.core.security.SecurityContext;
import org.rapidcontext.core.storage.Path;
import org.rapidcontext.core.storage.Storage;
import org.rapidcontext.core.storage.StorageException;
import org.rapidcontext.core.storage.RootStorage;
import org.rapidcontext.core.task.Scheduler;
import org.rapidcontext.core.task.Task;
import org.rapidcontext.core.type.Environment;
import org.rapidcontext.core.type.Session;
import org.rapidcontext.core.type.WebMatcher;
import org.rapidcontext.core.type.WebService;
import org.rapidcontext.util.FileUtil;

/**
 * The application context. This is a singleton object that contains
 * references to global application settings and objects. It also
 * provides simple procedure execution and resource and plug-in
 * initialization and deinitialization.
 *
 * @author   Per Cederberg
 * @version  1.0
 */
public class ApplicationContext {

    /**
     * The class logger.
     */
    private static final Logger LOG = Logger.getLogger(ApplicationContext.class.getName());

    /**
     * The path to the global configuration.
     */
    public static final Path PATH_CONFIG = new Path("/config");

    /**
     * The class load time (system initialization time).
     */
    public static final Date INIT_TIME = new Date();

    /**
     * The context start (or reset) time.
     */
    public static Date START_TIME = new Date();

    /**
     * The number of milliseconds between each run of the expired
     * session cleaner job.
     */
    private static final long EXPIRED_INTERVAL_MILLIS = DateUtils.MILLIS_PER_HOUR;

    /**
     * The singleton application context instance.
     */
    private static ApplicationContext instance = null;

    /**
     * The application root storage.
     */
    private RootStorage storage;

    /**
     * The plug-in manager.
     */
    private PluginManager pluginManager;

    /**
     * The application configuration.
     */
    private Dict config;

    /**
     * The active environment.
     */
    private Environment env = null;

    /**
     * The cached list of web matchers (from the web services).
     */
    private WebMatcher[] matchers = null;

    /**
     * The procedure library.
     */
    private Library library = null;

    /**
     * The thread call context map.
     */
    private Map threadContext = Collections.synchronizedMap(new HashMap());

    /**
     * Creates and initializes the application context. If the start
     * flag is set, all plug-ins will be loaded along with procedures
     * and the environment configuration. Otherwise only the storages
     * will be initialized. Note that if the context has already been
     * created, it will not be recreated.
     *
     * @param baseDir        the base application directory
     * @param localDir       the local add-on directory
     * @param start          the initialize plug-ins flag
     *
     * @return the application context created or found
     */
    protected static synchronized ApplicationContext init(File baseDir, File localDir, boolean start) {
        if (instance == null) {
            instance = new ApplicationContext(baseDir, localDir);
        }
        if (start) {
            instance.initAll();
            Task sessionCleaner = new Task("storage session cleaner") {
                public void execute() {
                    Session.removeExpired(getInstance().getStorage());
                }
            };
            long delay = EXPIRED_INTERVAL_MILLIS;
            Scheduler.schedule(sessionCleaner, 1000, delay);
        }
        return instance;
    }

    /**
     * Destroys the application context and frees all resources used.
     */
    protected static synchronized void destroy() {
        if (instance != null) {
            Scheduler.unscheduleAll();
            instance.destroyAll();
            instance.storage.unmountAll();
            instance = null;
        }
    }

    /**
     * Returns the singleton application context instance.
     *
     * @return the singleton application context instance
     */
    public static ApplicationContext getInstance() {
        return instance;
    }

    /**
     * Creates a new application context. This constructor should
     * only be called once in the application and it will store away
     * the instance created.
     *
     * @param baseDir        the base application directory
     * @param localDir       the local add-on directory
     */
    private ApplicationContext(File baseDir, File localDir) {
        File builtinDir = FileUtil.canonical(new File(baseDir, "plugin"));
        File pluginDir = FileUtil.canonical(new File(localDir, "plugin"));
        this.storage = new RootStorage(true);
        this.pluginManager = new PluginManager(builtinDir, pluginDir, storage);
        this.library = new Library(this.storage);
        this.config = (Dict) storage.load(PATH_CONFIG);
        if (this.config == null) {
            LOG.severe("failed to load application config");
        } else if (!this.config.containsKey("guid")) {
            this.config.set("guid", new UUID().toString());
            try {
                storage.store(PATH_CONFIG, this.config);
            } catch (Exception e) {
                LOG.severe("failed to update application config with GUID");
            }
        }
    }

    /**
     * Initializes this context by loading the plug-ins, procedures
     * and the environment configuration.
     */
    private void initAll() {
        Library.builtInPlugin = PluginManager.SYSTEM_PLUGIN;
        initLibrary();
        initPlugins();
        // TODO: Remove singleton environment reference
        Environment[] envs = Environment.findAll(storage);
        env = (envs.length > 0) ? envs[0] : null;
        try {
            SecurityContext.init(storage);
        } catch (StorageException e) {
            LOG.severe("Failed to load security config: " + e.getMessage());
        }
        START_TIME = new Date();
    }

    /**
     * Initializes the library in this context.
     */
    private void initLibrary() {

        // Register default procedure types
        try {
            Library.registerType("javascript", JsProcedure.class);
        } catch (ProcedureException e) {
            LOG.severe("failed to register javascript procedure type: " + e.getMessage());
        }

        // Add default interceptors
        Interceptor i = library.getInterceptor();
        library.setInterceptor(new JsCompileInterceptor(i));

        // Add default built-in procedures
        try {
            library.addBuiltIn(new AppListProcedure());
            library.addBuiltIn(new ConnectionListProcedure());
            library.addBuiltIn(new ConnectionValidateProcedure());
            library.addBuiltIn(new PluginInstallProcedure());
            library.addBuiltIn(new PluginListProcedure());
            library.addBuiltIn(new PluginLoadProcedure());
            library.addBuiltIn(new PluginUnloadProcedure());
            library.addBuiltIn(new ProcedureListProcedure());
            library.addBuiltIn(new ProcedureReadProcedure());
            library.addBuiltIn(new ProcedureTypesProcedure());
            library.addBuiltIn(new ProcedureWriteProcedure());
            library.addBuiltIn(new ProcedureDeleteProcedure());
            library.addBuiltIn(new ResetProcedure());
            library.addBuiltIn(new SessionAuthenticateProcedure());
            library.addBuiltIn(new SessionAuthenticateTokenProcedure());
            library.addBuiltIn(new SessionCurrentProcedure());
            library.addBuiltIn(new SessionTerminateProcedure());
            library.addBuiltIn(new StatusProcedure());
            library.addBuiltIn(new StorageCopyProcedure());
            library.addBuiltIn(new StorageDeleteProcedure());
            library.addBuiltIn(new StorageListProcedure());
            library.addBuiltIn(new StorageReadProcedure());
            library.addBuiltIn(new StorageWriteProcedure());
            library.addBuiltIn(new ThreadContextProcedure());
            library.addBuiltIn(new ThreadCreateProcedure());
            library.addBuiltIn(new ThreadInterruptProcedure());
            library.addBuiltIn(new ThreadListProcedure());
            library.addBuiltIn(new TypeListProcedure());
            library.addBuiltIn(new UserAccessProcedure());
            library.addBuiltIn(new UserAuthenticationTokenProcedure());
            library.addBuiltIn(new UserChangeProcedure());
            library.addBuiltIn(new UserListProcedure());
            library.addBuiltIn(new UserPasswordChangeProcedure());
            library.addBuiltIn(new UserSearchProcedure());
        } catch (ProcedureException e) {
            LOG.severe("failed to create built-in procedures: " + e.getMessage());
        }
    }

    /**
     * Loads all plug-ins listed in an application specific plug-in
     * configuration file. Also loads any jar libraries found in the
     * plug-in "lib" directories.
     */
    private void initPlugins() {
        Array list;
        String pluginId;

        list = config.getArray("plugins");
        for (int i = 0; i < list.size(); i++) {
            try {
                pluginId = list.getString(i, null);
                loadPlugin(pluginId);
            } catch (PluginException e) {
                LOG.warning("failed to load plugin " + list.getString(i, null) + ": " + e.getMessage());
            }
        }
    }

    /**
     * Destroys this context and frees all resources.
     */
    private void destroyAll() {
        pluginManager.unloadAll();
        Library.unregisterType("javascript");
        library = new Library(this.storage);
        matchers = null;
    }

    /**
     * Resets this context and reloads all resources.
     */
    public void reset() {
        destroyAll();
        initAll();
    }

    /**
     * Returns the application configuration.
     *
     * @return the application configuration
     */
    public Dict getConfig() {
        return this.config;
    }

    /**
     * Returns the application data storage. This is the global data
     * storage that contains all loaded plug-ins and maps requests to
     * them in order.
     *
     * @return the application data store
     */
    public Storage getStorage() {
        return this.storage;
    }

    /**
     * Returns the environment used.
     *
     * @return the environment used
     */
    public Environment getEnvironment() {
        return this.env;
    }

    /**
     * Returns the array of cached web matchers (from the web services).
     * This list is only re-read when the context is reset.
     *
     * @return the array of cached web matchers
     *
     * @see #reset()
     */
    public WebMatcher[] getWebMatchers() {
        if (matchers == null) {
            matchers = WebService.findAllMatchers(storage);
        }
        return matchers;
    }

    /**
     * Returns the procedure library used.
     *
     * @return the procedure library used
     */
    public Library getLibrary() {
        return library;
    }

    /**
     * Returns the application base directory.
     *
     * @return the application base directory
     */
    public File getBaseDir() {
        return pluginManager.pluginDir;
    }

    /**
     * Returns the plug-in class loader.
     *
     * @return the plug-in class loader
     */
    public ClassLoader getClassLoader() {
        return pluginManager.classLoader;
    }

    /**
     * Checks if the specified plug-in is currently loaded.
     *
     * @param pluginId       the unique plug-in id
     *
     * @return true if the plug-in was loaded, or
     *         false otherwise
     */
    public boolean isPluginLoaded(String pluginId) {
        return pluginManager.isLoaded(pluginId);
    }

    /**
     * Returns the specified plug-in configuration dictionary.
     *
     * @param pluginId       the unique plug-in id
     *
     * @return the plug-in configuration dictionary, or
     *         null if not found
     */
    public Dict pluginConfig(String pluginId) {
        return pluginManager.config(pluginId);
    }

    /**
     * Installs a plug-in from the specified file. If an existing
     * plug-in with the same id already exists, it will be
     * replaced without warning. After installation, the new plug-in
     * will also be loaded and added to the default configuration
     * for automatic launch on the next restart.
     *
     * @param file           the plug-in ZIP file
     *
     * @return the unique plug-in id
     *
     * @throws PluginException if the plug-in couldn't be installed
     *             correctly
     */
    public String installPlugin(File file) throws PluginException {
        String pluginId = pluginManager.install(file);
        loadPlugin(pluginId);
        return pluginId;
    }

    /**
     * Loads a plug-in. If the plug-in was loaded successfully, it will
     * also be added to the default configuration for automatic
     * launch on the next restart.
     *
     * @param pluginId       the unique plug-in id
     *
     * @throws PluginException if no plug-in instance could be created
     *             or if the plug-in initialization failed
     */
    public void loadPlugin(String pluginId) throws PluginException {
        Array pluginList;
        String msg;

        pluginManager.load(pluginId);
        pluginList = config.getArray("plugins");
        if (!pluginList.containsValue(pluginId)) {
            pluginList.add(pluginId);
            try {
                storage.store(PATH_CONFIG, config);
            } catch (StorageException e) {
                msg = "failed to update application config: " + e.getMessage();
                throw new PluginException(msg);
            }
        }
    }

    /**
     * Unloads a plug-in. The plug-in will also be removed from the
     * default configuration for automatic launches.
     *
     * @param pluginId       the unique plug-in id
     *
     * @throws PluginException if the plug-in deinitialization failed
     */
    public void unloadPlugin(String pluginId) throws PluginException {
        Array pluginList;
        String msg;

        pluginManager.unload(pluginId);
        library.clearCache();
        pluginList = config.getArray("plugins");
        pluginList.remove(pluginList.indexOf(pluginId));
        try {
            storage.store(PATH_CONFIG, config);
        } catch (StorageException e) {
            msg = "failed to update application config: " + e.getMessage();
            throw new PluginException(msg);
        }
    }

    /**
     * Executes a procedure within this context.
     *
     * @param name           the procedure name
     * @param args           the procedure arguments
     * @param source         the call source information
     * @param trace          the trace buffer or null for none
     *
     * @return the result of the call, or
     *         null if the call produced no result
     *
     * @throws ProcedureException if the procedure execution failed
     */
    public Object execute(String name, Object[] args, String source, StringBuffer trace) throws ProcedureException {

        CallContext cx = new CallContext(storage, env, library);

        threadContext.put(Thread.currentThread(), cx);
        cx.setAttribute(CallContext.ATTRIBUTE_USER, SecurityContext.currentUser());
        cx.setAttribute(CallContext.ATTRIBUTE_SOURCE, source);
        if (trace != null) {
            cx.setAttribute(CallContext.ATTRIBUTE_TRACE, Boolean.TRUE);
            cx.setAttribute(CallContext.ATTRIBUTE_LOG_BUFFER, trace);
        }
        try {
            return cx.execute(name, args);
        } finally {
            threadContext.remove(Thread.currentThread());
        }
    }

    /**
     * Executes a procedure asynchronously within this context. This
     * method will sleep for 10 minutes after terminating the
     * procedure execution, allowing the results to be fetched from
     * the context by another thread.
     *
     * @param name           the procedure name
     * @param args           the procedure arguments
     * @param source         the call source information
     */
    public void executeAsync(String name, Object[] args, String source) {
        CallContext cx = new CallContext(storage, env, library);
        Object res;

        threadContext.put(Thread.currentThread(), cx);
        cx.setAttribute(CallContext.ATTRIBUTE_USER, SecurityContext.currentUser());
        cx.setAttribute(CallContext.ATTRIBUTE_SOURCE, source);
        try {
            res = cx.execute(name, args);
            cx.setAttribute(CallContext.ATTRIBUTE_RESULT, res);
        } catch (Exception e) {
            cx.setAttribute(CallContext.ATTRIBUTE_ERROR, e.getMessage());
        } finally {
            // Delay thread context removal for 10 minutes
            try {
                Thread.sleep(600000);
            } catch (InterruptedException ignore) {
                // Allow thread interrupt to remove context
            }
            threadContext.remove(Thread.currentThread());
        }
    }

    /**
     * Finds the currently active call context for a thread.
     *
     * @param thread         the thread to search for
     *
     * @return the call context found, or
     *         null if no context was active
     */
    public CallContext findContext(Thread thread) {
        return (CallContext) threadContext.get(thread);
    }

    /**
     * Finds the currently active call context for a thread id. The
     * thread id is identical to the hash code for the thread.
     *
     * @param threadId       the thread id to search for
     *
     * @return the call context found, or
     *         null if no context was active
     */
    public CallContext findContext(int threadId) {
        Iterator iter;
        Object obj;

        synchronized (threadContext) {
            iter = threadContext.keySet().iterator();
            while (iter.hasNext()) {
                obj = iter.next();
                if (obj.hashCode() == threadId) {
                    return (CallContext) threadContext.get(obj);
                }
            }
        }
        return null;
    }
}