David Caruana 575c970565 Merging BRANCHES/DEV/CMIS_10 to HEAD:
17717: This check-in contains changes in Java and .NET TCK tests related to CMIS-43  and CMIS-44 JIRA tasks. Also some bugs were faced out and fixed in 
   17727: CMIS-69: Alfresco to CMIS ACL mapping: Part 1: API
   17732: Merge HEAD to DEV/CMIS10
   17756: MOB-563: SQL Tests - Lexer
   17764: CMIS-69: Alfresco to CMIS ACL mapping: get ACL support
   17802: More for CMIS-69: Alfresco to CMIS ACL mapping. Implementation for applyAcl.
   17830: Fixes for CMIS lexer and parser tests
   17838: Access fix ups for access by the WS/Rest layers
   17869: 1) remote-api:
   17874: SAIL-146: Alfresco to CMIS ACL mapping: Support to group ACEs by principal id
   17883: Adjust version properties for dev/cmis10 branch.
   17885: Update OASIS CMIS TC status.
   17889: Fix issue where objectid is not rendered correctly for CMIS private working copies.
   17890: SAIL-146: Alfresco to CMIS ACL mapping: Fixes for ACL merging when reporting and ordering of ACEs. Report full permissions and not unique short names.
   17902: Fix issue where CMIS queries via GET used incorrect defaults for paging.
   17909: Fix CMIS link relations for folder tree.
   17912: Fix CMIS type descendants atompub link
   17922: Update AtomPub binding to CMIS 1.0 CD05 XSDs.
   17924: SAIL-146: Alfresco to CMIS ACL mapping: Test set using full permissions (as opposed to short unique names)
   17927: Fix content stream create/update status to comply with CMIS 1.0 CD05.
   17934: Resolve encoding issues in CMIS AtomPub binding.
   17973: SAIL-171: CMIS Renditions REST binding
   17975: SAIL-146: Alfresco to CMIS ACL mapping: Completed AllowedAction and Permissions mapping. Added missing canDeleteTree.
   17990: Update CMIS AtomPub to CD06
   17996: Updates for cmis.alfresco.com for CD06 in prep for public review 2.
   18007: WS-Bindings were updated with CMIS 1.0 cd06 changes.
   18016: CMIS web services: Add missing generated files from WSDL
   18018: CMIS index page updates for cmis.alfresco.com
   18041: Merged HEAD to DEV/CMIS_10
   18059: SAIL-227:
   18067: SAIL-157: Strict vs Non-Strict Query Language: Enforce restrictions on the use of SCORE() and CONTAINS()
   18080: Fix for SAIL-213:Bug: Query engine does not check that select list properties are valid for selectors
   18131: SAIL-156: Query Language Compliance: Fix support for LIKE, including escaping of '%' and '_' with '\'.
   18132: SAIL-156: Query Language Compliance: Fix support for LIKE, including escaping of '%' and '_' with '\': Fix underlying lucene impl for prefix and fuzzy queries to match wildcard/like
   18143: SAIL-156: Query Language Compliance: Fix and check qualifiers in IN_TREE and IN_FOLDER. Improved scoring for CONTAINS()
   18173: SAIL-245: Exclude thumbnails from normal query results
   18179: SAIL 214: Query Language Compliance: Check for valid object ids in IN_FOLDER and IN_TREE
   18210: SAIL-156:  Query Language Compliance: Support for simple column aliases in predicates/function arguments/embedded FTS. Check property/selector binding in embedded FTS.
   18211: SAIL-156:  Query Language Compliance: Support for simple column aliases in predicates/function arguments/embedded FTS. Check property/selector binding in embedded FTS.
   18215: SAIL 156: Query Language Compliance: Fix CMIS type info to reflect the underlying settings of the Alfresco type for includeInSuperTypeQuery
   18244: SAIL 156: Query Language Compliance: includeInSuperTypeQuery -> includedInSuperTypeQuery: First cut of cmis query test model. Fixed modelSchema.xml to validate
   18255: SAIL 156: Query Language Compliance: First set of tests for predicates using properties mapped to CMIS Strings.
   18261: CMIS-49 SAIL-163: Alfresco to CMIS Change Log mapping - New CMIS Audit mapping is implemented. ChangeLogDataExtractor was added.
   18263: Build Fix
   18285: SAIL 156: Query Language Compliance: Restrictions on predicates that may be used by single-valued and multi-valued properties
   18287: SAIL-186: Changes to make CMIS Rendition REST bindings pass new TCK tests
   18291: Fix Eclipse classpath problems
   18323: CMIS-44 SAIL-187: Change Log tests (WS) – Java and .NET tests for change log were implemented.
   18325: SAIL 156: Query Language Compliance: Fixes and tests for d:mltext mappings
   18329: Updated Chemistry TCK jar including Dave W's rendition tests.
   18333: Fix compile error - spurious imports.
   18334: Fix issue where absurl web script method failed when deployed to root context.
   18339: Update CMIS index page for start of public review 2.
   18387: SAIL-147: CMIS ACL REST bindings + framework fixes
   18392: Fix typo
   18394: SAIL 156: Query Language Compliance: Fixes and tests for d:<numeric>
   18406: SAIL 156: Query Language Compliance: Remaining type/predicate combinations. Restriction of In/Comparisons for ID/Boolean
   18408: CMIS Query language - remove (pointless) multi-valued column from language definition
   18409: Formatting change for CMIS.g
   18410: Formatting change for FTS.g
   18411: CMIS TCK tests were updated to CMIS 1.0 cd06 schemas.
   18412: SAIL 156: Query Language Compliance: Tests and fixes for aliases for all data types in simple predicates (they behave as the direct column reference)
   18417: Update Chemistry TCK which now incorporates Dave W's ACL tests.
   18419: Update CMIS index page to include public review end date.
   18427: SAIL 156: Query Language Compliance: Expose multi-valued properties in queries. Tests for all accessors. Fix content length to be long.
   18435: SAIL 156: Query Language Compliance: Use queryable correctly and fix up model mappings. Add tests for baseTypeId, contentStreamId and path.
   18472: SAIL 156: Query Language Compliance: Tests and fixes for FTS/Contains expressions. Adhere strictly to the spec - no extensions available by default. Improved FTS error reporting (and stop any recovery).
   18477: SAIL-164: CMIS change log REST bindings
   18495: SAIL 156: Query Language Compliance: Tests and fixes for escaping in string literals, LIKE and FTS expressions.
   18537: SAIL 156: Query Language Compliance: Sorting support. Basic sort test for all orderable/indexed CMIS properties.
   18538: SAIL-164: CMIS change log fixes for TCK compliance
   18547: SAIL 156: Query Language Compliance: Ordering tests for all datatypes, including null values. 
   18582: Incorporate latest Chemistry TCK
   18583: Update list of supported CMIS capabilities in index page.
   18606: SAIL-156, SAIL-157, SAIL-158: Query Language Compliance: Respect all query options including locale. Fixes and tests for MLText cross language support.
   18608: SAIL-159: Java / Javascript API access to CMIS Query Language
   18617: SAIL-158: Query Tests: Check policy and relationship types are not queryable.
   18636: SAIL-184: ACL tests (WS) 
   18663: ACL tests were updated in accordance with last requirements by David Caruana.
   18680: Update to CMIS CD07
   18681: Fix CMIS ContentStreamId property when document has no content.
   18700: CMIS: Head merge problem resolution.

Phase 1: Merge up to and including revision 18700, as this the point where both AtomPub and Web Services TCK tests succeed completely on dev branch.

Note: includes CMIS rendition support ready for integration and testing with DM renditions.

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@18790 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
2010-02-23 17:23:42 +00:00

573 lines
22 KiB
Java

/*
* Copyright (C) 2005-2007 Alfresco Software Limited.
*
* 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 2
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* As a special exception to the terms and conditions of version 2.0 of
* the GPL, you may redistribute this Program in connection with Free/Libre
* and Open Source Software ("FLOSS") applications as described in Alfresco's
* FLOSS exception. You should have recieved a copy of the text describing
* the FLOSS exception, and it is also available here:
* http://www.alfresco.com/legal/licensing"
*/
package org.alfresco.repo.jscript;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.processor.ProcessorExtension;
import org.alfresco.repo.processor.BaseProcessor;
import org.alfresco.scripts.ScriptException;
import org.alfresco.scripts.ScriptResourceHelper;
import org.alfresco.scripts.ScriptResourceLoader;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.ScriptLocation;
import org.alfresco.service.cmr.repository.ScriptProcessor;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.WrapFactory;
import org.mozilla.javascript.WrappedException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.FileCopyUtils;
/**
* Implementation of the ScriptProcessor using the Rhino JavaScript library.
*
* @author Kevin Roast
*/
public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcessor, ScriptResourceLoader, InitializingBean
{
private static final Log logger = LogFactory.getLog(RhinoScriptProcessor.class);
private static final String PATH_CLASSPATH = "classpath:";
/** Wrap Factory */
private static final WrapFactory wrapFactory = new RhinoWrapFactory();
/** Base Value Converter */
private final ValueConverter valueConverter = new ValueConverter();
/** Store into which to resolve cm:name based script paths */
private StoreRef storeRef;
/** Store root path to resolve cm:name based scripts path from */
private String storePath;
/** Pre initialized secure scope object. */
private Scriptable secureScope;
/** Pre initialized non secure scope object. */
private Scriptable nonSecureScope;
/** Cache of runtime compiled script instances */
private final Map<String, Script> scriptCache = new ConcurrentHashMap<String, Script>(256);
/**
* Set the default store reference
*
* @param storeRef The default store reference
*/
public void setStoreUrl(String storeRef)
{
this.storeRef = new StoreRef(storeRef);
}
/**
* @param storePath The store path to set.
*/
public void setStorePath(String storePath)
{
this.storePath = storePath;
}
/**
* @see org.alfresco.service.cmr.repository.ScriptProcessor#reset()
*/
public void reset()
{
this.scriptCache.clear();
}
/**
* @see org.alfresco.service.cmr.repository.ScriptProcessor#execute(org.alfresco.service.cmr.repository.ScriptLocation, java.util.Map)
*/
public Object execute(ScriptLocation location, Map<String, Object> model)
{
try
{
// test the cache for a pre-compiled script matching our path
Script script = null;
String path = location.getPath();
if (location.isCachable())
{
script = this.scriptCache.get(path);
}
if (script == null)
{
if (logger.isDebugEnabled())
logger.debug("Resolving and compiling script path: " + path);
// retrieve script content and resolve imports
ByteArrayOutputStream os = new ByteArrayOutputStream();
FileCopyUtils.copy(location.getInputStream(), os); // both streams are closed
byte[] bytes = os.toByteArray();
String source = new String(bytes, "UTF-8");
source = resolveScriptImports(new String(bytes));
// compile the script and cache the result
Context cx = Context.enter();
try
{
script = cx.compileString(source, path, 1, null);
// We do not worry about more than one user thread compiling the same script.
// If more than one request thread compiles the same script and adds it to the
// cache that does not matter - the results will be the same. Therefore we
// rely on the ConcurrentHashMap impl to deal both with ensuring the safety of the
// underlying structure with asynchronous get/put operations and for fast
// multi-threaded access to the common cache.
if (location.isCachable())
{
this.scriptCache.put(path, script);
}
}
finally
{
Context.exit();
}
}
return executeScriptImpl(script, model, location.isSecure());
}
catch (Throwable err)
{
throw new ScriptException("Failed to execute script '" + location.toString() + "': " + err.getMessage(), err);
}
}
/**
* @see org.alfresco.service.cmr.repository.ScriptProcessor#execute(java.lang.String, java.util.Map)
*/
public Object execute(String location, Map<String, Object> model)
{
return execute(new ClasspathScriptLocation(location), model);
}
/**
* @see org.alfresco.service.cmr.repository.ScriptProcessor#execute(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.util.Map)
*/
public Object execute(NodeRef nodeRef, QName contentProp, Map<String, Object> model)
{
try
{
if (this.services.getNodeService().exists(nodeRef) == false)
{
throw new AlfrescoRuntimeException("Script Node does not exist: " + nodeRef);
}
if (contentProp == null)
{
contentProp = ContentModel.PROP_CONTENT;
}
ContentReader cr = this.services.getContentService().getReader(nodeRef, contentProp);
if (cr == null || cr.exists() == false)
{
throw new AlfrescoRuntimeException("Script Node content not found: " + nodeRef);
}
// compile the script based on the node content
Script script;
Context cx = Context.enter();
try
{
script = cx.compileString(resolveScriptImports(cr.getContentString()), nodeRef.toString(), 1, null);
}
finally
{
Context.exit();
}
return executeScriptImpl(script, model, false);
}
catch (Throwable err)
{
throw new ScriptException("Failed to execute script '" + nodeRef.toString() + "': " + err.getMessage(), err);
}
}
/**
* @see org.alfresco.service.cmr.repository.ScriptProcessor#executeString(java.lang.String, java.util.Map)
*/
public Object executeString(String source, Map<String, Object> model)
{
try
{
// compile the script based on the node content
Script script;
Context cx = Context.enter();
try
{
script = cx.compileString(resolveScriptImports(source), "AlfrescoJS", 1, null);
}
finally
{
Context.exit();
}
return executeScriptImpl(script, model, true);
}
catch (Throwable err)
{
throw new ScriptException("Failed to execute supplied script: " + err.getMessage(), err);
}
}
/**
* Resolve the imports in the specified script. Supported include directives are of the following form:
* <pre>
* <import resource="classpath:alfresco/includeme.js">
* <import resource="workspace://SpacesStore/6f73de1b-d3b4-11db-80cb-112e6c2ea048">
* <import resource="/Company Home/Data Dictionary/Scripts/includeme.js">
* </pre>
* Either a classpath resource, NodeRef or cm:name path based script can be includes. Multiple includes
* of the same script are dealt with correctly and nested includes of scripts is fully supported.
* <p>
* Note that for performance reasons the script import directive syntax and placement in the file
* is very strict. The import lines <i>must</i> always be first in the file - even before any comments.
* Immediately that the script service detects a non-import line it will assume the rest of the
* file is executable script and no longer attempt to search for any further import directives. Therefore
* all imports should be at the top of the script, one following the other, in the correct syntax and with
* no comments present - the only separators valid between import directives is white space.
*
* @param script The script content to resolve imports in
*
* @return a valid script with all nested includes resolved into a single script instance
*/
private String resolveScriptImports(String script)
{
return ScriptResourceHelper.resolveScriptImports(script, this, logger);
}
/**
* Load a script content from the specific resource path.
*
* @param resource Resources can be of the form:
* <pre>
* classpath:alfresco/includeme.js
* workspace://SpacesStore/6f73de1b-d3b4-11db-80cb-112e6c2ea048
* /Company Home/Data Dictionary/Scripts/includeme.js
* </pre>
*
* @return the content from the resource, null if not recognised format
*
* @throws AlfrescoRuntimeException on any IO or ContentIO error
*/
public String loadScriptResource(String resource)
{
String result = null;
if (resource.startsWith(PATH_CLASSPATH))
{
try
{
// load from classpath
String scriptClasspath = resource.substring(PATH_CLASSPATH.length());
InputStream stream = getClass().getClassLoader().getResource(scriptClasspath).openStream();
if (stream == null)
{
throw new AlfrescoRuntimeException("Unable to load included script classpath resource: " + resource);
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
FileCopyUtils.copy(stream, os); // both streams are closed
byte[] bytes = os.toByteArray();
// create the string from the byte[] using encoding if necessary
result = new String(bytes, "UTF-8");
}
catch (IOException err)
{
throw new AlfrescoRuntimeException("Unable to load included script classpath resource: " + resource);
}
}
else
{
NodeRef scriptRef;
if (resource.startsWith("/"))
{
// resolve from default SpacesStore as cm:name based path
// TODO: remove this once FFS correctly allows name path resolving from store root!
NodeRef rootNodeRef = this.services.getNodeService().getRootNode(this.storeRef);
List<NodeRef> nodes = this.services.getSearchService().selectNodes(
rootNodeRef, this.storePath, null, this.services.getNamespaceService(), false);
if (nodes.size() == 0)
{
throw new AlfrescoRuntimeException("Unable to find store path: " + this.storePath);
}
StringTokenizer tokenizer = new StringTokenizer(resource, "/");
List<String> elements = new ArrayList<String>(6);
if (tokenizer.hasMoreTokens())
{
tokenizer.nextToken();
}
while (tokenizer.hasMoreTokens())
{
elements.add(tokenizer.nextToken());
}
try
{
FileInfo fileInfo = this.services.getFileFolderService().resolveNamePath(nodes.get(0), elements);
scriptRef = fileInfo.getNodeRef();
}
catch (FileNotFoundException err)
{
throw new AlfrescoRuntimeException("Unable to load included script repository resource: " + resource);
}
}
else
{
scriptRef = new NodeRef(resource);
}
// load from NodeRef default content property
try
{
ContentReader cr = this.services.getContentService().getReader(scriptRef, ContentModel.PROP_CONTENT);
if (cr == null || cr.exists() == false)
{
throw new AlfrescoRuntimeException("Included Script Node content not found: " + resource);
}
result = cr.getContentString();
}
catch (ContentIOException err)
{
throw new AlfrescoRuntimeException("Unable to load included script repository resource: " + resource);
}
}
return result;
}
/**
* Execute the supplied script content. Adds the default data model and custom configured root
* objects into the root scope for access by the script.
*
* @param script The script to execute.
* @param model Data model containing objects to be added to the root scope.
* @param secure True if the script is considered secure and may access java.* libs directly
*
* @return result of the script execution, can be null.
*
* @throws AlfrescoRuntimeException
*/
private Object executeScriptImpl(Script script, Map<String, Object> model, boolean secure)
throws AlfrescoRuntimeException
{
long startTime = 0;
if (logger.isDebugEnabled())
{
startTime = System.nanoTime();
}
// Convert the model
model = convertToRhinoModel(model);
Context cx = Context.enter();
try
{
// Create a thread-specific scope from one of the shared scopes.
// See http://www.mozilla.org/rhino/scopes.html
cx.setWrapFactory(wrapFactory);
Scriptable sharedScope = secure ? this.nonSecureScope : this.secureScope;
Scriptable scope = cx.newObject(sharedScope);
scope.setPrototype(sharedScope);
scope.setParentScope(null);
// there's always a model, if only to hold the util objects
if (model == null)
{
model = new HashMap<String, Object>();
}
// add the global scripts
for (ProcessorExtension ex : this.processorExtensions.values())
{
model.put(ex.getExtensionName(), ex);
}
// insert supplied object model into root of the default scope
for (String key : model.keySet())
{
// set the root scope on appropriate objects
// this is used to allow native JS object creation etc.
Object obj = model.get(key);
if (obj instanceof Scopeable)
{
((Scopeable)obj).setScope(scope);
}
// convert/wrap each object to JavaScript compatible
Object jsObject = Context.javaToJS(obj, scope);
// insert into the root scope ready for access by the script
ScriptableObject.putProperty(scope, key, jsObject);
}
// execute the script and return the result
Object result = script.exec(cx, scope);
// extract java object result if wrapped by Rhino
return valueConverter.convertValueForJava(result);
}
catch (WrappedException w)
{
Throwable err = w.getWrappedException();
if (err instanceof RuntimeException)
{
throw (RuntimeException)err;
}
throw new AlfrescoRuntimeException(err.getMessage(), err);
}
catch (Throwable err)
{
throw new AlfrescoRuntimeException(err.getMessage(), err);
}
finally
{
Context.exit();
if (logger.isDebugEnabled())
{
long endTime = System.nanoTime();
logger.debug("Time to execute script: " + (endTime - startTime)/1000000f + "ms");
}
}
}
/**
* Converts the passed model into a Rhino model
*
* @param model the model
*
* @return Map<String, Object> the converted model
*/
private Map<String, Object> convertToRhinoModel(Map<String, Object> model)
{
Map<String, Object> newModel = null;
if (model != null)
{
newModel = new HashMap<String, Object>(model.size());
for (Map.Entry<String, Object> entry : model.entrySet())
{
if (entry.getValue() instanceof NodeRef)
{
newModel.put(entry.getKey(), new ScriptNode((NodeRef)entry.getValue(), this.services));
}
else
{
newModel.put(entry.getKey(), entry.getValue());
}
}
}
else
{
newModel = new HashMap<String, Object>(1, 1.0f);
}
return newModel;
}
/**
* Rhino script value wraper
*/
private static class RhinoWrapFactory extends WrapFactory
{
/* (non-Javadoc)
* @see org.mozilla.javascript.WrapFactory#wrapAsJavaObject(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.Object, java.lang.Class)
*/
public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class staticType)
{
if (javaObject instanceof Map && !(javaObject instanceof ScriptableHashMap))
{
return new NativeMap(scope, (Map)javaObject);
}
return super.wrapAsJavaObject(cx, scope, javaObject, staticType);
}
}
/**
* Pre initializes two scope objects (one secure and one not) with the standard objects preinitialised.
* This saves on very expensive calls to reinitialize a new scope on every web script execution. See
* http://www.mozilla.org/rhino/scopes.html
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception
{
// Initialise the secure scope
Context cx = Context.enter();
try
{
cx.setWrapFactory(wrapFactory);
this.secureScope = cx.initStandardObjects(null, true);
// remove security issue related objects - this ensures the script may not access
// unsecure java.* libraries or import any other classes for direct access - only
// the configured root host objects will be available to the script writer
this.secureScope.delete("Packages");
this.secureScope.delete("getClass");
this.secureScope.delete("java");
}
finally
{
Context.exit();
}
// Initialise the non-secure scope
cx = Context.enter();
try
{
cx.setWrapFactory(wrapFactory);
// allow access to all libraries and objects, including the importer
// @see http://www.mozilla.org/rhino/ScriptingJava.html
this.nonSecureScope = new ImporterTopLevel(cx, true);
}
finally
{
Context.exit();
}
}
}