com.bluexml.side.Integration.alfresco.xforms.webscript.XFormsWork.java Source code

Java tutorial

Introduction

Here is the source code for com.bluexml.side.Integration.alfresco.xforms.webscript.XFormsWork.java

Source

/*
Copyright (C) 2007-2011  BlueXML - www.bluexml.com
    
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
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
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
*/

package com.bluexml.side.Integration.alfresco.xforms.webscript;

import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.Map.Entry;

import javax.xml.parsers.DocumentBuilderFactory;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.search.impl.lucene.ADMLuceneSearcherImpl;
import org.alfresco.repo.search.impl.lucene.LuceneConfig;
import org.alfresco.repo.search.impl.lucene.LuceneIndexer;
import org.alfresco.repo.search.impl.lucene.LuceneSearcher;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.authority.AuthorityDAO;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.workflow.WorkflowModel;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.LimitBy;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.cmr.workflow.WorkflowDefinition;
import org.alfresco.service.cmr.workflow.WorkflowInstance;
import org.alfresco.service.cmr.workflow.WorkflowPath;
import org.alfresco.service.cmr.workflow.WorkflowService;
import org.alfresco.service.cmr.workflow.WorkflowTask;
import org.alfresco.service.cmr.workflow.WorkflowTaskDefinition;
import org.alfresco.service.cmr.workflow.WorkflowTaskQuery;
import org.alfresco.service.cmr.workflow.WorkflowTaskState;
import org.alfresco.service.namespace.QName;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.queryParser.QueryParser;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;

import com.bluexml.side.Integration.alfresco.xforms.webscript.XFormsWebscript.XFormsQueryType;
import com.bluexml.side.form.utils.DOMUtil;
import com.thoughtworks.xstream.XStream;

public class XFormsWork implements RunAsWork<String> {
    // bug #1685 allow to search on system properties
    private static final boolean FORCE_SYSTEM_PROPERTIES_RESOLUTION = true;

    private static final String WEBSCRIPT_SEPARATOR = "{::}";
    /** */
    private static Log logger = LogFactory.getLog(XFormsWork.class);
    private static XStream xstream = null;

    private final XFormsWebscript formsWebscript;
    private final XFormsQueryType queryType;
    private final DataLayer dataLayer;
    private final Map<String, String> parameters;
    private final ServiceRegistry serviceRegistry;

    private static final String BLUEXML_MODEL_URI = "http://www.bluexml.com/model/content/";
    private static final String BLUEXML_WORKFLOW_URI = "http://www.bluexml.com/model/workflow/";

    private static Map<QName, Set<QName>> searchableAttributesCache = new HashMap<QName, Set<QName>>();

    // Variable used to return the id of the Class which is suspected to
    // generate a problem (exception)
    private String faultyId = null;

    /*
     * public XFormsWork(XFormsWebscript formsWebscript, XFormsQueryType
     * queryType, BrowseBean
     * browseBean, DataLayer dataLayer, Map<String, String> parameters) {
     * super();
     * this.formsWebscript = formsWebscript; this.queryType = queryType;
     * this.browseBean =
     * browseBean; this.dataLayer = dataLayer; this.parameters = parameters; }
     */

    public XFormsWork(XFormsWebscript formsWebscript, XFormsQueryType queryType, Map<String, String> parameters,
            ServiceRegistry serviceRegistry) {
        super();
        this.formsWebscript = formsWebscript;
        this.queryType = queryType;
        this.dataLayer = new DataLayer(serviceRegistry);
        this.parameters = parameters;
        this.serviceRegistry = serviceRegistry;
    }

    private class DoWorkInTransaction implements RetryingTransactionCallback<String> {

        /**
         * 
         */
        public DoWorkInTransaction() {
            super();
        }

        @SuppressWarnings("synthetic-access")
        public String execute() throws Throwable {
            String result = null;
            faultyId = null; // reset faultyId on each execution

            dataLayer.setInTransaction(true);
            // if (queryType == XFormsWebscript.XFormsQueryType.create) {
            // result = create();
            // }
            // if (queryType == XFormsWebscript.XFormsQueryType.update) {
            // result = update();
            // }

            logger.debug("queryType :" + queryType);
            if (queryType == XFormsWebscript.XFormsQueryType.read) {
                result = read();
            } else if (queryType == XFormsWebscript.XFormsQueryType.list) {
                result = list();
            } else if (queryType == XFormsWebscript.XFormsQueryType.batch) {
                result = batch();
            } else if (queryType == XFormsWebscript.XFormsQueryType.workflow) {
                result = wfManage();
            } else if (queryType == XFormsWebscript.XFormsQueryType.addToPackage) {
                result = addInPackage();
            } else if (queryType == XFormsWebscript.XFormsQueryType.nodeinfo) {
                result = nodeInfo();
            } else if (queryType == XFormsWebscript.XFormsQueryType.upload) {
                result = upload();
            } else if (queryType == XFormsWebscript.XFormsQueryType.service) {
                result = service();
            } else if (queryType == XFormsWebscript.XFormsQueryType.createPath) {
                result = createPath();
            } else if (queryType == XFormsWebscript.XFormsQueryType.labels) {
                result = labels();
            } else if (queryType == XFormsWebscript.XFormsQueryType.enum_) {
                result = enum_();
            } else if (queryType == XFormsWebscript.XFormsQueryType.auth) {
                result = authenticate();
            } else if (queryType == XFormsWebscript.XFormsQueryType.delete) {
                result = delete();
            } else if (queryType == XFormsWebscript.XFormsQueryType.help) {
                result = help();
            }
            dataLayer.setInTransaction(false);

            logger.debug("BlueXML XForms webscript worker (opcode '" + queryType + "'). Returning:");
            logger.debug(result);
            logger.debug(">-----------<");

            return result;
        }

    }

    public String doWork() {
        String result = null;
        try {
            boolean readOnly = false;
            boolean requiresNew = true;
            if (queryType == XFormsWebscript.XFormsQueryType.read
                    || queryType == XFormsWebscript.XFormsQueryType.labels
                    || queryType == XFormsWebscript.XFormsQueryType.enum_
                    || queryType == XFormsWebscript.XFormsQueryType.list) {
                readOnly = true;
            }
            RetryingTransactionHelper transactionHelper = formsWebscript.getTransactionHelper();
            transactionHelper.setMaxRetries(2);
            result = transactionHelper.doInTransaction(new DoWorkInTransaction(), readOnly, requiresNew);
        } catch (Exception e) {
            logger.error(e, e);
            StringBuffer sb = new StringBuffer();
            HashSet<Throwable> causes = new HashSet<Throwable>();
            causes.add(e);
            exceptionToString(e, sb, causes);
            result = sb.toString();
        }
        return result;
    }

    private static XStream getXStream() {
        if (xstream == null) {
            xstream = new XStream();
        }
        return xstream;
    }

    /**
     * Displays a help text for the <b>public</b> services of this webscript.
     * 
     * @return the help text.
     */
    protected String help() {
        StringBuffer buffer = new StringBuffer();
        // don't show the lines that are commented out: internal use
        buffer.append("BlueXML XForms Webscript for Alfresco\n");
        buffer.append("-------------------------------------\n");
        buffer.append("List of services (see the documentation for specifics).\n");
        buffer.append("\n");
        buffer.append("/xforms/auth: authenticate a user.\n");
        buffer.append("/xforms/batch: perform a set of CRUD operations against the repository.\n");
        // buffer.append("/xforms/delete: delete a content from the repository.\n");
        buffer.append("/xforms/enum: list items of a dynamic enumeration.\n");
        buffer.append("/xforms/help: show this help.\n");
        // Amenel: never got to know the use of "labels" TODO: check and test
        buffer.append("/xforms/labels: get a translation for a dynamic enumeration litteral.\n");
        buffer.append("/xforms/list: list all objects of a given type.\n");
        buffer.append("/xforms/mkdir: create a path in Alfresco.\n");
        // buffer.append("/xforms/package: \n"); // don't show: internal use
        buffer.append("/xforms/read: read an object of any type from the repository.\n");
        // buffer.append("/xforms/service: call functions of chosen services from the Alfresco's API.\n");
        buffer.append("/xforms/upload: upload a file to the repository.\n");
        buffer.append("/xforms/workflow: call functions of the WorkflowService interface.\n");
        buffer.append("-------------------------------------\n");

        return buffer.toString();
    }

    /**
     * Tests whether user credentials authenticate successfully with Alfresco.
     * <p/>
     * Parameters: "username", "password".
     * <p/>
     * 
     * @return "success" if successful, "failure" if an exception occurred or
     *         the auth failed.
     */
    protected String authenticate() {
        AuthenticationService authService = serviceRegistry.getAuthenticationService();
        String username = parameters.get("username");
        String password = parameters.get("password");

        try {
            authService.authenticate(username, password.toCharArray());
        } catch (AuthenticationException e) {
            return "failure";
        }
        return "success";
    }

    /**
     * Provides information about one or several nodes. Modes:
     * <ol>
     * <li>node content information: returns info about the node content.
     * Parameter: "nodeId"
     * <li>objects information: returns information about node(s). Parameters:
     * "ids", "format"
     * <li>find object and retrieve information: finds the object that has a
     * specific value in a specific property and returns information about the
     * node. Parameters: "datatype", "identifier", "id". Optional parameters:
     * "format", "labelLength".
     * </ol>
     * <p>
     * See the individual handler functions for the format of the parameters.
     * 
     * @return an empty string if any problem or the comma-separated string
     *         containing the
     *         information built. Currently, {full node id}, {content size in
     *         bytes}.
     */
    protected String nodeInfo() {
        String nodeId = parameters.get("nodeId");
        if (nodeId != null) {
            return contentInfo(nodeId);
        }

        String ids = parameters.get("ids");
        if (ids != null) {
            String sharedFormat = parameters.get("format");
            return nodeObjectsInfo(ids, sharedFormat);
        }
        String datatype = parameters.get("datatype");
        String identifier = parameters.get("identifier");

        // configure the filtering/limiting
        QName identifierQName = null;
        boolean includeSystemProperties = FORCE_SYSTEM_PROPERTIES_RESOLUTION;
        if (StringUtils.trimToNull(identifier) != null) {
            identifierQName = resolveIdentifierQName(identifier, datatype);
            includeSystemProperties = (identifierQName != null);
        }

        if (datatype != null) {
            String format = parameters.get("format");
            String labelLength = parameters.get("labelLength");
            String idValue = parameters.get("id");
            return resolveObjectInfo(datatype, identifier, format, labelLength, idValue, includeSystemProperties);
        }

        return "";
    }

    /**
     * Finds the node (of the given datatype) that has the given id value on the
     * identifier property
     * and formats its noderef id, label and qname into a string that has the
     * format
     * "id{SEPARATOR}label{SEPARATOR}prefixed_qname".
     * 
     * @param datatype
     *            a prefixed content type, e.g. "cm:person"
     * @param identifier
     *            the local name of a property present in the datatype's
     *            definition, e.g. "userName"
     * @param format
     *            the pattern for formatting the label. e.g.
     *            "format=firstName@lastName@{ (}@email@{)}"
     * @param labelLength
     *            the length at which to truncate the label that will be
     *            computed. If "0", no
     *            truncation happens.
     * @param idValue
     *            the value which, when found on the identifier property, elects
     *            the node as the one
     *            being looked for. e.g. "johndoe"
     * @return <code>null</code> if either no identifier property was found or
     *         no node was found
     *         with the id value on that property. Otherwise, returns the info
     *         string for the node,
     *         e.g."workspace://SpacesStore/ca151555-95e0-4361-aa4e-0050adb7027d{::}John Doe (johndoe@email.com){::}cm:person"
     */
    private String resolveObjectInfo(String datatype, String identifier, String format, String labelLength,
            String idValue, boolean includeSystemProperties) {
        // get the identifier property's qname
        QName idQName = resolveIdentifierQName(identifier, datatype);
        if (idQName == null) {
            return null;
        }

        // get all objects of the datatype
        ResultSet luceneResultSet = getResultSet(datatype, new ArrayList<String>(), 0, null,
                includeSystemProperties);
        int nbResults = luceneResultSet.length();
        NodeRef electedNode = null;

        // test each object against the id value
        for (int i = 0; i < nbResults; i++) {
            NodeRef nodeRef = luceneResultSet.getNodeRef(i);
            String nodeValue = resolveIdentifierValue(nodeRef, idQName);
            if (StringUtils.equals(nodeValue, idValue)) {
                electedNode = nodeRef;
                break;
            }
        }

        luceneResultSet.close();

        // build the label for the elected node
        if (electedNode == null) {
            return null;
        }
        // include system properties as this function is likely to be used for system types
        String label = dataLayer.getLabelForNode(electedNode, format, true);

        String trimmedLabel = label;
        try {
            int length = Integer.parseInt(labelLength);
            trimmedLabel = StringUtils.left(label, length);
        } catch (Exception e) {
            // nothing to do, the trimmed label remains the same as the initial label
        }
        QName qName = serviceRegistry.getNodeService().getType(electedNode);

        String objectInfo = electedNode.toString() + WEBSCRIPT_SEPARATOR + trimmedLabel + WEBSCRIPT_SEPARATOR
                + qName.toPrefixString();
        return objectInfo;
    }

    /**
     * Builds a string containing some information about the given ids. The ids
     * should refer to
     * objects of the same type because there is only one pattern for formatting
     * the node labels.<br/>
     * Currently, each information item contains information related to a node
     * under the format:
     * "id{SEPARATOR}label{SEPARATOR}prefixed_qname"
     * 
     * @param ids
     *            a comma-separated list of node ids with protocol and store
     * @param format
     *            the format pattern for the labels
     * @return a comma separated list with the same number of items as the list
     *         of node ids.
     */
    private String nodeObjectsInfo(String ids, String format) {
        StringBuffer result = new StringBuffer("");

        String[] splittedIds = StringUtils.split(ids, ',');
        boolean first = true;
        if (splittedIds != null) {
            for (String id : splittedIds) {
                String objectInfo;
                try {
                    // get the info for each node
                    NodeRef nodeRef = new NodeRef(id);
                    String label = dataLayer.getLabelForNode(nodeRef, format, false);
                    QName qName = serviceRegistry.getNodeService().getType(nodeRef);
                    String qnameStr = qName.toPrefixString();
                    objectInfo = nodeRef.toString() + WEBSCRIPT_SEPARATOR + label + WEBSCRIPT_SEPARATOR + qnameStr;
                } catch (InvalidNodeRefException inre) {
                    objectInfo = "<INVALID ID>";
                }
                if (first == false) {
                    result.append(',');
                }
                result.append(objectInfo);
                first = false;
            }
        }
        return result.toString();
    }

    /**
     * Builds some information about the content of a node.
     * 
     * @param nodeId
     *            complete (store + workspace) id
     * @return an empty string if any problem or the comma-separated string
     *         containing the
     *         information built. Currently, {full node id},{content size in
     *         bytes}.
     */
    private String contentInfo(String nodeId) {
        try {
            String result;
            NodeRef nodeRef = new NodeRef(nodeId);
            Serializable name = this.serviceRegistry.getNodeService().getProperty(nodeRef, ContentModel.PROP_NAME);
            Serializable content = this.serviceRegistry.getNodeService().getProperty(nodeRef,
                    ContentModel.PROP_CONTENT);
            if (content == null) {
                return "";
            }
            ContentData contentData = (ContentData) content;

            long size = contentData.getSize();

            result = name.toString() + "," + size;
            return result;

        } catch (Exception e) {
            logger.error("Failed to provide info for node id: " + nodeId + ". An error occurred.");
        }
        return "";
    }

    private void exceptionToString(Throwable e, StringBuffer sb, HashSet<Throwable> causes) {
        exceptionToStringOpenTag("exception", sb);
        exceptionToStringAddType(e.getClass(), sb);
        // exceptionToStringAddEntry("message", e.getMessage(), sb);
        exceptionToStringOpenTag("message", sb);
        sb.append(e.getMessage());
        if (faultyId != null) {
            // Brice : added information to return suspected id
            exceptionToStringAddEntry("suspectedId", faultyId, sb);
        }
        exceptionToStringCloseTag("message", sb);
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        String stacktrace = sw.toString();
        exceptionToStringAddEntry("stacktrace", stacktrace, sb);
        Throwable cause = e.getCause();
        if (cause != null && !causes.contains(cause)) {
            causes.add(cause);
            exceptionToStringOpenTag("cause", sb);
            exceptionToString(cause, sb, causes);
            exceptionToStringCloseTag("cause", sb);
        }
        exceptionToStringCloseTag("exception", sb);
    }

    private void exceptionToStringAddType(Class<?> exceptionClass, StringBuffer sb) {
        exceptionToStringOpenTag("type", sb);
        exceptionToStringAddEntry("name", exceptionClass.getCanonicalName(), sb);
        Class<?> superclass = exceptionClass.getSuperclass();
        if (superclass != null) {
            exceptionToStringOpenTag("superclass", sb);
            exceptionToStringAddType(superclass, sb);
            exceptionToStringCloseTag("superclass", sb);
        }
        exceptionToStringCloseTag("type", sb);
    }

    private void exceptionToStringAddEntry(String tagName, String content, StringBuffer sb) {
        exceptionToStringOpenTag(tagName, sb);
        sb.append(StringEscapeUtils.escapeXml(content));
        exceptionToStringCloseTag(tagName, sb);
    }

    private void exceptionToStringOpenTag(String tagName, StringBuffer sb) {
        sb.append("<");
        sb.append(tagName);
        sb.append(">");
    }

    private void exceptionToStringCloseTag(String tagName, StringBuffer sb) {
        sb.append("</");
        sb.append(tagName);
        sb.append(">");
    }

    /**
     * @param xmlResult
     * @param id
     * @param label
     * @param qname
     *            the actual datatype of the node being added to the results
     *            list.
     */
    private void appendResult(StringBuffer xmlResult, String id, String label, String qname) {
        // the tag names used here must be in sync with the ones defined in the XForms controller
        xmlResult.append("<item><id>");
        xmlResult.append(StringEscapeUtils.escapeXml(id));
        xmlResult.append("</id><label>");
        xmlResult.append(StringEscapeUtils.escapeXml(label));
        xmlResult.append("</label><qname>");
        if (qname != null) {
            xmlResult.append(StringEscapeUtils.escapeXml(qname));
        }
        xmlResult.append("</qname></item>\n");
    }

    /**
     * @return
     * @throws Exception
     */
    protected String labels() throws Exception {
        String query = parameters.get("query");
        StringBuffer xmlResult = new StringBuffer("");

        xmlResult.append("<results>\n");

        if (query.startsWith("enum;")) {
            String[] ids = query.split(";");
            appendResult(xmlResult, ids[1], getLitteralTranslation(ids[1]), null);
        } else {
            String[] ids = query.split(";");
            for (String id : ids) {
                NodeRef nodeRef = new NodeRef(id);
                // Amenel: I suspect id refers to LitteralTranslation objects
                // but not sure...
                // "value" is dependent on the model (changes should propagate)
                String nodeName = resolveNodeName(nodeRef, "value", false);
                appendResult(xmlResult, nodeRef.toString(), nodeName, null);
            }
        }

        xmlResult.append("</results>");
        return xmlResult.toString();
    }

    private String getLitteralTranslation(String code) throws SQLException {
        java.sql.ResultSet resultSet = SQLRequester.executeQuery(getSQLQueryLitteral(code));
        if (resultSet.next()) {
            return resultSet.getString("VALUE");
        }
        return null;
    }

    /**
     * @return
     */
    protected String enum_() {
        String type = parameters.get("type");

        String parent = parameters.get("parent");
        String context = parameters.get("context");
        String query = parameters.get("query");
        String slimit = parameters.get("limit");
        boolean limit = StringUtils.equals(slimit, "true");

        java.sql.ResultSet resultSet = SQLRequester.executeQuery(getSQLQuery(type, parent, context, query, limit));

        String code = null;
        String value = null;

        StringBuffer xmlResult = new StringBuffer("");
        xmlResult.append("<results>\n");
        try {
            while (resultSet.next()) {
                code = resultSet.getString("CODE");
                value = resultSet.getString("VALUE");
                if (StringUtils.trimToNull(code) == null) {
                    value = resultSet.getString("UUID");
                }
                appendResult(xmlResult, code, value, null);
            }
            SQLRequester.closeQuery(resultSet);
        } catch (SQLException e) {
            logger.error(e.getMessage(), e);
        }
        xmlResult.append("</results>");
        return xmlResult.toString();
    }

    /**
     * Provides the items to be listed in a selection widget. Returns an XML
     * string with root
     * '&lt;results&gt;':<br>
     * <ul>
     * <li>query (appears once)</li>
     * <ul>
     * <li>count: the total number of objects available in the Lucene result set
     * </li>
     * <li>maxResults: if non-zero, sets the maximum number of objects requested
     * by the client. Returned as received.</li>
     * <li>returned: the number of result items returned to the client</li>
     * <li>filteredOut: if a filtering association was given, tells the number
     * of items that have been filtered out because they already have a
     * reference on that association.</li>
     * <li>typeFound: if <code>false</code>, the type is unknown to Alfresco.</li>
     * <li>query: a string to filter the items. The value returned is the
     * TrimToEmpty'ed version of the value that was received.</li>
     * </ul>
     * <li>item (number of occurrences is 0..*)</li>
     * <ul>
     * <li>id: the complete Alfresco id, including protocol and workspace</li>
     * <li>value: the display label for the item</li>
     * </ul>
     * </ul> Parameters: ("NULL-able" means that an empty value is accepted.)<br/>
     * "type": the data type to search. This parameter is MANDATORY.<br/>
     * "query": the search keyword string. NULL-able.<br/>
     * "queryFilter": an additional search keyword string. NULL-able.<br/>
     * "format": the format pattern for the label of objects. NULL-able.<br/>
     * "maxLength": the length at which labels computed using the format are
     * truncated. NULL-able.<br/>
     * "maxResults": the max number of items allowed in the result set.
     * NULL-able.<br/>
     * "identifier": the local name of a property whose value will be used as
     * the id of results.
     * NULL-able. Quite obviously, that field MUST 1- be non-null, 2- be an
     * actual identifier (i.e.
     * no value is duplicated in the value set)<br/>
     * "filterAssoc": the qualified name of an association by which it will be
     * determined whether
     * nodes are referenced. If present, nodes that are already pointed to by
     * that association will
     * be filtered out of the results. NULL-able.<br/>
     * "isComposition": if "1" and "filterAssoc" is given, denotes that the
     * association is a
     * composition. NULL-able.<br/>
     * "isSearchMode": if "1", an empty item list will be returned on an empty
     * "query". NULL-able.<br/>
     * "luceneQuery": a Lucene query that overrides the one that's normally
     * built here. NULL-able.<br/>
     * 
     * @return
     * @throws Exception
     */
    protected String list() throws Exception {
        class ResultBean { // #1406
            String id;
            String label;
            String qname;

            ResultBean(String nodeId, String nodeLabel, String nodeQName) {
                this.id = nodeId;
                this.label = nodeLabel;
                this.qname = nodeQName;
            }
        }

        logger.debug("Initialization of system time: " + System.nanoTime());
        Timer globalTimer = new Timer();
        Timer luceneTimer = new Timer();
        Timer noderefLuceneTimer = new Timer();
        Timer noderefTimer = new Timer();
        Timer labelTimer = new Timer();
        Timer qnameTimer = new Timer();
        Timer filteringTimer = new Timer();
        Timer referenceTimer = new Timer();
        Timer sortTimer = new Timer();

        globalTimer.start();
        //
        // collect parameters. Some of these are not necessary before the filtering/limiting/sorting
        // part but for convenience, all parameters are collected with, possibly, some work done
        // here that will come out as useless, but it makes this function more readable.
        //
        String type = parameters.get("type");
        if (StringUtils.trimToNull(type) == null) {
            return getListOpcodeDefaultResult("<Invalid type>", false);
        }
        String identifier = parameters.get("identifier"); // #1529
        String format = parameters.get("format");
        // URL-decode the format pattern
        if (StringUtils.trimToNull(format) != null) {
            try {
                format = URLDecoder.decode(format, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                logger.error("UTF-8 is unsupported. Format is defaulted to 'uuid'.");
                format = null;
            }
        }

        int maxResults = 10; // arbitrary default value
        String maxParam = parameters.get("maxResults");
        try {
            maxResults = Integer.parseInt(maxParam);
        } catch (NumberFormatException ne) {
            logger.error("'list' op: wrong number in parameter 'maxResults' (" + maxParam + ")");
        }

        int maxLength = 0; // arbitrary default value
        String lengthParam = parameters.get("maxLength");
        try {
            maxLength = Integer.parseInt(lengthParam);
        } catch (NumberFormatException nfe) {
            logger.error("'list' op: wrong number in parameter 'maxLength' (" + lengthParam + ")");
        }
        // ** #1310
        String filterAssoc = parameters.get("filterAssoc");
        boolean applyFilterAssoc = (StringUtils.trimToNull(filterAssoc) != null);
        String isCompositionParam = parameters.get("isComposition");
        boolean isComposition = StringUtils.equals(isCompositionParam, "1");
        // ** #1310

        String isSearchModeParam = parameters.get("isSearchMode");
        boolean isSearchMode = StringUtils.equals(isSearchModeParam, "1");

        List<String> searchedValues = new ArrayList<String>();
        String query = StringUtils.trimToNull(parameters.get("query"));
        if (query != null) {
            searchedValues.add(query);
        } else {
            if (isSearchMode) {
                return getListOpcodeDefaultResult(query, true); // empty list for search widgets
            }
        }

        String queryFilter = parameters.get("queryFilter");
        if (StringUtils.trimToNull(queryFilter) != null) {
            searchedValues.add(queryFilter);
        }

        String userLuceneQuery = StringUtils.trimToNull(parameters.get("luceneQuery"));
        if (userLuceneQuery != null) {
            try {
                // double-decoded because the string was double-encoded.
                userLuceneQuery = URLDecoder.decode(userLuceneQuery, "UTF-8");
                userLuceneQuery = URLDecoder.decode(userLuceneQuery, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                logger.error("UTF-8 is unsupported. User Lucene expression will be ignored.");
                userLuceneQuery = null;
            }
        }

        // configure the filtering/limiting
        QName identifierQName = null;
        boolean includeSystemProperties = FORCE_SYSTEM_PROPERTIES_RESOLUTION;
        if (StringUtils.trimToNull(identifier) != null) {
            identifierQName = resolveIdentifierQName(identifier, type);
            includeSystemProperties = (identifierQName != null);
        }

        // perform the search
        luceneTimer.start();
        ResultSet luceneResultSet = getResultSet(type, searchedValues, maxResults, userLuceneQuery,
                includeSystemProperties);
        luceneTimer.stop();
        if (luceneResultSet == null) {
            // result for the pathological case when the type is unknown to Alfresco.
            return getListOpcodeDefaultResult(query, false);
        }
        int luceneResultLength = luceneResultSet.length();
        if (luceneResultLength == 0) {
            return getListOpcodeDefaultResult(query, true);
        }

        //
        //
        // collect items and apply filtering and/or limiting. Node names/labels are also computed.
        //
        //

        /**
         * whether we need to collect all elements before applying the filtering
         * & limiting
         */
        boolean collectAllNodes = false;
        if ((includeSystemProperties) && (query != null)) {
            // when searching system datatypes (e.g. "cm:permson"), we may have to collect all nodes
            // before filtering (happens when no property of the datatype is indexed)
            List<QName> subTypes = formsWebscript.getSubTypes(type);
            Set<QName> attributes = getSearchableAttributes(subTypes.get(0), includeSystemProperties);
            if (attributes.size() == 0) {
                // the datatype is not indexed
                collectAllNodes = true;
            }
        }

        int returnAtMost;
        if (collectAllNodes == false) {
            if (maxResults > 0) {
                returnAtMost = Math.min(luceneResultLength, maxResults);
            } else {
                returnAtMost = luceneResultLength;
            }
        } else {
            returnAtMost = luceneResultLength;
        }

        int effectivelyReturned = 0;
        int filteredOut = 0;
        // collect items and apply filtering and/or limiting
        filteringTimer.start();
        List<ResultBean> resultBeanList = new ArrayList<ResultBean>(returnAtMost);
        noderefLuceneTimer.start();
        List<NodeRef> resultSet = luceneResultSet.getNodeRefs();
        noderefLuceneTimer.stop();
        luceneResultSet.close();
        for (int i = 0; i < returnAtMost; i++) {
            noderefTimer.start();
            NodeRef nodeRef = resultSet.get(i);
            noderefTimer.stop();
            labelTimer.start();
            String label = resolveNodeName(nodeRef, format, includeSystemProperties);
            labelTimer.stop();
            if (maxLength > 0) {
                label = StringUtils.left(label, maxLength);
            }
            qnameTimer.start();
            QName qname = serviceRegistry.getNodeService().getType(nodeRef); // #1510
            String id;
            if (identifierQName == null) {
                id = nodeRef.toString();
            } else {
                id = resolveIdentifierValue(nodeRef, identifierQName);
            }
            String qnameStr = qname.toPrefixString(formsWebscript.getNamespacePrefixResolver());
            qnameTimer.stop();
            ResultBean aBean = new ResultBean(id, label, qnameStr);
            boolean isAddableBean = true; // whether the result will be added to the item list
            if (includeSystemProperties) {
                // for system datatypes, search the label (in case indexing is off for that type)
                if ((collectAllNodes == false) && (query != null) && (aBean.label.contains(query) == false)) {
                    isAddableBean = false;
                    filteredOut++;
                }
            } else {
                // retrieving objects of a standard BlueXML generated type
                if (applyFilterAssoc) {
                    referenceTimer.start();
                    if (dataLayer.isRefencenced(nodeRef, filterAssoc, isComposition) == true) {
                        // do not add if already referenced via the filtering association
                        isAddableBean = false;
                        filteredOut++;
                    }
                    referenceTimer.stop();
                }
            }
            if (isAddableBean) {
                resultBeanList.add(aBean);
                effectivelyReturned++;
            }
        }
        filteringTimer.stop();

        //
        // sort the result list by computed labels. #1406
        sortTimer.start();
        Collections.sort(resultBeanList, new Comparator<ResultBean>() {
            public int compare(ResultBean o1, ResultBean o2) {
                return o1.label.compareTo(o2.label);
            }
        });
        sortTimer.stop();

        //
        // write all results in the items string buffer
        //
        StringBuffer itemsBuf = new StringBuffer("");
        if (collectAllNodes == true) {
            // in case all nodes were collected, the filtering and limiting takes place here
            filteredOut = 0;
            effectivelyReturned = 0;
            for (ResultBean aBean : resultBeanList) {
                if ((query != null)
                        && StringUtils.contains(aBean.label.toLowerCase(), query.toLowerCase()) == false) {
                    filteredOut++;
                } else {
                    appendResult(itemsBuf, aBean.id, aBean.label, aBean.qname);
                    effectivelyReturned++;
                    if (effectivelyReturned == returnAtMost) {
                        break;
                    }
                }
            }
        } else { // filtering and limiting already happened
            for (ResultBean aBean : resultBeanList) {
                appendResult(itemsBuf, aBean.id, aBean.label, aBean.qname);
            }
        }

        StringBuffer xmlResult = new StringBuffer("");
        xmlResult.append("<results>\n");
        xmlResult.append("<query>\n");
        xmlResult.append("  <count>").append(luceneResultLength).append("</count>\n");
        xmlResult.append("  <maxResults>").append(maxResults).append("</maxResults>\n");
        xmlResult.append("  <returned>").append(effectivelyReturned).append("</returned>\n");
        xmlResult.append("  <filteredOut>").append(filteredOut).append("</filteredOut>\n");
        xmlResult.append("  <typeFound>true</typeFound>\n");
        xmlResult.append("  <query>").append(StringUtils.trimToEmpty(query)).append("</query>\n");
        xmlResult.append("</query>\n");

        xmlResult.append(itemsBuf);

        xmlResult.append("</results>");

        globalTimer.stop();
        logger.debug(">>><<< Timing:");
        logger.debug("Total : " + globalTimer.getTotalTime() + " ns.");
        logger.debug(" Lucene : " + luceneTimer.getTotalTime() + " ns.");
        logger.debug(" Filtering : " + filteringTimer.getTotalTime() + " ns.");
        logger.debug("  Nref Lucene : " + noderefLuceneTimer.getTotalTime() + " ns.");
        logger.debug("  Nref SIDE: " + noderefTimer.getTotalTime() + " ns.");
        logger.debug("  Labels : " + labelTimer.getTotalTime() + " ns.");
        logger.debug("  QName : " + qnameTimer.getTotalTime() + " ns.");
        logger.debug("  Ref? : " + referenceTimer.getTotalTime() + " ns.");
        logger.debug(" Sort : " + sortTimer.getTotalTime() + " ns.");
        logger.debug(">>><<<");

        return xmlResult.toString();
    }

    /**
     * Builds a string for cases when the list operation returns no results.
     * 
     * @param query
     *            the search string that was provided
     * @param status
     *            whether the data type provided is a legitimate type registered
     *            with Alfresco. Was
     *            added for debug purposes.
     * @return
     */
    private String getListOpcodeDefaultResult(String query, boolean status) {
        StringBuffer xmlResult = new StringBuffer("");
        xmlResult.append("<results>\n");
        xmlResult.append("<query>\n");
        xmlResult.append("  <count>0</count>\n");
        xmlResult.append("  <maxResults>0</maxResults>\n");
        xmlResult.append("  <returned>0</returned>\n");
        xmlResult.append("  <filteredOut>0</filteredOut>\n");
        xmlResult.append("  <typeFound>").append(status).append("</typeFound>\n");
        xmlResult.append("  <query>").append(StringUtils.trimToEmpty(query)).append("</query>\n");
        xmlResult.append("</query>\n");
        xmlResult.append("</results>");
        return xmlResult.toString();
    }

    /**
     * Returns the qname of the property whose local name matches the
     * identifier.
     * 
     * @param identifier
     *            a non-<code>null</code> string that SHOULD be the local name
     *            of one the node's
     *            properties as can be found in the type definition.
     * @param type
     *            the node type whose definition should contain the identifier
     */
    private QName resolveIdentifierQName(String identifier, String type) { // #1529
        List<QName> listQNames = formsWebscript.getSubTypes(type);
        if ((listQNames != null) && (listQNames.size() != 0)) {
            QName qname = listQNames.get(0);
            TypeDefinition typeDef = serviceRegistry.getDictionaryService().getType(qname);
            Map<QName, PropertyDefinition> properties = typeDef.getProperties();

            for (QName property : properties.keySet()) {
                if (property.getLocalName().equalsIgnoreCase(identifier)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Resolved qname '" + property + "'for identifier '" + identifier + "'");
                    }
                    return property;
                }
            }
        }

        logger.error(
                "Failed in resolving identifier qname. identifier ='" + identifier + "', datatype='" + type + "'");
        return null;
    }

    /**
     * Provides the value of the node's identifier property.
     * 
     * @param nodeRef
     *            an existing node
     * @param identifierQName
     *            the qname of a node property
     * @return
     */
    private String resolveIdentifierValue(NodeRef nodeRef, QName identifierQName) { // #1529
        if (identifierQName == null) {
            return "";
        }
        Serializable value = serviceRegistry.getNodeService().getProperty(nodeRef, identifierQName);

        if (value == null) {
            return "";
        }

        return value.toString();
    }

    /**
     * Provides the label (i.e. user-readable form) for the node.
     * 
     * @param nodeRef
     * @param format
     *            the URL-decoded format pattern
     * @param includeSystemProperties
     *            if <code>true</code>, system properties are also considered,
     *            in addition to
     *            properties from the data models.
     * @return
     */
    private String resolveNodeName(NodeRef nodeRef, String format, boolean includeSystemProperties) {
        return dataLayer.getLabelForNode(nodeRef, format, includeSystemProperties);
    }

    private ResultSet getResultSet(String type, List<String> searchedValues, int maxResults, String userLuceneQuery,
            boolean includeSystemProperties) {
        SearchParameters searchParameters = createSearchParameters(type, searchedValues, maxResults,
                userLuceneQuery, includeSystemProperties);
        if (searchParameters == null) {
            return null;
        }

        return getUnsecureLuceneSearcher().query(searchParameters);
    }

    private String getSQLQuery(String type, String parent, String context, String query, boolean limit) {

        String rparent = StringUtils.trimToNull(parent);
        String rcontext = StringUtils.trimToNull(context);
        String rquery = StringUtils.trimToNull(query);

        StringBuffer sql_query = new StringBuffer("");
        sql_query.append("SELECT distinct L.uuid as UUID, L.code as CODE, LT.value as VALUE");
        sql_query.append(
                " FROM Litteral L, LitteralTranslation LT, EnumerationType ET, Litteral_translated_LitteralTranslation LTL, EnumerationType_typeOf_Litteral ETL");
        if (rparent != null) {
            sql_query.append(", Litteral LP, Litteral_parent_Litteral LPL");
        }
        if (rcontext != null) {
            sql_query.append(", VisibilityContext VC, Litteral_visibility_VisibilityContext LVVC");
        }
        sql_query.append(" WHERE L.id = LTL.Litteral");
        sql_query.append(" AND LTL.LitteralTranslation = LT.id");
        sql_query.append(" AND lang = 'fr'");
        sql_query.append(" AND ET.id = ETL.EnumerationType");
        sql_query.append(" AND ETL.Litteral = L.id");
        sql_query.append(" AND ET.name = '");
        sql_query.append(type);
        sql_query.append("'");
        if (rparent != null) {
            sql_query.append(" AND LP.code = '");
            sql_query.append(rparent);
            sql_query.append("' AND LPL.Litteral_source = L.id AND LPL.Litteral_target = LP.id");
        }
        if (rcontext != null) {
            sql_query.append(" AND VC.value = '");
            sql_query.append(rcontext);
            sql_query.append("' AND LVVC.Litteral = L.id AND LVVC.VisibilityContext = VC.id");
        }
        if (rquery != null) {
            sql_query.append(" AND LT.value like '%");
            sql_query.append(rquery);
            sql_query.append("%'");
        }
        if (limit) {
            sql_query.append(" LIMIT 0 , 10");
        }
        return sql_query.toString();
    }

    private String getSQLQueryLitteral(String code) {
        StringBuffer sql_query = new StringBuffer("");
        sql_query.append("SELECT LT.value as VALUE");
        sql_query.append(" FROM Litteral L, LitteralTranslation LT, Litteral_translated_LitteralTranslation LTL");
        sql_query.append(" WHERE L.code = '");
        sql_query.append(code);
        sql_query.append("'");
        sql_query.append(" AND L.id = LTL.Litteral");
        sql_query.append(" AND LTL.LitteralTranslation = LT.id");
        sql_query.append(" AND LT.lang = 'fr'");
        // Amenel: why is this condition on "L.code" repeated ?
        sql_query.append(" AND L.code = '");
        sql_query.append(code);
        sql_query.append("'");
        return sql_query.toString();
    }

    protected String delete() {
        String nodeRef = parameters.get("nodeRef");
        dataLayer.delete(nodeRef);
        return nodeRef;
    }

    protected String read() {
        String objectId = parameters.get("objectId");
        String result = dataLayer.read(objectId);
        return result;
    }

    protected String batch() throws Exception {
        String datas = parameters.get("datas");
        // build the DOM tree
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        InputSource source = new InputSource(new StringReader(datas));
        Document document = factory.newDocumentBuilder().parse(source);
        Element batchElement = document.getDocumentElement();

        List<Element> children = DOMUtil.getAllChildren(batchElement);
        Map<String, String> created = new HashMap<String, String>();
        String saveToPath = null;
        // ** #1544
        String saveToAttribute = batchElement.getAttribute("saveTo");
        if (StringUtils.trimToNull(saveToAttribute) != null) {
            parameters.put("path", saveToAttribute);
            String createdPath = createPath();
            if (StringUtils.trimToNull(createdPath) != null) {
                saveToPath = saveToAttribute;
            } else {
                logger.error("The target space '" + saveToAttribute
                        + "' does not exist and could not be created. Reverting to the default path.");
            }
        }
        // ** #1544
        String currentId = null;
        try {
            for (Element element : children) {
                replaceIds(element, created);
                if (StringUtils.equals(element.getTagName(), "create")) {
                    Element toCreate = DOMUtil.getChild(element, "class");
                    String transactionId = toCreate.getAttribute("id");
                    currentId = transactionId; // for faulty id management
                    NodeRef result = dataLayer.create(saveToPath, toCreate, null);
                    created.put(transactionId, result.toString());
                } else if (StringUtils.equals(element.getTagName(), "update")) {
                    Element toUpdate = DOMUtil.getChild(element, "class");
                    String nodeId = toUpdate.getAttribute("id");
                    currentId = nodeId; // for faulty id management
                    // ** #1421
                    String massAttr = toUpdate.getAttribute("massTagging");
                    boolean isMassTagging = StringUtils.equals(massAttr, "true");
                    if (isMassTagging) {
                        String result = dataLayer.updateMassTagging(nodeId, toUpdate);
                        created.put("massTagging", result.toString());
                        // ** #1421
                    } else {
                        NodeRef result = dataLayer.update(nodeId, toUpdate);
                        created.put(nodeId, result.toString());
                    }
                } else if (StringUtils.equals(element.getTagName(), "delete")) {
                    dataLayer.delete(element.getTextContent());
                } else if (StringUtils.equals(element.getTagName(), "requester")) {
                    if (StringUtils.equals(element.getTextContent(), "XFormsController") == true) {
                        // NOTHING YET
                    }
                } else if (StringUtils.equals(element.getTagName(), "attach")) {
                    String target = DOMUtil.getChild(element, "targetNode").getTextContent();
                    String filename = DOMUtil.getChild(element, "fileName").getTextContent();
                    String filepath = DOMUtil.getChild(element, "filePath").getTextContent();
                    String mimetype = DOMUtil.getChild(element, "mimeType").getTextContent();
                    String contentType = DOMUtil.getChild(element, "contentType").getTextContent();
                    String appendStr = DOMUtil.getChild(element, "appendSuffix").getTextContent();
                    boolean shouldAppendSuffix = !(StringUtils.equals(appendStr, "false"));

                    // we must ensure the receiver is a valid id
                    String receiver = target;
                    try {
                        @SuppressWarnings("unused")
                        NodeRef nodeRef = new NodeRef(target);
                    } catch (Exception e) {
                        receiver = created.get(target);
                    }
                    dataLayer.attachContent(receiver, filename, filepath, mimetype, contentType,
                            shouldAppendSuffix);
                }
            }
        } catch (RuntimeException e) {
            /*
             * this exception catching enables to retrieve the faulty id which
             * will be an additional
             * information that will be given to user
             */
            this.faultyId = currentId;
            throw e;
        }
        StringBuffer sb = new StringBuffer();
        createdToXML(sb, created);
        return sb.toString();
    }

    /**
     * Service provider for managing workflows. <br/>
     * Required parameters: "method": an identifier for the service being asked
     * for.<br/>
     * Optional parameters: service-specific. See their code.
     * <p>
     * Services indicated via "method" are either calls to helper functions of
     * ours (prefixed with "wf" e.g. "wfCollectInstanceProperties") or direct
     * calls to WorkflowService functions.
     * </p>
     * 
     * @return the XML-serialized version of the call result.
     */
    @SuppressWarnings("unchecked")
    protected String wfManage() {
        WorkflowService wfs = serviceRegistry.getWorkflowService();
        String result = "";
        XStream xstream = getXStream();

        String method = parameters.get("method");

        if (method != null) {
            logger.debug("Workflow worker: received request for method " + method);
        }
        if (StringUtils.equals(method, "wfStart")) {
            // startWf : need @wfName and @attributes. Return: the instance id
            String wfName = parameters.get("wfName");
            Map<QName, Serializable> atts = (Map<QName, Serializable>) xstream
                    .fromXML(parameters.get("attributes"));
            WorkflowPath path = wfs.startWorkflow(wfName, atts);
            String instanceId = null;
            if (path != null) {
                WorkflowInstance instance = path.instance;
                instanceId = instance.id;
            }
            result = xstream.toXML(instanceId);
        } else if (StringUtils.equals(method, "wfUpdate")) {
            // updateWf : need @taskId, @attributes, @add, @remove
            Boolean resStatus = wfUpdate(wfs);
            result = xstream.toXML(resStatus);
        } else if (StringUtils.equals(method, "wfEnd")) {
            Boolean resStatus = wfEnd(wfs);
            result = xstream.toXML(resStatus);
        } else if (StringUtils.equals(method, "wfSignal")) {
            // signalWf need @pathId and @transitionId
            String pathId = parameters.get("pathId");
            String transitionId = parameters.get("transitionId");
            result = xstream.toXML(wfs.signal(pathId, transitionId));
        } else if (StringUtils.equals(method, "wfCancel")) {
            // cancelWf need @workflowId
            String workflowId = parameters.get("workflowId");
            result = xstream.toXML(wfs.cancelWorkflow(workflowId));
        } else if (StringUtils.equals(method, "wfDelete")) {
            // deleteWf need @workflowId
            String workflowId = parameters.get("workflowId");
            result = xstream.toXML(wfs.deleteWorkflow(workflowId));
        } else if (StringUtils.equals(method, "wfCollectInstanceProperties")) {
            // we need @workflowId, which is an instance id
            Map<QName, Serializable> properties = wfCollectInstanceProperties(wfs);
            result = xstream.toXML(properties);
        } else if (StringUtils.equals(method, "wfGetIdForProcessDefinition")) {// #1534
            String id = wfGetIdForProcessDefinition(wfs);
            result = xstream.toXML(id);
        } else if (StringUtils.equals(method, "wfGetCurrentTasksIds")) {// #1534
            List<String> resList = wfGetCurrentTasks(wfs);
            result = xstream.toXML(resList);
        } else if (StringUtils.equals(method, "wfGetTaskDefinitionPropertyQNames")) {// #1534
            List<String> res = wfGetTaskDefinitionPropertyQNames(wfs);
            result = xstream.toXML(res);
        } else if (StringUtils.equals(method, "wfGetInstanceHistory")) {// #1069
            result = wfGetInstanceHistory(wfs);
        } else if (StringUtils.equals(method, "getAssignedTasks")) {//
            String authority = parameters.get("arg0");
            String state = parameters.get("arg1");
            result = xstream.toXML(wfs.getAssignedTasks(authority, WorkflowTaskState.valueOf(state)));
        } else {
            // calls a generic method
            // result = serviceMethodCall(wfs, xstream, method);
            Method[] methods = wfs.getClass().getDeclaredMethods();
            Method wfsMethod = null;
            for (Method aMethod : methods) {
                if (StringUtils.equals(method, aMethod.getName())) {
                    wfsMethod = aMethod;
                }
            }
            if (wfsMethod != null) {
                Class<?>[] parameterTypes = wfsMethod.getParameterTypes();
                Object[] methodParameters = new Object[parameterTypes.length];
                int i = 0;
                for (Class<?> parameterType : parameterTypes) {
                    String sparameter = parameters.get("arg" + i);
                    if (parameterType.isInstance("a")) { // String params are not XStream'ed
                        methodParameters[i] = sparameter;
                    } else { // non-String params MUST be XStream'ed
                        methodParameters[i] = (sparameter == null) ? null : xstream.fromXML(sparameter);
                    }
                    i++;
                }
                Object oresult = null;
                try {
                    oresult = wfsMethod.invoke(wfs, methodParameters);
                } catch (IllegalArgumentException e) {
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
                if (oresult != null) {
                    result = xstream.toXML(oresult);
                } else {
                    result = "";
                }
            }
        }
        return result;
    }

    /**
     * Calls a method of a target object with the given parameters.
     * 
     * @param target
     *            the service interface that is to be called
     * @param method
     * @return
     */
    private String serviceMethodCall(Object target, String method, List<Object> methodParams) {
        String result = "";
        XStream xstream = getXStream();
        Method[] methods = target.getClass().getDeclaredMethods();
        Method wfsMethod = null;
        for (Method aMethod : methods) {
            if (StringUtils.equals(method, aMethod.getName())) {
                wfsMethod = aMethod;
            }
        }
        if (wfsMethod != null) {
            Object[] methodParameters = methodParams.toArray();

            Object oresult = null;
            try {
                oresult = wfsMethod.invoke(target, methodParameters);
            } catch (Exception e) {
                return "";
            }
            if (oresult != null) {
                result = xstream.toXML(oresult);
            } else {
                result = "";
            }
        }
        return result;
    }

    /**
     * Ends a task by following a transition.
     * <p/>
     * Parameters: <br/>
     * "taskId": the task id <br/>
     * "properties": the id of the transition to trigger
     * 
     * @param wfs
     * @return true if the task has been ended without problems, false
     *         otherwise.
     */
    @SuppressWarnings("unchecked")
    private boolean wfUpdate(WorkflowService wfs) {
        String taskId = parameters.get("taskId");
        String properties = parameters.get("properties");
        Map<QName, Serializable> props = (Map<QName, Serializable>) xstream.fromXML(properties);

        WorkflowTask res = null;
        boolean resStatus = true;
        try {
            res = wfs.updateTask(taskId, props, null, null);
        } catch (Exception e) {
            e.printStackTrace();
            resStatus = false;
        }
        return (res == null ? false : resStatus);
    }

    /**
     * Ends a task by following a transition.
     * <p/>
     * Parameters: <br/>
     * "taskId": the task id <br/>
     * "transitionId": the id of the transition to trigger
     * 
     * @param wfs
     * @return true if the task has been ended without problems, false
     *         otherwise.
     */
    private boolean wfEnd(WorkflowService wfs) {
        String taskId = parameters.get("taskId");
        String transitionId = parameters.get("transitionId");

        boolean resStatus = true;
        WorkflowTask res = null;
        try {
            res = wfs.endTask(taskId, transitionId);
        } catch (Exception e) {
            resStatus = false;
        }
        return (res == null ? false : resStatus);
    }

    /**
     * Collects all non empty BlueXML properties available on completed (and
     * in-progress) tasks of
     * an instanceId.<br/>
     * Parameters: "workflowId": the workflow instance id
     * 
     * @param wfs
     *            the WorkflowService object
     * @return a non null map of relevant properties.
     */
    private Map<QName, Serializable> wfCollectInstanceProperties(WorkflowService wfs) {
        Map<QName, Serializable> properties = new HashMap<QName, Serializable>();
        String workflowId = parameters.get("workflowId");
        // build a query to get all completed tasks for the workflow instance id
        WorkflowTaskQuery queryCompleted = new WorkflowTaskQuery();
        queryCompleted.setProcessId(workflowId);
        queryCompleted.setTaskState(WorkflowTaskState.COMPLETED);
        List<WorkflowTask> tasksComplete = wfs.queryTasks(queryCompleted);
        // Also collect properties from in-progress forms because the workflow may be updated from
        // another interface than our forms, for instance, through Alfresco's web client
        WorkflowTaskQuery queryToDo = new WorkflowTaskQuery();
        queryToDo.setProcessId(workflowId);
        queryToDo.setTaskState(WorkflowTaskState.IN_PROGRESS);
        List<WorkflowTask> tasksToDo = wfs.queryTasks(queryToDo);
        // merge both lists
        List<WorkflowTask> tasks = new ArrayList<WorkflowTask>(tasksToDo.size() + tasksComplete.size());
        tasks.addAll(tasksComplete);
        tasks.addAll(tasksToDo);
        // get the relevant properties
        for (WorkflowTask task : tasks) {
            Set<QName> qnames = task.properties.keySet();
            for (QName qname : qnames) {
                if (qname.getNamespaceURI().startsWith(BLUEXML_WORKFLOW_URI)) {
                    Serializable value = task.properties.get(qname);
                    if (value != null) {
                        properties.put(qname, value);
                    }
                }
            }
        }
        return properties;
    }

    /**
     * Gets some history information id of the latest version of a process
     * definition.<br/>
     * Parameters: "workflowId": the workflow instance id
     * 
     * @param wfs
     *            the WorkflowService object
     * @return a json string with information about the past tasks.
     */
    private String wfGetInstanceHistory(WorkflowService wfs) {
        String workflowId = parameters.get("workflowId");

        // build a query to get all completed tasks for the workflow instance id
        WorkflowTaskQuery queryCompleted = new WorkflowTaskQuery();
        queryCompleted.setProcessId(workflowId);
        queryCompleted.setTaskState(WorkflowTaskState.COMPLETED);
        List<WorkflowTask> tasksComplete = wfs.queryTasks(queryCompleted);

        StringBuffer result = new StringBuffer("tasks:[");
        boolean first = true;
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
        for (WorkflowTask task : tasksComplete) {
            if (!first) {
                result.append(',');
            }
            Map<QName, Serializable> taskProps = task.properties;
            StringBuffer taskBuffer = new StringBuffer("");

            // add whatever properties are necessary
            String name = (String) taskProps.get(WorkflowModel.TYPE_TASK);
            String id = (String) taskProps.get(WorkflowModel.PROP_TASK_ID);
            Serializable startDate = taskProps.get(WorkflowModel.PROP_START_DATE);
            Serializable endDate = taskProps.get(WorkflowModel.PROP_COMPLETION_DATE);

            taskBuffer.append("task:{");
            taskBuffer.append("name:\"").append(name).append("\"");
            taskBuffer.append(",id:\"").append(id).append("\"");
            taskBuffer.append(",startDate:\"").append(formatter.format(startDate)).append("\"");
            taskBuffer.append(",endDate:\"").append(formatter.format(endDate)).append("\"");
            taskBuffer.append("}");

            result.append(taskBuffer);
            first = false;
        }
        result.append(']');
        return result.toString();
    }

    /**
     * Gets the id of the latest version of a process definition.<br/>
     * Parameters: "processName": the name of the workflow as per the definition
     * 
     * @param wfs
     *            the WorkflowService object
     * @return the id, or <code>null</code> if an exception occurred.
     */
    private String wfGetIdForProcessDefinition(WorkflowService wfs) {
        String workflowName = parameters.get("processName");

        logger.debug("Asking Alfresco for the id of process '" + workflowName + "'");
        WorkflowDefinition def = wfs.getDefinitionByName(workflowName);
        String result = (def != null) ? def.id : null;
        if (result == null) {
            List<WorkflowDefinition> defList = wfs.getAllDefinitions();
            logger.debug("No process definition id returned by Alfresco. Listing definitions:");
            for (WorkflowDefinition defItem : defList) {
                logger.debug("  Name: " + defItem.name + "; id: " + defItem.id + "; version: " + defItem.version);
            }
        }
        logger.debug(">>Returning '" + result + "'");
        return result;
    }

    /**
     * Gets the set of properties qualified names a for a task of a process
     * definition.<br/>
     * Parameters: <br/>
     * "processDefId": the process definition id, e.g. "jbpm$105"<br/>
     * "taskId": the task id in the process definition, e.g.
     * "wfbxDigitizationProcess:Debut"<br/>
     * 
     * @param wfs
     *            the WorkflowService object
     * @return the id, or <code>null</code> if an exception occurred.
     */
    private List<String> wfGetTaskDefinitionPropertyQNames(WorkflowService wfs) {
        String processDefId = parameters.get("processId");
        String taskName = parameters.get("taskId");

        // get all definitions
        List<WorkflowTaskDefinition> taskDefs = wfs.getTaskDefinitions(processDefId);
        if (taskDefs == null) {
            return null;
        }

        // find the task
        WorkflowTaskDefinition taskDef = null;
        for (WorkflowTaskDefinition aTaskDef : taskDefs) {
            if (StringUtils.equals(aTaskDef.id, taskName)) {
                taskDef = aTaskDef;
                break;
            }
        }
        if (taskDef == null) {
            return null;
        }

        // return prop qnames
        Map<QName, PropertyDefinition> propDefs = taskDef.metadata.getProperties();

        List<String> qnameList = new ArrayList<String>(propDefs.size());
        for (QName propQName : propDefs.keySet()) {
            qnameList.add(propQName.toString());
        }
        return qnameList;
    }

    /**
     * Gets selected pieces of information about the tasks whose state is
     * "in-progress" for a
     * workflow instance. For each task, the pieces are concatenated in a string
     * (which ends up in
     * the list) and separated by a SEPARATOR substring.<br/>
     * Current pieces of info returned (<b>IN THAT ORDER</b>): id, name, title.<br/>
     * (e.g. with SEPARATOR="{::}":
     * 
     * "jbpm$64{::}wfbxDigitizationProcess:Debut{::}Demarrage de la dematerialisation"
     * )
     * <p/>
     * Parameters: "workflowId": the workflow instance id
     * 
     * @param wfs
     *            the WorkflowService object
     * @return the list, in which each item is formatted.
     */
    private List<String> wfGetCurrentTasks(WorkflowService wfs) {
        String workflowId = parameters.get("workflowId");
        List<WorkflowPath> paths = wfs.getWorkflowPaths(workflowId);

        List<String> result = new Vector<String>();
        // we need to probe all active paths
        for (WorkflowPath path : paths) {
            if (path.active) {
                // get the tasks for the path
                List<WorkflowTask> tasks = wfs.getTasksForWorkflowPath(path.id);
                if (tasks == null) {
                    return result;
                }
                // add the tasks to complete
                for (WorkflowTask task : tasks) {
                    if (task.state == WorkflowTaskState.IN_PROGRESS) {
                        result.add(task.id + WEBSCRIPT_SEPARATOR + task.name + WEBSCRIPT_SEPARATOR + task.title);
                    }
                }
            }
        }
        return result;
    }

    /**
     * Provides access to either the Alfresco service registry's services or
     * Alfresco's beans.
     * <p/>
     * Parameters: "serviceName", "methodName", and "methodParams" (list of
     * parameters specific to the method being called).
     * <p/>
     * Currently supported services/beans (<b>CASE SENSITIVE</b>): NodeService,
     * DictionaryService, PersonService, AuthorityDAO. <br/>
     * See the service-specific call functions for supported methods (which are
     * also <b>CASE SENSITIVE</b>). Parameters to these functions must be of the
     * right types and the list of parameters <b>MUST</b> have been xstreamed to
     * XML at call time since it will be xstreamed from XML here.
     * 
     * @return the result of the call to the service's method, xstreamed to XML
     *         (even if the result
     *         is a String), or null if an exception occurred.
     */
    @SuppressWarnings("unchecked")
    protected String service() {
        String serviceName = parameters.get("serviceName");
        String methodName = parameters.get("methodName");
        String paramStr = parameters.get("methodParams");
        XStream xstream = getXStream();
        List<Object> methodParams = (List<Object>) xstream.fromXML(paramStr);
        Object result = null;
        try {
            if (StringUtils.equals(serviceName, "NodeService")) {
                result = callNodeService(methodName, methodParams);
            } else if (StringUtils.equals(serviceName, "DictionaryService")) {
                result = callDictionaryService(methodName, methodParams);
            } else if (StringUtils.equals(serviceName, "PersonService")) {
                result = callPersonService(methodName, methodParams);
            } else if (StringUtils.equals(serviceName, "AuthorityDAO")) {
                result = callAuthorityDAO(methodName, methodParams);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        String resultStr = xstream.toXML(result);
        return resultStr;
    }

    /**
     * Executes and returns the result of a AuthorityDAO method. <br/>
     * Supported methods: getAuthorityNodeRefOrNull, getAllAuthorities,
     * getContainingAuthorities.
     * 
     * @param methodName
     * @param methodParams
     * @return
     * @throws SecurityException
     */
    private Object callAuthorityDAO(String methodName, List<Object> methodParams) throws SecurityException {
        AuthorityDAO authorityDAO = formsWebscript.getAuthorityDAO();
        Object[] paramArray = methodParams.toArray();
        if (StringUtils.equals(methodName, "getAuthorityNodeRefOrNull")) {
            String authorityName = (String) paramArray[0];
            return authorityDAO.getAuthorityNodeRefOrNull(authorityName);
        }
        if (StringUtils.equals(methodName, "getAllAuthorities")) {
            AuthorityType authorityType = (AuthorityType) paramArray[0];
            return authorityDAO.getAllAuthorities(authorityType);
        }
        if (StringUtils.equals(methodName, "getContainingAuthorities")) {
            AuthorityType authorityType = (AuthorityType) paramArray[0];
            String name = (String) paramArray[1];
            Boolean immediate = (Boolean) paramArray[2];
            return authorityDAO.getContainingAuthorities(authorityType, name, immediate);
        }
        return null;
    }

    /**
     * Executes and returns the result of a DictionaryService method. Supported
     * methods: getType.
     * 
     * @param methodName
     * @param methodParams
     * @return
     * @throws SecurityException
     * @throws NoSuchMethodException
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    private Object callDictionaryService(String methodName, List<Object> methodParams) throws SecurityException,
            NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        DictionaryService theService = serviceRegistry.getDictionaryService();
        Class<?> theClass = theService.getClass();
        Object[] paramArray = methodParams.toArray();
        if (StringUtils.equals(methodName, "getType")) {
            Method method = theClass.getMethod(methodName, new Class[] { QName.class });
            QName theType = (QName) paramArray[0];
            return method.invoke(theService, new Object[] { theType });
        }
        return null;
    }

    /**
     * Supported methods: getProperty, getPath, getType.
     * 
     * @param methodName
     * @param methodParams
     * @return
     * @throws NoSuchMethodException
     * @throws SecurityException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws IllegalArgumentException
     */
    private Object callNodeService(String methodName, List<Object> methodParams) throws SecurityException,
            NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        NodeService theService = serviceRegistry.getNodeService();
        Class<?> theClass = theService.getClass();
        Object[] paramArray = methodParams.toArray();
        if (StringUtils.equals(methodName, "getProperty")) {
            Method method = theClass.getMethod(methodName, new Class[] { NodeRef.class, QName.class });
            NodeRef ref = (NodeRef) paramArray[0];
            QName qname = (QName) paramArray[1];
            return method.invoke(theService, new Object[] { ref, qname });
        } else if (StringUtils.equals(methodName, "getPath")) {
            Method method = theClass.getMethod(methodName, new Class[] { NodeRef.class });
            NodeRef ref = (NodeRef) paramArray[0];
            return method.invoke(theService, new Object[] { ref });
        } else if (StringUtils.equals(methodName, "getType")) {
            Method method = theClass.getMethod(methodName, new Class[] { NodeRef.class });
            NodeRef ref = (NodeRef) paramArray[0];
            return method.invoke(theService, new Object[] { ref });
        }
        return null;
    }

    /**
     * Calls a PersonService method.
     * 
     * @param methodName
     *            the method to call, case-sensitive.
     * @param methodParams
     *            the parameters to pass to the method.
     * @return the result of the call.
     */
    private Object callPersonService(String methodName, List<Object> methodParams) {
        return serviceMethodCall(serviceRegistry.getPersonService(), methodName, methodParams);
    }

    /**
     * Ensures a path exists.<br/>
     * Parameters: "path" (e.g. /app:company_home/app:dictionary/cm:BXDSDATA)
     * 
     * @return a node ref to the deepest folder (e.g. "cm:BXDSDATA")
     */
    protected String createPath() {
        NodeRef result;
        String path = parameters.get("path");
        try {
            result = dataLayer.createPath(path);
        } catch (Exception e) {
            logger.error("Error while creating path.", e);
            return "";
        }
        return result.toString();
    }

    /**
     * Adds a content to a folder that already has the aspect
     * "workflow package".<br/>
     * If a valid package node was provided at call time, that same package is
     * returned. Otherwise
     * (i.e. if the package is <code>null</code>), a new package is created.<br/>
     * Parameters: "content", "package"
     * 
     * @return the noderef (as a serialized XML string) of the package, or
     *         <code>null</code> if the
     *         content is not effectively associated with the package when the
     *         job to be done is
     *         over.
     */
    protected String addInPackage() {
        String nodeStr = parameters.get("content");
        String packageStr = parameters.get("package");

        NodeRef wkPackage;
        if (packageStr == null) { // it's up to us to create the package
            wkPackage = serviceRegistry.getWorkflowService().createPackage(null);
        } else { // one was provided
            wkPackage = new NodeRef(packageStr);
        }
        String resultXStreamedPackage = xstream.toXML(wkPackage);

        if (nodeStr == null) { // #1284: workflows may have no attached data
            return resultXStreamedPackage;
        }
        NodeService nodeService = serviceRegistry.getNodeService();
        NodeRef noderef = new NodeRef(nodeStr);
        ChildAssociationRef childAssoc = nodeService.getPrimaryParent(noderef);
        nodeService.addChild(wkPackage, noderef, ContentModel.ASSOC_CONTAINS, childAssoc.getQName());

        // check that the association is effective
        List<ChildAssociationRef> assocs = nodeService.getChildAssocs(wkPackage);
        for (ChildAssociationRef asso : assocs) {
            if (asso.getChildRef().equals(noderef)) {
                return resultXStreamedPackage;
            }
        }
        return null;
    }

    /**
     * Uploads an existing file to the repository at a specific location.
     * Parameters: "filename"
     * (name+extension), filepath (complete filesystem path, including filename
     * and extension),
     * "location" (path in the repository), "mimetype", "suffixAppend" (whether
     * existing filenames
     * should be appended with a number if not available, e.g. 'filename.ext'
     * becomes 'filename
     * (1).ext'. DEFAULTS to true)
     * <p/>
     * The file system path must be a valid path (not an escaped URL)<br/>
     * .
     * 
     * @return the node ref (protocol, store and id) to the newly created node,
     *         or empty string if
     *         any errors occur (e.g. the parent folder does not exist).
     */
    protected String upload() {
        NodeRef parent, newNode = null;
        String FAILURE = "";

        String filename = parameters.get("filename");
        String filepath = parameters.get("filepath");
        String location = parameters.get("location");
        String mimeType = parameters.get("mimetype");
        String suffixAppend = parameters.get("suffixAppend");
        boolean shouldAppendSuffix = !(StringUtils.equals(suffixAppend, "false"));
        // test for the parent folder
        List<NodeRef> results = null;
        try {
            results = dataLayer.request(location);
        } catch (Exception e) {
            logger.error("Error while testing existence of upload location: " + location, e);
            return FAILURE;
        }
        if (results.size() == 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(location + " does not exist: trying to create it.");
            }
            try {
                NodeRef createdPath = dataLayer.createPath(location);
                results = new ArrayList<NodeRef>(1);
                results.add(createdPath);
                if (logger.isDebugEnabled()) {
                    logger.debug("Suceeded in creating upload location '" + location + "'.");
                }
            } catch (Exception e) {
                logger.error("Creation of '" + location + "' failed. Giving up on the upload.", e);
                return FAILURE;
            }
        }
        parent = results.get(0);
        // set node type and other qnames
        QName assocTypeQName = ContentModel.ASSOC_CONTAINS;
        QName assocQName = QName.createQName("{http://www.alfresco.org/model/content/1.0}" + filename);
        QName nodeTypeQName = ContentModel.PROP_CONTENT;
        // create the node
        newNode = serviceRegistry.getNodeService().createNode(parent, assocTypeQName, assocQName, nodeTypeQName)
                .getChildRef();

        return dataLayer.uploadContentToNode(newNode, filename, filepath, mimeType, nodeTypeQName, true,
                shouldAppendSuffix);
    }

    private void createdToXML(StringBuffer sb, Map<String, String> created) {
        exceptionToStringOpenTag("ids", sb);

        Set<Entry<String, String>> entrySet = created.entrySet();
        for (Entry<String, String> entry : entrySet) {
            exceptionToStringOpenTag("id", sb);
            exceptionToStringAddEntry("from", entry.getKey(), sb);
            exceptionToStringAddEntry("to", entry.getValue(), sb);
            exceptionToStringCloseTag("id", sb);
        }

        exceptionToStringCloseTag("ids", sb);
    }

    private void replaceIds(Element element, Map<String, String> created) {
        if (StringUtils.equals(element.getTagName(), "target")
                || StringUtils.equals(element.getTagName(), "associationClass")) {
            String newId = created.get(element.getTextContent());
            if (newId != null) {
                element.setTextContent(newId);
            }
        }
        String elementId = element.getAttribute("id");
        String newId = created.get(elementId);
        if (newId != null) {
            element.setAttribute("id", newId);
        }
        List<Element> children = DOMUtil.getAllChildren(element);
        for (Element child : children) {
            replaceIds(child, created);
        }
    }

    private LuceneSearcher getUnsecureLuceneSearcher() {
        LuceneIndexer indexer = formsWebscript.getIndexerAndSearcherFactory()
                .getIndexer(formsWebscript.getStoreRef());
        LuceneConfig config = formsWebscript.getIndexerAndSearcherFactory();
        ADMLuceneSearcherImpl searcher = ADMLuceneSearcherImpl.getSearcher(formsWebscript.getStoreRef(), indexer,
                config);
        searcher.setTenantService(formsWebscript.getTenantService());
        searcher.setDictionaryService(serviceRegistry.getDictionaryService());
        searcher.setNamespacePrefixResolver(formsWebscript.getNamespacePrefixResolver());
        return searcher;
    }

    /**
     * Builds a search string with a type clause and one clause per search
     * value.
     * 
     * @param type
     * @param searchedValues
     * @param userLuceneQuery
     * @return
     */
    private String getLuceneQuery(String type, List<String> searchedValues, String userLuceneQuery,
            boolean includeSystemProperties) {
        StringBuilder query = new StringBuilder();

        List<QName> subTypes = formsWebscript.getSubTypes(type);
        if (subTypes == null) {
            return null;
        }

        // the searchable attributes
        Set<QName> attributes = getSearchableAttributes(subTypes.get(0), includeSystemProperties);

        if (userLuceneQuery != null) { // #1556
            query.append(userLuceneQuery);
            logger.debug("Got user-specified Lucene query :'" + userLuceneQuery + "'");
        } else {
            // the type clause
            query.append("(");
            for (Iterator<QName> iterator = subTypes.iterator(); iterator.hasNext();) {
                QName name = iterator.next();
                query.append("TYPE:\"");
                query.append(name.toString());
                query.append("\"");
                if (iterator.hasNext()) {
                    query.append(" OR ");
                }
            }
            query.append(")");
            logger.debug("No user-specified query");
        }

        // add one clause per significant search value
        for (String strRequest : searchedValues) {
            StringBuilder clause = getLuceneClauseForSearchValue(attributes, strRequest);
            if (clause.length() > 0) {
                query.append(" AND ");
                query.append(clause);
            }
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Built Lucene query: " + query);
        }

        return query.toString();
    }

    /**
     * Gets a search string fragment in which search criteria match the given
     * type's searchable
     * attributes with the search keyword.
     * 
     * @param attributes
     *            the qnames of searchable attributes
     * @param keyword
     *            the search keyword
     * @return the search string fragment surrounded by parentheses, or empty
     *         string if no
     *         searchable attributes are found.
     */
    private StringBuilder getLuceneClauseForSearchValue(Set<QName> attributes, String keyword) {
        StringBuilder query = new StringBuilder();

        if (StringUtils.trimToNull(keyword) != null) {
            String escapedRequest = QueryParser.escape(keyword);

            Iterator<QName> it = attributes.iterator();

            if (it.hasNext()) { // at least one searchable attribute before returning non-empty
                query.append("(");
                while (it.hasNext()) {
                    String attribute = it.next().toString();
                    appendAttribute(query, attribute);
                    query.append("*").append(escapedRequest).append("*");
                    query.append(" OR ");
                    appendAttribute(query, attribute);
                    query.append("\"").append(escapedRequest).append("\"");
                    if (it.hasNext()) {
                        query.append(" OR ");
                    }
                }
                query.append(")");
            }
        }
        return query;
    }

    private void appendAttribute(StringBuilder query, String attribute) {
        query.append("@");
        query.append(attribute.replace(":", "\\:").replace("}", "\\}").replace("{", "\\{"));
        query.append(":");
    }

    /**
     * Gets the set of searchable attributes for the given type. If the set does
     * not exist, it will
     * be computed and registered in a cache.
     * 
     * @param type
     * @return the set
     */
    private synchronized Set<QName> getSearchableAttributes(QName type, boolean includeSystemProps) {
        Set<QName> result = searchableAttributesCache.get(type);
        if (result == null) {
            result = computeSearchableAttributes(type, includeSystemProps);
            searchableAttributesCache.put(type, result);
        }
        return result;
    }

    /**
     * Collects all searchable properties for a given type, whether they a
     * defined in the type or in
     * a related aspects or in a parent type or in one the parent's aspects.
     * 
     * @param pCurrentType
     *            the type to start from. It is the bottom-most type in the
     *            hierarchy of collected
     *            types.
     * @return the set of searchable attributes
     */
    private Set<QName> computeSearchableAttributes(QName pCurrentType, boolean includeSystemProps) {
        QName currentType = pCurrentType;
        List<QName> classTypes = new ArrayList<QName>();

        // we collect the hierarchy of parent classes
        while (currentType != null
                && (currentType.getNamespaceURI().startsWith(BLUEXML_MODEL_URI) || includeSystemProps)) {
            classTypes.add(currentType);
            TypeDefinition nodeRefTypeDefinition = serviceRegistry.getDictionaryService().getType(currentType);
            QName parentType = nodeRefTypeDefinition.getParentName();
            currentType = parentType;
        }

        // the list of searchable properties from all types in the hierarchy
        Map<QName, PropertyDefinition> typeProperties = new HashMap<QName, PropertyDefinition>();

        // we look into the definition of each collected type for the searchable properties
        for (QName type : classTypes) {
            TypeDefinition typeDefinition = serviceRegistry.getDictionaryService().getType(type);
            addSearchableAttributes(typeProperties, typeDefinition.getProperties(), includeSystemProps);
            for (AspectDefinition ad : typeDefinition.getDefaultAspects()) {
                addSearchableAttributes(typeProperties, ad.getProperties(), includeSystemProps);
            }
        }
        return typeProperties.keySet();
    }

    /**
     * Filters the definition properties and adds the seachable ones to the
     * selection.
     * 
     * @param typeProperties
     *            the selection (of searchable properties) that is being built
     * @param properties
     *            the properties from the type dictionary definition
     */
    private void addSearchableAttributes(Map<QName, PropertyDefinition> typeProperties,
            Map<QName, PropertyDefinition> properties, boolean includeSystemProperties) {
        Set<Entry<QName, PropertyDefinition>> propertiesEntrySet = properties.entrySet();
        for (Entry<QName, PropertyDefinition> entry : propertiesEntrySet) {
            PropertyDefinition value = entry.getValue();
            QName key = entry.getKey();
            if (value.isIndexed()
                    && (key.getNamespaceURI().startsWith(BLUEXML_MODEL_URI) || includeSystemProperties)) {
                typeProperties.put(key, value);
            }
        }
    }

    // %% changed visibility to private
    private SearchParameters createSearchParameters(String type, List<String> searchedValues, int maxResults,
            String userLuceneQuery, boolean includeSystemProperties) {
        String luceneQuery = getLuceneQuery(type, searchedValues, userLuceneQuery, includeSystemProperties);
        return createSearchParameters(luceneQuery, maxResults);
    }

    private SearchParameters createSearchParameters(String query, int maxResults) {
        if (query == null) {
            return null;
        }
        // setup search parameters, including limiting the results
        SearchParameters searchParams = new SearchParameters();
        searchParams.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
        searchParams.setLanguage(SearchService.LANGUAGE_LUCENE);
        searchParams.setQuery(query);
        if (maxResults > 0) {
            searchParams.setLimit(maxResults);
            searchParams.setLimitBy(LimitBy.FINAL_SIZE);
        }
        return searchParams;
    }

}