org.alfresco.repo.web.scripts.bean.BaseRemoteStore.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.web.scripts.bean.BaseRemoteStore.java

Source

/*
 * #%L
 * Alfresco Remote API
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.web.scripts.bean;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

import javax.servlet.http.HttpServletRequest;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;

import org.alfresco.service.cmr.repository.MimetypeService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.webscripts.AbstractWebScript;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
import org.springframework.extensions.webscripts.WrappingWebScriptRequest;
import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest;

/**
 * Remote Store service.
 * <p>
 * Responsible for providing remote HTTP based access to a store. Designed to be accessed
 * from a web-tier application to remotely mirror a WebScript Store instance.
 * <p>
 * Request format:
 * <pre>
 *      <servicepath>/<method>/<path>[?<args>]
 *      <servicepath>/<method>/s/<store>/<path>[?<args>]
 *      <servicepath>/<method>/s/<store>/w/<webapp>/<path>[?<args>]
 * </pre><p>
 * Example:
 * <pre>
 *      /service/remotestore/lastmodified/sites/xyz/pages/page.xml
 * </pre><p>
 * where:
 * <pre>
 *      /service/remotestore -> service path
 *      /lastmodified        -> method name
 *      /sites/../page.xml   -> document path
 * </pre><p>
 * optional request parameters:
 * <pre>
 *      s                    -> the store id
 * </pre><p>
 * Note: path is relative to the root path as configured for this webscript bean
 * <p>
 * Further URL arguments may be provided if required by specific API methods.
 * <p>
 * For content create and update the request should be POSTed and the content sent as the
 * payload of the request content.
 * <p>
 * Supported API methods:
 * <pre>
 *      GET lastmodified -> return timestamp of a document in ms since 1970 as a long string value
 *      GET has -> return true or false string as existence for a document
 *      GET get -> return raw document content - in addition the appropriate HTTP headers for the
 *                 character encoding, content type, length and modified date will be set
 *      GET list -> return the list of available document paths under a path - UTF-8 response text
 *      GET listall -> return the list of available document paths (recursively) under a given path
 *                     - UTF-8 response text
 *      GET listpattern -> return the list of document paths matching a file pattern under a given path
 *                         - UTF-8 response text
 *      POST create -> create a new document with request content payload
 *      POST createmulti -> create multiple new documents with request content payload
 *      POST update -> update an existing document with request content payload
 *      DELETE delete -> delete an existing document 
 * </pre>
 * @author Kevin Roast
 */
public abstract class BaseRemoteStore extends AbstractWebScript {
    public static final String TOKEN_STORE = "s";

    public static final String REQUEST_PARAM_STORE = "s";

    private static final Log logger = LogFactory.getLog(BaseRemoteStore.class);

    protected String defaultStore;
    protected MimetypeService mimetypeService;

    protected static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
    protected static ThreadLocal<Transformer> transformer = new ThreadLocal<Transformer>() {
        @Override
        protected Transformer initialValue() {
            try {
                return TRANSFORMER_FACTORY.newTransformer();
            } catch (TransformerConfigurationException e) {
                throw new RuntimeException(e);
            }
        }
    };

    /**
     * @param defaultStore     the default store name of the store to process document requests against
     */
    public void setStore(String defaultStore) {
        this.defaultStore = defaultStore;
    }

    /**
     * @param mimetypeService   the MimetypeService to set
     */
    public void setMimetypeService(MimetypeService mimetypeService) {
        this.mimetypeService = mimetypeService;
    }

    /**
     * Execute the webscript based on the request parameters
     */
    public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException {
        // NOTE: This web script must be executed in a HTTP Servlet environment

        // Unwrap to a WebScriptServletRequest if we have one
        WebScriptServletRequest webScriptServletRequest = null;
        WebScriptRequest current = req;
        do {
            if (current instanceof WebScriptServletRequest) {
                webScriptServletRequest = (WebScriptServletRequest) current;
                current = null;
            } else if (current instanceof WrappingWebScriptRequest) {
                current = ((WrappingWebScriptRequest) req).getNext();
            } else {
                current = null;
            }
        } while (current != null);

        if (webScriptServletRequest == null) {
            throw new WebScriptException("Remote Store access must be executed in HTTP Servlet environment");
        }

        HttpServletRequest httpReq = webScriptServletRequest.getHttpServletRequest();

        // the request path for the remote store
        String extPath = req.getExtensionPath();

        // values that we need to determine
        String methodName = null;
        String store = null;
        StringBuilder pathBuilder = new StringBuilder(128);

        // tokenize the path and figure out tokenized values
        StringTokenizer tokenizer = new StringTokenizer(extPath, "/");
        if (tokenizer.hasMoreTokens()) {
            methodName = tokenizer.nextToken();

            if (tokenizer.hasMoreTokens()) {
                String el = tokenizer.nextToken();

                if (TOKEN_STORE.equals(el)) {
                    // if the token is TOKEN_STORE, then the next token is the id of the store
                    store = tokenizer.nextToken();

                    // reset element
                    el = (tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null);
                }

                while (el != null) {
                    pathBuilder.append('/');
                    pathBuilder.append(el);

                    el = (tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null);
                }
            }
        } else {
            throw new WebScriptException("Unable to tokenize web path: " + extPath);
        }

        // if we don't have a store, check whether it came in on a request parameter
        if (store == null) {
            store = req.getParameter(REQUEST_PARAM_STORE);
            if (store == null) {
                store = this.defaultStore;
            }
            if (store == null) {
                // not good, we should have a store by this point
                // this means that a store was not passed in and that we also didn't have a configured store
                throw new WebScriptException("Unable to determine which store to operate against."
                        + " A store was not specified and a default was not provided.");
            }
        }

        String path = pathBuilder.toString();

        long start = 0;
        if (logger.isDebugEnabled()) {
            logger.debug(
                    "Remote method: " + methodName.toUpperCase() + "   Store Id: " + store + "   Path: " + path);
            start = System.nanoTime();
        }

        try {
            // generate enum from string method name - so we can use a fast switch table lookup
            APIMethod method = APIMethod.valueOf(methodName.toUpperCase());
            switch (method) {
            case LASTMODIFIED:
                validatePath(path);
                lastModified(res, store, path);
                break;

            case HAS:
                validatePath(path);
                hasDocument(res, store, path);
                break;

            case GET:
                validatePath(path);
                getDocument(res, store, path);
                break;

            case LIST:
                listDocuments(res, store, path, false);
                break;

            case LISTALL:
                listDocuments(res, store, path, true);
                break;

            case LISTPATTERN:
                listDocuments(res, store, path, req.getParameter("m"));
                break;

            case CREATE:
                validatePath(path);
                if (logger.isDebugEnabled())
                    logger.debug("CREATE: content length=" + httpReq.getContentLength());
                createDocument(res, store, path, httpReq.getInputStream());
                break;

            case CREATEMULTI:
                if (logger.isDebugEnabled())
                    logger.debug("CREATEMULTI: content length=" + httpReq.getContentLength());
                createDocuments(res, store, httpReq.getInputStream());
                break;

            case UPDATE:
                validatePath(path);
                if (logger.isDebugEnabled())
                    logger.debug("UPDATE: content length=" + httpReq.getContentLength());
                updateDocument(res, store, path, httpReq.getInputStream());
                break;

            case DELETE:
                validatePath(path);
                deleteDocument(res, store, path);
                break;
            }
        } catch (IllegalArgumentException enumErr) {
            throw new WebScriptException("Unknown method specified to remote store API: " + methodName);
        } catch (IOException ioErr) {
            throw new WebScriptException("Error during remote store API: " + ioErr.getMessage());
        }

        if (logger.isDebugEnabled()) {
            long end = System.nanoTime();
            logger.debug("Time to execute method: " + (end - start) / 1000000f + "ms");
        }
    }

    /**
     * Validate we have a path argument.
     */
    private static void validatePath(String path) {
        if (path == null) {
            throw new WebScriptException("Remote Store expecting document path elements.");
        }
    }

    /**
     * Helper to break down webscript extension path into path component elements
     */
    protected List<String> getPathParts(String[] extPaths) {
        List<String> pathParts = new ArrayList<String>(extPaths.length - 1);
        for (int i = 1; i < extPaths.length; i++) {
            pathParts.add(extPaths[i]);
        }
        return pathParts;
    }

    /**
     * Gets the last modified timestamp for the document.
     * 
     * The output will be the last modified date as a long toString().
     * 
     * @param store the store id
     * @param path  document path to an existing document
     */
    protected abstract void lastModified(WebScriptResponse res, String store, String path) throws IOException;

    /**
     * Determines if the document exists.
     * 
     * The output will be either the string "true" or the string "false".
     * 
     * @param store the store id
     * @param path  document path
     */
    protected abstract void hasDocument(WebScriptResponse res, String store, String path) throws IOException;

    /**
     * Gets a document.
     * 
     * The output will be the document content stream.
     * 
     * @param store the store id
     * @param path  document path
     *
     * @throws IOException if an error occurs retrieving the document
     */
    protected abstract void getDocument(WebScriptResponse res, String store, String path) throws IOException;

    /**
     * Lists the document paths under a given path.
     * 
     * The output will be the list of relative document paths found under the path.
     * Separated by newline characters.
     * 
     * @param store     the store id
     * @param path      document path
     * @param recurse   true to peform a recursive list, false for direct children only.
     * 
     * @throws IOException if an error occurs listing the documents
     */
    protected abstract void listDocuments(WebScriptResponse res, String store, String path, boolean recurse)
            throws IOException;

    /**
     * Lists the document paths matching a file pattern under a given path.
     * 
     * The output will be the list of relative document paths found under the path that
     * match the given file pattern. Separated by newline characters.
     * 
     * @param store     the store id
     * @param path      document path
     * @param pattern   file pattern to match - allows wildcards e.g. *.xml or site*.xml
     * 
     * @throws IOException if an error occurs listing the documents
     */
    protected abstract void listDocuments(WebScriptResponse res, String store, String path, String pattern)
            throws IOException;

    /**
     * Creates a document.
     * 
     * @param store         the store id
     * @param path          document path
     * @param content       content of the document to write
     * 
     */
    protected abstract void createDocument(WebScriptResponse res, String store, String path, InputStream content);

    /**
     * Creates multiple XML documents encapsulated in a single one. 
     *
     * @param res          WebScriptResponse
     * @param store         the store id
     * @param content       content of the document to write
     * 
     */
    protected abstract void createDocuments(WebScriptResponse res, String store, InputStream content);

    /**
     * Updates an existing document.
     * 
     * @param store the store id
     * @param path  document path
     * @param content       content to update the document with
     * 
     */
    protected abstract void updateDocument(WebScriptResponse res, String store, String path, InputStream content);

    /**
     * Deletes an existing document.
     * 
     * @param store the store id
     * @param path  document path
     * 
     */
    protected abstract void deleteDocument(WebScriptResponse res, String store, String path);

    /**
     * Enum representing the available API methods on the Store.
     */
    private enum APIMethod {
        LASTMODIFIED, HAS, GET, LIST, LISTALL, LISTPATTERN, CREATE, CREATEMULTI, UPDATE, DELETE
    };

    protected static String encodePath(final String s) {
        StringBuilder sb = null; //create on demand
        char ch;
        final int len = s.length();
        for (int i = 0; i < len; i++) {
            ch = s.charAt(i);

            if (('A' <= ch && ch <= 'Z') || // 'A'..'Z'
                    ('a' <= ch && ch <= 'z') || // 'a'..'z'
                    ('0' <= ch && ch <= '9') || // '0'..'9'
                    ch == '/' || ch == '\'' || ch == ' ' || ch == '.' || ch == '~' || ch == '-' || ch == '_'
                    || ch == '@' || ch == '!' || ch == '(' || ch == ')' || ch == ';' || ch == ',' || ch == '+'
                    || ch == '$') {
                if (sb != null) {
                    sb.append(ch);
                }
            } else if ((int) ch <= 0x007f) // other ASCII
            {
                if (sb == null) {
                    final String soFar = s.substring(0, i);
                    sb = new StringBuilder(len + 16);
                    sb.append(soFar);
                }
                sb.append(hex[ch]);
            } else if ((int) ch <= 0x07FF) // non-ASCII <= 0x7FF
            {
                if (sb == null) {
                    final String soFar = s.substring(0, i);
                    sb = new StringBuilder(len + 16);
                    sb.append(soFar);
                }
                sb.append(hex[0xc0 | (ch >> 6)]);
                sb.append(hex[0x80 | (ch & 0x3F)]);
            } else // 0x7FF < ch <= 0xFFFF
            {
                if (sb == null) {
                    final String soFar = s.substring(0, i);
                    sb = new StringBuilder(len + 16);
                    sb.append(soFar);
                }
                sb.append(hex[0xe0 | (ch >> 12)]);
                sb.append(hex[0x80 | ((ch >> 6) & 0x3F)]);
                sb.append(hex[0x80 | (ch & 0x3F)]);
            }
        }
        return (sb != null ? sb.toString() : s);
    }

    private final static String[] hex = { "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", "%08", "%09",
            "%0a", "%0b", "%0c", "%0d", "%0e", "%0f", "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17", "%18",
            "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f", "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27",
            "%28", "%29", "%2a", "%2b", "%2c", "%2d", "%2e", "%2f", "%30", "%31", "%32", "%33", "%34", "%35", "%36",
            "%37", "%38", "%39", "%3a", "%3b", "%3c", "%3d", "%3e", "%3f", "%40", "%41", "%42", "%43", "%44", "%45",
            "%46", "%47", "%48", "%49", "%4a", "%4b", "%4c", "%4d", "%4e", "%4f", "%50", "%51", "%52", "%53", "%54",
            "%55", "%56", "%57", "%58", "%59", "%5a", "%5b", "%5c", "%5d", "%5e", "%5f", "%60", "%61", "%62", "%63",
            "%64", "%65", "%66", "%67", "%68", "%69", "%6a", "%6b", "%6c", "%6d", "%6e", "%6f", "%70", "%71", "%72",
            "%73", "%74", "%75", "%76", "%77", "%78", "%79", "%7a", "%7b", "%7c", "%7d", "%7e", "%7f", "%80", "%81",
            "%82", "%83", "%84", "%85", "%86", "%87", "%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f", "%90",
            "%91", "%92", "%93", "%94", "%95", "%96", "%97", "%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f",
            "%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7", "%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae",
            "%af", "%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7", "%b8", "%b9", "%ba", "%bb", "%bc", "%bd",
            "%be", "%bf", "%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7", "%c8", "%c9", "%ca", "%cb", "%cc",
            "%cd", "%ce", "%cf", "%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7", "%d8", "%d9", "%da", "%db",
            "%dc", "%dd", "%de", "%df", "%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7", "%e8", "%e9", "%ea",
            "%eb", "%ec", "%ed", "%ee", "%ef", "%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7", "%f8", "%f9",
            "%fa", "%fb", "%fc", "%fd", "%fe", "%ff" };
}