Java tutorial
/* * #%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" }; }