org.rapidcontext.core.proc.CallContext.java Source code

Java tutorial

Introduction

Here is the source code for org.rapidcontext.core.proc.CallContext.java

Source

/*
 * RapidContext <http://www.rapidcontext.com/>
 * Copyright (c) 2007-2013 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.core.proc;

import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang.StringUtils;
import org.rapidcontext.core.js.JsSerializer;
import org.rapidcontext.core.security.SecurityContext;
import org.rapidcontext.core.storage.Storage;
import org.rapidcontext.core.type.Channel;
import org.rapidcontext.core.type.Connection;
import org.rapidcontext.core.type.ConnectionException;
import org.rapidcontext.core.type.Environment;
import org.rapidcontext.core.type.Role;
import org.rapidcontext.core.type.User;
import org.rapidcontext.util.DateUtil;

/**
 * A procedure call context. Each procedure call occurs within a
 * specific call context that contains the environment, library and
 * currently used adapter connections. The context also keeps track
 * of the call stack and other information relevant to the procedure
 * call.
 *
 * @author   Per Cederberg
 * @version  1.0
 */
public class CallContext {

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

    /**
     * The attribute used for storing the execution root procedure.
     * This attribute value is automatically stored by the execute()
     * method.
     */
    public static final String ATTRIBUTE_PROCEDURE = "procedure";

    /**
     * The attribute used for storing the execution start time. This
     * attribute value is automatically stored by the execute()
     * method.
     */
    public static final String ATTRIBUTE_START_TIME = "startTime";

    /**
     * The attribute used for storing the execution end time. This
     * attribute value is automatically stored by the execute()
     * method.
     */
    public static final String ATTRIBUTE_END_TIME = "endTime";

    /**
     * The attribute used for storing the progress ratio. The
     * progress value should be a double between 0.0 and 1.0,
     * corresponding to the "percent complete" of the overall call.
     * It is generally only safe to set this value for a top-level
     * procedure, i.e. when the call stack height is one (1).
     */
    public static final String ATTRIBUTE_PROGRESS = "progress";

    /**
     * The attribute used for storing the user information.
     */
    public static final String ATTRIBUTE_USER = "user";

    /**
     * The attribute used for storing call source information.
     */
    public static final String ATTRIBUTE_SOURCE = "source";

    /**
     * The attribute used for storing the result data.
     */
    public static final String ATTRIBUTE_RESULT = "result";

    /**
     * The attribute used for storing the error message.
     */
    public static final String ATTRIBUTE_ERROR = "error";

    /**
     * The attribute used for storing the trace flag.
     */
    public static final String ATTRIBUTE_TRACE = "trace";

    /**
     * The attribute used for storing the log string buffer.
     */
    public static final String ATTRIBUTE_LOG_BUFFER = "log";

    /**
     * The maximum number of characters to store in the log buffer.
     */
    public static final int MAX_LOG_LENGTH = 500000;

    /**
     * The thread that is executing this call context.
     */
    private Thread thread;

    /**
     * The data storage to use.
     */
    private Storage storage;

    /**
     * The connectivity environment to use.
     */
    private Environment env;

    /**
     * The procedure library to use.
     */
    private Library library;

    /**
     * The local procedure call interceptor. This variable is only
     * set if the default library procedure call interceptor should
     * be overridden.
     */
    private Interceptor interceptor = null;

    /**
     * The map of reserved connection channels. Before executing a
     * procedure tree, all the required connection channels are
     * reserved and stored here. The channels stored in this map are
     * indexed by their connection id.
     */
    private HashMap connections = new HashMap();

    /**
     * The context call stack.
     */
    private CallStack stack = new CallStack();

    /**
     * The map of call attributes. Call attributes can be used for
     * storing generic objects in the call context, which is useful
     * for setting flags or storing objects across procedures.
     */
    private HashMap attributes = new HashMap();

    /**
     * The call interrupted flag. Once this flag has been set, it
     * cannot be reversed for this call context and all further
     * procedure calls will be stopped immediately with an error.
     */
    private boolean interrupted = false;

    /**
     * Creates a new procedure call context.
     *
     * @param storage        the data storage to use
     * @param env            the environment to use
     * @param library        the procedure library to use
     */
    public CallContext(Storage storage, Environment env, Library library) {
        this.thread = Thread.currentThread();
        this.storage = storage;
        this.env = env;
        this.library = library;
    }

    /**
     * Returns the data storage used by this context.
     *
     * @return the data storage used by this context
     */
    public Storage getStorage() {
        return storage;
    }

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

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

    /**
     * Returns the local procedure call interceptor. If no local
     * interceptor has been set, the library procedure call
     * interceptor will be returned instead.
     *
     * @return the call interceptor to use
     */
    public Interceptor getInterceptor() {
        if (interceptor != null) {
            return interceptor;
        } else {
            return library.getInterceptor();
        }
    }

    /**
     * Sets the local procedure call interceptor, overriding the
     * default library procedure call interceptor for calls in this
     * context.
     *
     * @param i              the interceptor to use
     */
    public void setInterceptor(Interceptor i) {
        this.interceptor = i;
    }

    /**
     * Returns a call attribute value.
     *
     * @param name           the attribute name
     *
     * @return the call attribute value, or
     *         null if not defined
     */
    public Object getAttribute(String name) {
        return attributes.get(name);
    }

    /**
     * Sets a call attribute value.
     *
     * @param name           the attribute name
     * @param value          the attribute value
     */
    public void setAttribute(String name, Object value) {
        attributes.put(name, value);
    }

    /**
     * Returns the procedure call stack.
     *
     * @return the procedure call stack
     */
    public CallStack getCallStack() {
        return stack;
    }

    /**
     * Returns the permission required to read a path at the current
     * call stack height. If the current call stack height is less or
     * equal to the specified depth, a normal read permission is
     * returned. Otherwise an internal permission is returned.
     *
     * @param depth          the max stack depth for read permission
     *
     * @return the permission required for reading (read or internal)
     *
     * @see Role#PERM_INTERNAL
     * @see Role#PERM_READ
     */
    public String readPermission(int depth) {
        return (stack.height() <= depth) ? Role.PERM_READ : Role.PERM_INTERNAL;
    }

    /**
     * Checks if the currently authenticated user has the specified
     * access permission to a storage path.
     *
     * @param path           the object storage path
     * @param permission     the requested permission
     *
     * @throws ProcedureException if the current user didn't have
     *             the requested access permission
     */
    public static void checkAccess(String path, String permission) throws ProcedureException {

        if (!SecurityContext.hasAccess(path, permission)) {
            User user = SecurityContext.currentUser();
            String id = (user == null) ? "anonymous user" : user.toString();
            LOG.info(permission + " permission denied for " + path + ", " + id);
            throw new ProcedureException("permission denied");
        }
    }

    /**
     * Checks if the currently authenticated user has internal access
     * to a storage path.
     *
     * @param path           the object storage path
     *
     * @throws ProcedureException if the current user didn't have
     *             internal access
     */
    public static void checkInternalAccess(String path) throws ProcedureException {
        checkAccess(path, Role.PERM_INTERNAL);
    }

    /**
     * Checks if the currently authenticated user has read access to
     * a storage path.
     *
     * @param path           the object storage path
     *
     * @throws ProcedureException if the current user didn't have
     *             read access
     */
    public static void checkReadAccess(String path) throws ProcedureException {
        checkAccess(path, Role.PERM_READ);
    }

    /**
     * Checks if the currently authenticated user has search access to
     * a storage path.
     *
     * @param path           the object storage path
     *
     * @throws ProcedureException if the current user didn't have
     *             search access
     */
    public static void checkSearchAccess(String path) throws ProcedureException {
        checkAccess(path, Role.PERM_SEARCH);
    }

    /**
     * Checks if the currently authenticated user has write access to
     * a storage path.
     *
     * @param path           the object storage path
     *
     * @throws ProcedureException if the current user didn't have
     *             write access
     */
    public static void checkWriteAccess(String path) throws ProcedureException {
        checkAccess(path, Role.PERM_WRITE);
    }

    /**
     * Checks if the call has been interrupted. Once a call has been
     * interrupted the procedure call chain will be stopped as soon
     * as possible. Any further procedure calls in this call context
     * will terminate immediately with an error.
     *
     * @return true if this call has been interrupted, or
     *         false otherwise
     *
     * @see #interrupt()
     */
    public boolean isInterrupted() {
        return interrupted;
    }

    /**
     * Interrupts the current procedure call. Once a call has been
     * interrupted the procedure call chain will be stopped as soon
     * as possible. Any further procedure calls in this call context
     * will terminate immediately with an error.
     *
     * @see #isInterrupted()
     */
    public void interrupt() {
        interrupted = true;
        thread.interrupt();
    }

    /**
     * Executes a procedure with the specified name and arguments.
     * This is a convenience method for performing a procedure
     * lookup and proper calls to reserve(), call() and
     * releaseAll(). Before execution all required resources will be
     * reserved, and once the execution terminates they will be
     * released. The arguments must be specified in the same order
     * as in the default bindings for the procedure.
     *
     * @param name           the procedure name
     * @param args           the call arguments
     *
     * @return the result of the call, or
     *         null if the call produced no result
     *
     * @throws ProcedureException if the call execution caused an
     *             error
     */
    public Object execute(String name, Object[] args) throws ProcedureException {

        Procedure proc = library.getProcedure(name);
        boolean commit = false;
        setAttribute(ATTRIBUTE_PROCEDURE, proc);
        setAttribute(ATTRIBUTE_START_TIME, new Date());
        try {
            reserve(proc);
            Object res = call(proc, args);
            commit = true;
            return res;
        } catch (Exception e) {
            if (e instanceof ProcedureException) {
                String msg = "Execution error in procedure '" + name + "'";
                LOG.log(Level.FINE, msg, e);
                throw (ProcedureException) e;
            } else {
                String msg = "Caught unhandled exception in procedure '" + name + "'";
                LOG.log(Level.WARNING, msg, e);
                throw new ProcedureException(msg, e);
            }
        } finally {
            releaseAll(commit);
            setAttribute(ATTRIBUTE_END_TIME, new Date());
        }
    }

    /**
     * Reserves all adapter connections needed for executing the
     * specified procedure. The reservation will be forwarded to the
     * current call interceptor. Any connection or procedure in the
     * default procedure bindings will be reserved recursively in
     * this call context.
     *
     * @param proc           the procedure definition
     *
     * @throws ProcedureException if the connections couldn't be
     *             reserved
     */
    public void reserve(Procedure proc) throws ProcedureException {
        checkAccess("procedure/" + proc.getName(), readPermission(0));
        getInterceptor().reserve(this, proc);
    }

    /**
     * Releases all currently reserved adapter connections. The
     * release will be forwarded to the current call interceptor.
     * Each reserved connection will either be committed or rolled
     * back, depending on the commit flag.
     *
     * @param commit         the commit (or rollback) flag
     */
    public void releaseAll(boolean commit) {
        getInterceptor().releaseAll(this, commit);
    }

    /**
     * Calls a procedure with the specified arguments. The call will
     * be forwarded to the current call interceptor. The arguments
     * must be specified in the same order as in the default bindings
     * for the procedure. All the required arguments must be provided
     * and all connections must already have been reserved. Use
     * execute() when a procedure is to be called from outside a
     * prepared call context.
     *
     * @param proc           the procedure definition
     * @param args           the call arguments
     *
     * @return the call bindings
     *
     * @throws ProcedureException if the argument binding failed
     */
    public Object call(Procedure proc, Object[] args) throws ProcedureException {

        Bindings bindings = proc.getBindings();
        Bindings callBindings = new Bindings(bindings);
        String[] names = bindings.getNames();
        int pos = 0;
        for (int i = 0; i < names.length; i++) {
            if (bindings.getType(names[i]) == Bindings.PROCEDURE) {
                Object value = bindings.getValue(names[i]);
                value = library.getProcedure((String) value);
                callBindings.set(names[i], Bindings.PROCEDURE, value, null);
            } else if (bindings.getType(names[i]) == Bindings.CONNECTION) {
                Object value = bindings.getValue(names[i], null);
                if (value != null) {
                    value = connections.get(value);
                    if (value == null) {
                        String msg = "no connection defined for '" + names[i] + "'";
                        throw new ProcedureException(msg);
                    }
                }
                callBindings.set(names[i], Bindings.CONNECTION, value, null);
            } else if (bindings.getType(names[i]) == Bindings.ARGUMENT) {
                if (pos >= args.length) {
                    String msg = "missing argument " + (pos + 1) + " '" + names[i] + "' in call to '"
                            + proc.getName() + "'";
                    throw new ProcedureException(msg);
                }
                callBindings.set(names[i], Bindings.ARGUMENT, args[pos], null);
                pos++;
            }
        }
        if (pos != args.length) {
            String msg = "too many arguments in call to '" + proc.getName() + "'; expected " + pos + ", found "
                    + args.length;
            throw new ProcedureException(msg);
        }
        return call(proc, callBindings);
    }

    /**
     * Calls a procedure with the specified arguments. The call will
     * be forwarded to the current call interceptor. All the
     * required arguments must be provided and all connections must
     * already have been reserved. Use execute() when a procedure is
     * to be called from outside a prepared call context.
     *
     * @param proc           the procedure definition
     * @param bindings       the bindings to use
     *
     * @return the result of the call, or
     *         null if the call produced no result
     *
     * @throws ProcedureException if the call execution caused an
     *             error
     *
     * @see #execute(String, Object[])
     */
    public Object call(Procedure proc, Bindings bindings) throws ProcedureException {

        if (isInterrupted()) {
            throw new ProcedureException("procedure call interrupted");
        }
        stack.push(proc, bindings);
        try {
            return getInterceptor().call(this, proc, bindings);
        } finally {
            stack.pop();
        }
    }

    /**
     * Reserves a connection channel. The reserved channel will be
     * stored in this context until all channels are released.
     *
     * @param id             the connection identifier
     *
     * @return the reserved connection channel
     *
     * @throws ProcedureException if the channel couldn't be
     *             reserved
     *
     * @see #connectionReleaseAll(boolean)
     */
    public Channel connectionReserve(String id) throws ProcedureException {

        if (id != null && !connections.containsKey(id)) {
            checkInternalAccess("connection/" + id);
            if (isTracing()) {
                logInternal(0, "... Reserving connection channel on '" + id + "'");
            }
            Connection con = null;
            if (env == null) {
                con = Connection.find(storage, id);
            } else {
                con = env.findConnection(storage, id);
            }
            if (con == null) {
                String msg = "failed to reserve connection channel: " + "no connection '" + id + "' found";
                if (isTracing()) {
                    log("ERROR: " + msg);
                }
                LOG.warning(msg);
                throw new ProcedureException(msg);
            }
            try {
                connections.put(id, con.reserve());
            } catch (ConnectionException e) {
                String msg = "failed to reserve connection channel on '" + id + "': " + e.getMessage();
                if (isTracing()) {
                    log("ERROR: " + msg);
                }
                LOG.warning(msg);
                throw new ProcedureException(msg);
            }
        }
        return (Channel) connections.get(id);
    }

    /**
     * Releases all reserved adapter connections. The connections
     * will either be committed or rolled back, depending on the
     * commit flag.
     *
     * @param commit         the commit (or rollback) flag
     *
     * @see #connectionReserve(String)
     */
    public void connectionReleaseAll(boolean commit) {
        Iterator iter = connections.values().iterator();
        while (iter.hasNext()) {
            Channel channel = (Channel) iter.next();
            if (commit) {
                channel.commit();
            } else {
                channel.rollback();
            }
            channel.getConnection().release(channel);
        }
        connections.clear();
    }

    /**
     * Checks if this call context has call trace logging enabled.
     *
     * @return true if call trace logging is enabled, or
     *         false otherwise
     */
    public boolean isTracing() {
        return getAttribute(ATTRIBUTE_TRACE) != null;
    }

    /**
     * Logs the specified message to the log if tracing is enabled.
     *
     * @param message        the message text
     */
    public void log(String message) {
        if (isTracing()) {
            int indent = 2 * getCallStack().height() - 2;
            logInternal(indent, "... " + logIndent(4, message));
        }
    }

    /**
     * Logs the specified call to the log if tracing is enabled.
     *
     * @param name           the procedure or object method
     * @param args           the arguments, or null for none
     */
    public void logCall(String name, Object[] args) {
        if (isTracing()) {
            StringBuilder buffer = new StringBuilder();
            buffer.append(name);
            if (args != null) {
                buffer.append("(");
                for (int i = 0; i < args.length; i++) {
                    if (i > 0) {
                        buffer.append(", ");
                    }
                    String str = JsSerializer.serialize(args[i], false);
                    if (str.length() > 250) {
                        str = str.substring(0, 250) + "...";
                    }
                    buffer.append(str);
                }
                buffer.append(")");
            }
            int indent = 2 * getCallStack().height() - 2;
            logInternal(indent, logIndent(4, "--> " + buffer.toString()));
        }
    }

    /**
     * Logs the specified call response to the log if tracing is
     * enabled.
     *
     * @param obj            the call response
     */
    public void logResponse(Object obj) {
        if (isTracing()) {
            String str = JsSerializer.serialize(obj, true);
            if (str.length() > 1000) {
                str = str.substring(0, 1000) + "...";
            }
            int indent = 2 * getCallStack().height() - 2;
            logInternal(indent, logIndent(4, "<-- " + str));
        }
    }

    /**
     * Logs the specified call error to the log if tracing is
     * enabled.
     *
     * @param e              the exception to log
     */
    public void logError(Exception e) {
        if (isTracing()) {
            int indent = 2 * getCallStack().height() - 2;
            logInternal(indent, logIndent(4, "<-- ERROR: " + e.getMessage()));
        }
    }

    /**
     * Logs the specified message to the call log with indentation.
     *
     * @param indent         the indentation level
     * @param message        the message text
     */
    private void logInternal(int indent, String message) {
        StringBuffer buffer = (StringBuffer) attributes.get(ATTRIBUTE_LOG_BUFFER);
        if (buffer == null) {
            buffer = new StringBuffer();
            attributes.put(ATTRIBUTE_LOG_BUFFER, buffer);
        }
        String prefix = DateUtil.formatIsoTime(new Date()) + ": ";
        buffer.append(prefix);
        buffer.append(StringUtils.repeat(" ", indent));
        buffer.append(logIndent(prefix.length() + indent, message));
        buffer.append("\n");
        if (buffer.length() > MAX_LOG_LENGTH) {
            buffer.delete(0, buffer.length() - MAX_LOG_LENGTH);
        }
    }

    /**
     * Indents all lines after the first one in a text string.
     *
     * @param indent         the indentation depth (chars)
     * @param text           the text to indent
     *
     * @return the indented text
     */
    private String logIndent(int indent, String text) {
        if (indent <= 0 || text.indexOf('\n') < 0) {
            return text;
        } else {
            StringBuilder buffer = new StringBuilder();
            String indentStr = StringUtils.repeat(" ", indent);
            String[] lines = text.split("\n");
            for (int i = 0; i < lines.length; i++) {
                if (lines[i].trim().length() <= 0) {
                    if (i > 0) {
                        buffer.append("\n");
                    }
                } else {
                    if (i > 0) {
                        buffer.append("\n");
                        buffer.append(indentStr);
                    }
                    buffer.append(lines[i]);
                }
            }
            return buffer.toString();
        }
    }
}