/* * 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.InputStream; import java.io.Serializable; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ApplicationModel; import org.alfresco.model.ContentModel; import org.alfresco.repo.action.executer.TransformActionExecuter; import org.alfresco.repo.content.transform.magick.ImageTransformationOptions; import org.alfresco.repo.search.QueryParameterDefImpl; import org.alfresco.repo.version.VersionModel; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.lock.LockStatus; import org.alfresco.service.cmr.model.FileExistsException; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; 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.repository.TemplateImageResolver; import org.alfresco.service.cmr.search.QueryParameterDefinition; import org.alfresco.service.cmr.security.AccessPermission; import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.version.Version; import org.alfresco.service.cmr.version.VersionType; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.util.Content; import org.alfresco.util.GUID; import org.alfresco.util.ParameterCheck; import org.alfresco.util.URLEncoder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.UniqueTag; import org.mozilla.javascript.Wrapper; /** * Node class implementation, specific for use by ScriptService as part of the object model. *

* The class exposes Node properties, children and assocs as dynamically populated maps and lists. The various collection classes are mirrored as JavaScript properties. So can be * accessed using standard JavaScript property syntax, such as node.children[0].properties.name. *

* Various helper methods are provided to access common and useful node variables such as the content url and type information. * * @author Kevin Roast */ public class ScriptNode implements Serializable, Scopeable { private static final long serialVersionUID = -3378946227712939600L; private static Log logger = LogFactory.getLog(ScriptNode.class); private final static String NAMESPACE_BEGIN = "" + QName.NAMESPACE_BEGIN; private final static String CONTENT_DEFAULT_URL = "/d/d/{0}/{1}/{2}/{3}"; private final static String CONTENT_DOWNLOAD_URL = "/d/a/{0}/{1}/{2}/{3}"; private final static String CONTENT_PROP_URL = "/d/d/{0}/{1}/{2}/{3}?property={4}"; private final static String CONTENT_DOWNLOAD_PROP_URL = "/d/a/{0}/{1}/{2}/{3}?property={4}"; private final static String FOLDER_BROWSE_URL = "/n/browse/{0}/{1}/{2}"; /** Root scope for this object */ protected Scriptable scope; /** Node Value Converter */ protected NodeValueConverter converter = null; /** Cached values */ protected NodeRef nodeRef; private String name; private QName type; protected String id; /** The aspects applied to this node */ protected Set aspects = null; /** The target associations from this node */ private ScriptableQNameMap targetAssocs = null; /** The source assoications to this node */ private ScriptableQNameMap sourceAssocs = null; /** The child associations for this node */ private ScriptableQNameMap childAssocs = null; /** The children of this node */ private Scriptable children = null; /** The properties of this node */ private ScriptableQNameMap properties = null; protected ServiceRegistry services = null; private NodeService nodeService = null; private Boolean isDocument = null; private Boolean isContainer = null; private Boolean isLinkToDocument = null; private Boolean isLinkToContainer = null; private String displayPath = null; protected TemplateImageResolver imageResolver = null; protected ScriptNode parent = null; private ChildAssociationRef primaryParentAssoc = null; // NOTE: see the reset() method when adding new cached members! // ------------------------------------------------------------------------------ // Construction /** * Constructor * * @param nodeRef The NodeRef this Node wrapper represents * @param services The ServiceRegistry the Node can use to access services * @param resolver Image resolver to use to retrieve icons */ public ScriptNode(NodeRef nodeRef, ServiceRegistry services) { this(nodeRef, services, null); } /** * Constructor * * @param nodeRef The NodeRef this Node wrapper represents * @param services The ServiceRegistry the Node can use to access services * @param resolver Image resolver to use to retrieve icons * @param scope Root scope for this Node */ public ScriptNode(NodeRef nodeRef, ServiceRegistry services, Scriptable scope) { if (nodeRef == null) { throw new IllegalArgumentException("NodeRef must be supplied."); } if (services == null) { throw new IllegalArgumentException("The ServiceRegistry must be supplied."); } this.nodeRef = nodeRef; this.id = nodeRef.getId(); this.services = services; this.nodeService = services.getNodeService(); this.scope = scope; } @Override public int hashCode() { final int PRIME = 31; int result = 1; result = PRIME * result + ((nodeRef == null) ? 0 : nodeRef.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; if (!nodeRef.equals(((ScriptNode)obj).nodeRef)) return false; return true; } /** * Factory method */ public ScriptNode newInstance(NodeRef nodeRef, ServiceRegistry services, Scriptable scope) { return new ScriptNode(nodeRef, services, scope); } /** * @see org.alfresco.repo.jscript.Scopeable#setScope(org.mozilla.javascript.Scriptable) */ public void setScope(Scriptable scope) { this.scope = scope; } // ------------------------------------------------------------------------------ // Node Wrapper API /** * @return The GUID for the node */ public String getId() { return this.id; } /** * @return Returns the NodeRef this Node object represents */ public NodeRef getNodeRef() { return this.nodeRef; } /** * @return Returns the QName type. */ public QName getQNameType() { if (this.type == null) { this.type = this.nodeService.getType(this.nodeRef); } return type; } /** * @return Returns the type. */ public String getType() { return getQNameType().toString(); } /** * @return Helper to return the 'name' property for the node */ public String getName() { if (this.name == null) { // try and get the name from the properties first this.name = (String) getProperties().get("cm:name"); // if we didn't find it as a property get the name from the association name if (this.name == null) { ChildAssociationRef parentRef = this.nodeService.getPrimaryParent(this.nodeRef); if (parentRef != null && parentRef.getQName() != null) { this.name = parentRef.getQName().getLocalName(); } else { this.name = ""; } } } return this.name; } /** * Helper to set the 'name' property for the node. * * @param name Name to set */ public void setName(String name) { if (name != null) { QName typeQName = getQNameType(); if ((services.getDictionaryService().isSubClass(typeQName, ContentModel.TYPE_FOLDER) && !services.getDictionaryService().isSubClass(typeQName, ContentModel.TYPE_SYSTEM_FOLDER)) || services.getDictionaryService().isSubClass(typeQName, ContentModel.TYPE_CONTENT)) { try { this.services.getFileFolderService().rename(this.nodeRef, name); } catch (FileExistsException e) { throw new AlfrescoRuntimeException("Failed to rename node " + nodeRef + " to " + name, e); } catch (FileNotFoundException e) { throw new AlfrescoRuntimeException("Failed to rename node " + nodeRef + " to " + name, e); } } this.getProperties().put(ContentModel.PROP_NAME.toString(), name.toString()); } } /** * @return The children of this Node as JavaScript array of Node object wrappers */ public Scriptable getChildren() { if (this.children == null) { List childRefs = this.nodeService.getChildAssocs(this.nodeRef); Object[] children = new Object[childRefs.size()]; for (int i = 0; i < childRefs.size(); i++) { // create our Node representation from the NodeRef children[i] = newInstance(childRefs.get(i).getChildRef(), this.services, this.scope); } this.children = Context.getCurrentContext().newArray(this.scope, children); } return this.children; } /** * @return Returns the Node at the specified 'cm:name' based Path walking the children of this Node. * So a valid call might be: * mynode.childByNamePath("/QA/Testing/Docs"); */ public ScriptNode childByNamePath(String path) { // convert the name based path to a valid XPath query StringBuilder xpath = new StringBuilder(path.length() << 1); StringTokenizer t = new StringTokenizer(path, "/"); int count = 0; QueryParameterDefinition[] params = new QueryParameterDefinition[t.countTokens()]; DataTypeDefinition ddText = this.services.getDictionaryService().getDataType(DataTypeDefinition.TEXT); NamespaceService ns = this.services.getNamespaceService(); while (t.hasMoreTokens()) { if (xpath.length() != 0) { xpath.append('/'); } String strCount = Integer.toString(count); xpath.append("*[@cm:name=$cm:name") .append(strCount) .append(']'); params[count++] = new QueryParameterDefImpl( QName.createQName(NamespaceService.CONTENT_MODEL_PREFIX, "name" + strCount, ns), ddText, true, t.nextToken()); } Object[] nodes = getChildrenByXPath(xpath.toString(), params, true); return (nodes.length != 0) ? (ScriptNode)nodes[0] : null; } /** * @return Returns a JavaScript array of Nodes at the specified XPath starting at this Node. * So a valid call might be mynode.childrenByXPath("*[@cm:name='Testing']/*"); */ public Scriptable childrenByXPath(String xpath) { return Context.getCurrentContext().newArray(this.scope, getChildrenByXPath(xpath, null, false)); } /** * Return the target associations from this Node. As a Map of assoc name to a JavaScript array of Nodes. * The Map returned implements the Scriptable interface to allow access to the assoc arrays via JavaScript * associative array access. This means associations of this node can be access thus: * node.assocs["translations"][0] * * @return target associations as a Map of assoc name to a JavaScript array of Nodes. */ @SuppressWarnings("unchecked") public Map getAssocs() { if (this.targetAssocs == null) { // this Map implements the Scriptable interface for native JS syntax property access this.targetAssocs = new ScriptableQNameMap(this.services.getNamespaceService()); // get the list of target nodes for each association type List refs = this.nodeService.getTargetAssocs(this.nodeRef, RegexQNamePattern.MATCH_ALL); for (AssociationRef ref : refs) { String qname = ref.getTypeQName().toString(); List nodes = (List)this.targetAssocs.get(qname); if (nodes == null) { // first access of the list for this qname nodes = new ArrayList(4); this.targetAssocs.put(ref.getTypeQName().toString(), nodes); } nodes.add(newInstance(ref.getTargetRef(), this.services, this.scope)); } // convert each Node list into a JavaScript array object for (String qname : this.targetAssocs.keySet()) { List nodes = (List)this.targetAssocs.get(qname); Object[] objs = nodes.toArray(new Object[nodes.size()]); this.targetAssocs.put(qname, Context.getCurrentContext().newArray(this.scope, objs)); } } return this.targetAssocs; } public Map getAssociations() { return getAssocs(); } /** * Return the source associations to this Node. As a Map of assoc name to a JavaScript array of Nodes. * The Map returned implements the Scriptable interface to allow access to the assoc arrays via JavaScript * associative array access. This means source associations to this node can be access thus: * node.sourceAssocs["translations"][0] * * @return source associations as a Map of assoc name to a JavaScript array of Nodes. */ @SuppressWarnings("unchecked") public Map getSourceAssocs() { if (this.sourceAssocs == null) { // this Map implements the Scriptable interface for native JS syntax property access this.sourceAssocs = new ScriptableQNameMap(this.services.getNamespaceService()); // get the list of source nodes for each association type List refs = this.nodeService.getSourceAssocs(this.nodeRef, RegexQNamePattern.MATCH_ALL); for (AssociationRef ref : refs) { String qname = ref.getTypeQName().toString(); List nodes = (List)this.sourceAssocs.get(qname); if (nodes == null) { // first access of the list for this qname nodes = new ArrayList(4); this.sourceAssocs.put(ref.getTypeQName().toString(), nodes); } nodes.add(newInstance(ref.getSourceRef(), this.services, this.scope)); } // convert each Node list into a JavaScript array object for (String qname : this.sourceAssocs.keySet()) { List nodes = (List)this.sourceAssocs.get(qname); Object[] objs = nodes.toArray(new Object[nodes.size()]); this.sourceAssocs.put(qname, Context.getCurrentContext().newArray(this.scope, objs)); } } return this.sourceAssocs; } public Map getSourceAssociations() { return getSourceAssocs(); } /** * Return the child associations from this Node. As a Map of assoc name to a JavaScript array of Nodes. * The Map returned implements the Scriptable interface to allow access to the assoc arrays via JavaScript * associative array access. This means associations of this node can be access thus: * node.childAssocs["contains"][0] * * @return child associations as a Map of assoc name to a JavaScript array of Nodes. */ @SuppressWarnings("unchecked") public Map getChildAssocs() { if (this.childAssocs == null) { // this Map implements the Scriptable interface for native JS syntax property access this.childAssocs = new ScriptableQNameMap(this.services.getNamespaceService()); // get the list of child assoc nodes for each association type List refs = this.nodeService.getChildAssocs(nodeRef); for (ChildAssociationRef ref : refs) { String qname = ref.getTypeQName().toString(); List nodes = (List)this.childAssocs.get(qname); if (nodes == null) { // first access of the list for this qname nodes = new ArrayList(4); this.childAssocs.put(ref.getTypeQName().toString(), nodes); } nodes.add(newInstance(ref.getChildRef(), this.services, this.scope)); } // convert each Node list into a JavaScript array object for (String qname : this.childAssocs.keySet()) { List nodes = (List)this.childAssocs.get(qname); Object[] objs = nodes.toArray(new Object[nodes.size()]); this.childAssocs.put(qname, Context.getCurrentContext().newArray(this.scope, objs)); } } return this.childAssocs; } public Map getChildAssociations() { return getChildAssocs(); } /** * Return all the properties known about this node. The Map returned implements the Scriptable interface to * allow access to the properties via JavaScript associative array access. This means properties of a node can * be access thus: node.properties["name"] * * @return Map of properties for this Node. */ @SuppressWarnings("unchecked") public Map getProperties() { if (this.properties == null) { // this Map implements the Scriptable interface for native JS syntax property access // this impl of the QNameMap is capable of creating ScriptContentData on demand for 'cm:content' // properties that have not been initialised - see AR-1673. this.properties = new ContentAwareScriptableQNameMap(this, this.services); Map props = this.nodeService.getProperties(this.nodeRef); for (QName qname : props.keySet()) { Serializable propValue = props.get(qname); // perform the conversion to a script safe value and store this.properties.put(qname.toString(), getValueConverter().convertValueForScript(qname, propValue)); } } return this.properties; } /** * @return true if this Node is a container (i.e. a folder) */ public boolean getIsContainer() { if (isContainer == null) { DictionaryService dd = this.services.getDictionaryService(); isContainer = Boolean.valueOf((dd.isSubClass(getQNameType(), ContentModel.TYPE_FOLDER) == true && dd.isSubClass(getQNameType(), ContentModel.TYPE_SYSTEM_FOLDER) == false)); } return isContainer.booleanValue(); } /** * @return true if this Node is a Document (i.e. with content) */ public boolean getIsDocument() { if (isDocument == null) { DictionaryService dd = this.services.getDictionaryService(); isDocument = Boolean.valueOf(dd.isSubClass(getQNameType(), ContentModel.TYPE_CONTENT)); } return isDocument.booleanValue(); } /** * @return true if this Node is a Link to a Container (i.e. a folderlink) */ public boolean getIsLinkToContainer() { if (isLinkToContainer == null) { DictionaryService dd = this.services.getDictionaryService(); isLinkToContainer = Boolean.valueOf(dd.isSubClass(getQNameType(), ApplicationModel.TYPE_FOLDERLINK)); } return isLinkToContainer.booleanValue(); } /** * @return true if this Node is a Link to a Document (i.e. a filelink) */ public boolean getIsLinkToDocument() { if (isLinkToDocument == null) { DictionaryService dd = this.services.getDictionaryService(); isLinkToDocument = Boolean.valueOf(dd.isSubClass(getQNameType(), ApplicationModel.TYPE_FILELINK)); } return isLinkToDocument.booleanValue(); } /** * @return true if the Node is a Category */ public boolean getIsCategory() { // this valid is overriden by the CategoryNode sub-class return false; } /** * @return The list of aspects applied to this node */ public Set getAspectsSet() { if (this.aspects == null) { this.aspects = this.nodeService.getAspects(this.nodeRef); } return this.aspects; } /** * @return The array of aspects applied to this node */ public Scriptable getAspects() { Set aspects = getAspectsSet(); String[] result = new String[aspects.size()]; int count = 0; for (QName qname : aspects) { result[count++] = qname.toString(); } return Context.getCurrentContext().newArray(this.scope, result); } /** * @param aspect The aspect name to test for (full qualified or short-name form) * @return true if the node has the aspect false otherwise */ public boolean hasAspect(String aspect) { return getAspectsSet().contains(createQName(aspect)); } /** * @return QName path to this node. This can be used for Lucene PATH: style queries */ public String getQnamePath() { return this.services.getNodeService().getPath(getNodeRef()).toPrefixString(this.services.getNamespaceService()); } /** * @return Display path to this node */ public String getDisplayPath() { if (displayPath == null) { displayPath = this.nodeService.getPath(this.nodeRef).toDisplayPath( this.nodeService, this.services.getPermissionService()); } return displayPath; } /** * @return the small icon image for this node */ public String getIcon16() { return "/images/filetypes/_default.gif"; } /** * @return the large icon image for this node */ public String getIcon32() { return "/images/filetypes32/_default.gif"; } /** * @return true if the node is currently locked */ public boolean getIsLocked() { boolean locked = false; if (getAspectsSet().contains(ContentModel.ASPECT_LOCKABLE)) { LockStatus lockStatus = this.services.getLockService().getLockStatus(this.nodeRef); if (lockStatus == LockStatus.LOCKED || lockStatus == LockStatus.LOCK_OWNER) { locked = true; } } return locked; } /** * @return the parent node */ public ScriptNode getParent() { if (parent == null) { NodeRef parentRef = getPrimaryParentAssoc().getParentRef(); // handle root node (no parent!) if (parentRef != null) { parent = newInstance(parentRef, this.services, this.scope); } } return parent; } /** * @return the primary parent association so we can get at the association QName and the association type QName. */ public ChildAssociationRef getPrimaryParentAssoc() { if (primaryParentAssoc == null) { primaryParentAssoc = this.nodeService.getPrimaryParent(nodeRef); } return primaryParentAssoc; } // ------------------------------------------------------------------------------ // Content API /** * @return the content String for this node from the default content property (@see ContentModel.PROP_CONTENT) */ public String getContent() { String content = ""; ScriptContentData contentData = (ScriptContentData)getProperties().get(ContentModel.PROP_CONTENT); if (contentData != null) { content = contentData.getContent(); } return content; } /** * Set the content for this node * * @param content Content string to set */ public void setContent(String content) { ScriptContentData contentData = (ScriptContentData)getProperties().get(ContentModel.PROP_CONTENT); if (contentData != null) { contentData.setContent(content); } } /** * @return For a content document, this method returns the URL to the content stream for the default content * property (@see ContentModel.PROP_CONTENT) *

* For a container node, this method return the URL to browse to the folder in the web-client */ public String getUrl() { if (getIsDocument() == true) { return MessageFormat.format(CONTENT_DEFAULT_URL, new Object[] { nodeRef.getStoreRef().getProtocol(), nodeRef.getStoreRef().getIdentifier(), nodeRef.getId(), URLEncoder.encode(getName())}); } else { return MessageFormat.format(FOLDER_BROWSE_URL, new Object[] { nodeRef.getStoreRef().getProtocol(), nodeRef.getStoreRef().getIdentifier(), nodeRef.getId() }); } } /** * @return For a content document, this method returns the download URL to the content for * the default content property (@see ContentModel.PROP_CONTENT) *

* For a container node, this method returns an empty string */ public String getDownloadUrl() { if (getIsDocument() == true) { return MessageFormat.format(CONTENT_DOWNLOAD_URL, new Object[] { nodeRef.getStoreRef().getProtocol(), nodeRef.getStoreRef().getIdentifier(), nodeRef.getId(), URLEncoder.encode(getName()) }); } else { return ""; } } public String jsGet_downloadUrl() { return getDownloadUrl(); } /** * @return The WebDav cm:name based path to the content for the default content property * (@see ContentModel.PROP_CONTENT) */ public String getWebdavUrl() { try { List paths = this.services.getFileFolderService().getNamePath(null, getNodeRef()); // build up the webdav url StringBuilder path = new StringBuilder(128); path.append("/webdav"); // build up the path skipping the first path as it is the root folder for (int i=1; i * The default permissions are found in org.alfresco.service.cmr.security.PermissionService. * Most commonly used are "Write", "Delete" and "AddChildren". * * @param permission as found in org.alfresco.service.cmr.security.PermissionService * @return true if the user has the specified permission on the node. */ public boolean hasPermission(String permission) { ParameterCheck.mandatory("Permission Name", permission); boolean allowed = false; if (permission != null && permission.length() != 0) { AccessStatus status = this.services.getPermissionService().hasPermission(this.nodeRef, permission); allowed = (AccessStatus.ALLOWED == status); } return allowed; } /** * @return Array of permissions applied to this Node. * Strings returned are of the format [ALLOWED|DENIED];[USERNAME|GROUPNAME];PERMISSION for example * ALLOWED;kevinr;Consumer so can be easily tokenized on the ';' character. */ public Scriptable getPermissions() { String userName = this.services.getAuthenticationService().getCurrentUserName(); Set acls = this.services.getPermissionService().getAllSetPermissions(getNodeRef()); Object[] permissions = new Object[acls.size()]; int count = 0; for (AccessPermission permission : acls) { StringBuilder buf = new StringBuilder(64); buf.append(permission.getAccessStatus()) .append(';') .append(permission.getAuthority()) .append(';') .append(permission.getPermission()); permissions[count++] = buf.toString(); } return Context.getCurrentContext().newArray(this.scope, permissions); } /** * @return true if the node inherits permissions from the parent node, false otherwise */ public boolean inheritsPermissions() { return this.services.getPermissionService().getInheritParentPermissions(this.nodeRef); } /** * Set whether this node should inherit permissions from the parent node. * * @param inherit True to inherit parent permissions, false otherwise. */ public void setInheritsPermissions(boolean inherit) { this.services.getPermissionService().setInheritParentPermissions(this.nodeRef, inherit); } /** * Apply a permission for ALL users to the node. * * @param permission Permission to apply * @see org.alfresco.service.cmr.security.PermissionService */ public void setPermission(String permission) { ParameterCheck.mandatoryString("Permission Name", permission); this.services.getPermissionService().setPermission( this.nodeRef, PermissionService.ALL_AUTHORITIES, permission, true); } /** * Apply a permission for the specified authority (e.g. username or group) to the node. * * @param permission Permission to apply @see org.alfresco.service.cmr.security.PermissionService * @param authority Authority (generally a username or group name) to apply the permission for */ public void setPermission(String permission, String authority) { ParameterCheck.mandatoryString("Permission Name", permission); ParameterCheck.mandatoryString("Authority", authority); this.services.getPermissionService().setPermission( this.nodeRef, authority, permission, true); } /** * Remove a permission for ALL user from the node. * * @param permission Permission to remove @see org.alfresco.service.cmr.security.PermissionService */ public void removePermission(String permission) { ParameterCheck.mandatoryString("Permission Name", permission); this.services.getPermissionService().deletePermission( this.nodeRef, PermissionService.ALL_AUTHORITIES, permission); } /** * Remove a permission for the specified authority (e.g. username or group) from the node. * * @param permission Permission to remove @see org.alfresco.service.cmr.security.PermissionService * @param authority Authority (generally a username or group name) to apply the permission for */ public void removePermission(String permission, String authority) { ParameterCheck.mandatoryString("Permission Name", permission); ParameterCheck.mandatoryString("Authority", authority); this.services.getPermissionService().deletePermission( this.nodeRef, authority, permission); } // ------------------------------------------------------------------------------ // Ownership API /** * Set the owner of the node */ public void setOwner(String userId) { this.services.getOwnableService().setOwner(this.nodeRef, userId); } /** * Take ownership of the node. */ public void takeOwnership() { this.services.getOwnableService().takeOwnership(this.nodeRef); } /** * Get the owner of the node. * * @return */ public String getOwner() { return this.services.getOwnableService().getOwner(this.nodeRef); } // ------------------------------------------------------------------------------ // Create and Modify API /** * Persist the modified properties of this Node. */ public void save() { // persist properties back to the node in the DB Map props = new HashMap(getProperties().size()); for (String key : this.properties.keySet()) { Serializable value = (Serializable) this.properties.get(key); // perform the conversion from script wrapper object to repo serializable values value = getValueConverter().convertValueForRepo(value); props.put(createQName(key), value); } this.nodeService.setProperties(this.nodeRef, props); } /** * Re-sets the type of the node. Can be called in order specialise a node to a sub-type. This should be used * with caution since calling it changes the type of the node and thus* implies a different set of aspects, * properties and associations. It is the responsibility of the caller to ensure that the node is in a * approriate state after changing the type. * * @param type Type to specialize the node * * @return true if successful, false otherwise */ public boolean specializeType(String type) { ParameterCheck.mandatoryString("Type", type); QName qnameType = createQName(type); // Ensure that we are performing a specialise if (getQNameType().equals(qnameType) == false && this.services.getDictionaryService().isSubClass(qnameType, getQNameType()) == true) { // Specialise the type of the node this.nodeService.setType(this.nodeRef, qnameType); this.type = qnameType; return true; } return false; } /** * Create a new File (cm:content) node as a child of this node. *

* Once created the file should have content set using the content property. * * @param name Name of the file to create * * @return Newly created Node or null if failed to create. */ public ScriptNode createFile(String name) { ParameterCheck.mandatoryString("Node Name", name); FileInfo fileInfo = this.services.getFileFolderService().create( this.nodeRef, name, ContentModel.TYPE_CONTENT); reset(); ScriptNode file = newInstance(fileInfo.getNodeRef(), this.services, this.scope); file.setMimetype(this.services.getMimetypeService().guessMimetype(name)); return file; } /** * Create a new folder (cm:folder) node as a child of this node. * * @param name Name of the folder to create * * @return Newly created Node or null if failed to create. */ public ScriptNode createFolder(String name) { ParameterCheck.mandatoryString("Node Name", name); FileInfo fileInfo = this.services.getFileFolderService().create( this.nodeRef, name, ContentModel.TYPE_FOLDER); reset(); return newInstance(fileInfo.getNodeRef(), this.services, this.scope); } /** * Create a new Node of the specified type as a child of this node. * * @param name Name of the node to create (can be null for a node without a 'cm:name' property) * @param type QName type (fully qualified or short form such as 'cm:content') * * @return Newly created Node or null if failed to create. */ public ScriptNode createNode(String name, String type) { return createNode(name, type, null, ContentModel.ASSOC_CONTAINS.toString()); } /** * Create a new Node of the specified type as a child of this node. * * @param name Name of the node to create (can be null for a node without a 'cm:name' property) * @param type QName type (fully qualified or short form such as 'cm:content') * @param assocName QName of the child association (fully qualified or short form e.g. 'cm:contains') * * @return Newly created Node or null if failed to create. */ public ScriptNode createNode(String name, String type, String assocName) { return createNode(name, type, null, assocName); } /** * Create a new Node of the specified type as a child of this node. * * @param name Name of the node to create (can be null for a node without a 'cm:name' property) * @param type QName type (fully qualified or short form such as 'cm:content') * @param properties Associative array of the default properties for the node. * * @return Newly created Node or null if failed to create. */ public ScriptNode createNode(String name, String type, Object properties) { return createNode(name, type, properties, ContentModel.ASSOC_CONTAINS.toString()); } /** * Create a new Node of the specified type as a child of this node. * * @param name Name of the node to create (can be null for a node without a 'cm:name' property) * @param type QName type (fully qualified or short form such as 'cm:content') * @param properties Associative array of the default properties for the node. * @param assocName QName of the child association (fully qualified or short form e.g. 'cm:contains') * * @return Newly created Node or null if failed to create. */ public ScriptNode createNode(String name, String type, Object properties, String assocName) { ParameterCheck.mandatoryString("Node Type", type); ParameterCheck.mandatoryString("Association Name", assocName); Map props = null; if (properties instanceof ScriptableObject) { props = new HashMap(4, 1.0f); extractScriptableProperties((ScriptableObject)properties, props); } if (name != null) { if (props == null) props = new HashMap(1, 1.0f); props.put(ContentModel.PROP_NAME, name); } else { // set name for the assoc local name name = GUID.generate(); } ChildAssociationRef childAssocRef = this.nodeService.createNode( this.nodeRef, createQName(assocName), QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(name)), createQName(type), props); reset(); return newInstance(childAssocRef.getChildRef(), this.services, this.scope); } /** * Add an existing node as a child of this node. * * @param node node to add as a child of this node */ public void addNode(ScriptNode node) { ParameterCheck.mandatory("node", node); nodeService.addChild(this.nodeRef, node.nodeRef, ContentModel.ASSOC_CONTAINS, ContentModel.ASSOC_CONTAINS); reset(); } /** * Remove an existing child node of this node. * * Severs all parent-child relationships between two nodes. *

* The child node will be cascade deleted if one of the associations was the * primary association, i.e. the one with which the child node was created. * * @param node child node to remove */ public void removeNode(ScriptNode node) { ParameterCheck.mandatory("node", node); nodeService.removeChild(this.nodeRef, node.nodeRef); reset(); } /** * Create an association between this node and the specified target node. * * @param target Destination node for the association * @param assocType Association type qname (short form or fully qualified) */ public void createAssociation(ScriptNode target, String assocType) { ParameterCheck.mandatory("Target", target); ParameterCheck.mandatoryString("Association Type Name", assocType); this.nodeService.createAssociation(this.nodeRef, target.nodeRef, createQName(assocType)); reset(); } /** * Remove an association between this node and the specified target node. * * @param target Destination node on the end of the association * @param assocType Association type qname (short form or fully qualified) */ public void removeAssociation(ScriptNode target, String assocType) { ParameterCheck.mandatory("Target", target); ParameterCheck.mandatoryString("Association Type Name", assocType); this.nodeService.removeAssociation(this.nodeRef, target.nodeRef, createQName(assocType)); reset(); } /** * Remove this node. Any references to this Node or its NodeRef should be discarded! */ public boolean remove() { boolean success = false; if (nodeService.exists(this.nodeRef)) { this.nodeService.deleteNode(this.nodeRef); success = true; } reset(); return success; } /** * Copy this Node to a new parent destination. Note that children of the source Node are not copied. * * @param destination Node * * @return The newly copied Node instance or null if failed to copy. */ public ScriptNode copy(ScriptNode destination) { return copy(destination, false); } /** * Copy this Node and potentially all child nodes to a new parent destination. * * @param destination Node * @param deepCopy True for a deep copy, false otherwise. * * @return The newly copied Node instance or null if failed to copy. */ public ScriptNode copy(ScriptNode destination, boolean deepCopy) { ParameterCheck.mandatory("Destination Node", destination); ScriptNode copy = null; if (destination.getNodeRef().getStoreRef().getProtocol().equals(StoreRef.PROTOCOL_WORKSPACE)) { NodeRef copyRef = this.services.getCopyService().copyAndRename(this.nodeRef, destination.getNodeRef(), ContentModel.ASSOC_CONTAINS, getPrimaryParentAssoc().getQName(), deepCopy); copy = newInstance(copyRef, this.services, this.scope); } else { // NOTE: the deepCopy flag is not respected for this copy mechanism copy = getCrossRepositoryCopyHelper().copy(this, destination, getName()); } return copy; } /** * Move this Node to a new parent destination. * * @param destination Node * * @return true on successful move, false on failure to move. */ public boolean move(ScriptNode destination) { ParameterCheck.mandatory("Destination Node", destination); this.primaryParentAssoc = this.nodeService.moveNode(this.nodeRef, destination.getNodeRef(), ContentModel.ASSOC_CONTAINS, getPrimaryParentAssoc().getQName()); // reset cached values reset(); return true; } /** * Add an aspect to the Node. As no properties are provided in this call, it can only be used to add aspects that do not require any mandatory properties. * * @param type Type name of the aspect to add * * @return true if the aspect was added successfully, false if an error occured. */ public boolean addAspect(String type) { return addAspect(type, null); } /** * Add an aspect to the Node. * * @param type Type name of the aspect to add * @param props ScriptableObject (generally an assocative array) providing the named properties for the aspect * - any mandatory properties for the aspect must be provided! * * @return true if the aspect was added successfully, false if an error occured. */ public boolean addAspect(String type, Object props) { ParameterCheck.mandatoryString("Aspect Type", type); Map aspectProps = null; if (props instanceof ScriptableObject) { aspectProps = new HashMap(4, 1.0f); extractScriptableProperties((ScriptableObject)props, aspectProps); } QName aspectQName = createQName(type); this.nodeService.addAspect(this.nodeRef, aspectQName, aspectProps); // reset the relevant cached node members reset(); return true; } /** * Extract a map of properties from a scriptable object (generally an associative array) * * @param scriptable The scriptable object to extract name/value pairs from. * @param map The map to add the converted name/value pairs to. */ private void extractScriptableProperties(ScriptableObject scriptable, Map map) { // we need to get all the keys to the properties provided // and convert them to a Map of QName to Serializable objects Object[] propIds = scriptable.getIds(); for (int i = 0; i < propIds.length; i++) { // work on each key in turn Object propId = propIds[i]; // we are only interested in keys that are formed of Strings i.e. QName.toString() if (propId instanceof String) { // get the value out for the specified key - it must be Serializable String key = (String)propId; Object value = scriptable.get(key, scriptable); if (value instanceof Serializable) { value = getValueConverter().convertValueForRepo((Serializable)value); map.put(createQName(key), (Serializable)value); } } } } /** * Remove aspect from the node. * * @param type the aspect type * * @return true if successful, false otherwise */ public boolean removeAspect(String type) { ParameterCheck.mandatoryString("Aspect Type", type); QName aspectQName = createQName(type); this.nodeService.removeAspect(this.nodeRef, aspectQName); // reset the relevant cached node members reset(); return true; } // ------------------------------------------------------------------------------ // Checkout/Checkin Services /** * Perform a check-out of this document into the current parent space. * * @return the working copy Node for the checked out document */ public ScriptNode checkout() { NodeRef workingCopyRef = this.services.getCheckOutCheckInService().checkout(this.nodeRef); ScriptNode workingCopy = newInstance(workingCopyRef, this.services, this.scope); // reset the aspect and properties as checking out a document causes changes this.properties = null; this.aspects = null; return workingCopy; } /** * Perform a check-out of this document into the specified destination space. * * @param destination * Destination for the checked out document working copy Node. * @return the working copy Node for the checked out document */ public ScriptNode checkout(ScriptNode destination) { ParameterCheck.mandatory("Destination Node", destination); ChildAssociationRef childAssocRef = this.nodeService.getPrimaryParent(destination.getNodeRef()); NodeRef workingCopyRef = this.services.getCheckOutCheckInService().checkout(this.nodeRef, destination.getNodeRef(), ContentModel.ASSOC_CONTAINS, childAssocRef.getQName()); ScriptNode workingCopy = newInstance(workingCopyRef, this.services, this.scope); // reset the aspect and properties as checking out a document causes changes this.properties = null; this.aspects = null; return workingCopy; } /** * Check-in a working copy document. The current state of the working copy is copied to the original node, * this will include any content updated in the working node. Note that this method can only be called on a * working copy Node. * * @return the original Node that was checked out. */ public ScriptNode checkin() { return checkin("", false); } /** * Check-in a working copy document. The current state of the working copy is copied to the original node, * this will include any content updated in the working node. Note that this method can only be called on a * working copy Node. * * @param history Version history note * * @return the original Node that was checked out. */ public ScriptNode checkin(String history) { return checkin(history, false); } /** * Check-in a working copy document. The current state of the working copy is copied to the original node, * this will include any content updated in the working node. Note that this method can only be called on a * working copy Node. * * @param history Version history note * @param majorVersion True to save as a major version increment, false for minor version. * * @return the original Node that was checked out. */ public ScriptNode checkin(String history, boolean majorVersion) { Map props = new HashMap(2, 1.0f); props.put(Version.PROP_DESCRIPTION, history); props.put(VersionModel.PROP_VERSION_TYPE, majorVersion ? VersionType.MAJOR : VersionType.MINOR); NodeRef original = this.services.getCheckOutCheckInService().checkin(this.nodeRef, props); return newInstance(original, this.services, this.scope); } /** * Cancel the check-out of a working copy document. The working copy will be deleted and any changes made to it * are lost. Note that this method can only be called on a working copy Node. The reference to this working copy * Node should be discarded. * * @return the original Node that was checked out. */ public ScriptNode cancelCheckout() { NodeRef original = this.services.getCheckOutCheckInService().cancelCheckout(this.nodeRef); return newInstance(original, this.services, this.scope); } // ------------------------------------------------------------------------------ // Transformation and Rendering API /** * Transform a document to a new document mimetype format. A copy of the document is made and the extension * changed to match the new mimetype, then the transformation isapplied. * * @param mimetype Mimetype destination for the transformation * * @return Node representing the newly transformed document. */ public ScriptNode transformDocument(String mimetype) { return transformDocument(mimetype, getPrimaryParentAssoc().getParentRef()); } /** * Transform a document to a new document mimetype format. A copy of the document is made in the specified * destination folder and the extension changed to match the new mimetype, then then transformation is applied. * * @param mimetype Mimetype destination for the transformation * @param destination Destination folder location * * @return Node representing the newly transformed document. */ public ScriptNode transformDocument(String mimetype, ScriptNode destination) { return transformDocument(mimetype, destination.getNodeRef()); } private ScriptNode transformDocument(String mimetype, NodeRef destination) { ParameterCheck.mandatoryString("Mimetype", mimetype); ParameterCheck.mandatory("Destination Node", destination); // the delegate definition for transforming a document Transformer transformer = new Transformer() { public ScriptNode transform(ContentService contentService, NodeRef nodeRef, ContentReader reader, ContentWriter writer) { ScriptNode transformedNode = null; if (contentService.isTransformable(reader, writer)) { contentService.transform(reader, writer); transformedNode = newInstance(nodeRef, services, scope); } return transformedNode; } }; return transformNode(transformer, mimetype, destination); } /** * Generic method to transform Node content from one mimetype to another. * * @param transformer The Transformer delegate supplying the transformation logic * @param mimetype Mimetype of the destination content * @param destination Destination folder location for the resulting document * * @return Node representing the transformed content - or null if the transform failed */ private ScriptNode transformNode(Transformer transformer, String mimetype, NodeRef destination) { ScriptNode transformedNode = null; // get the content reader ContentService contentService = this.services.getContentService(); ContentReader reader = contentService.getReader(this.nodeRef, ContentModel.PROP_CONTENT); // only perform the transformation if some content is available if (reader != null) { // Copy the content node to a new node String copyName = TransformActionExecuter.transformName(this.services.getMimetypeService(), getName(), mimetype, true); NodeRef copyNodeRef = this.services.getCopyService().copy(this.nodeRef, destination, ContentModel.ASSOC_CONTAINS, QName.createQName(ContentModel.PROP_CONTENT.getNamespaceURI(), QName.createValidLocalName(copyName)), false); // modify the name of the copy to reflect the new mimetype this.nodeService.setProperty(copyNodeRef, ContentModel.PROP_NAME, copyName); // get the writer and set it up ContentWriter writer = contentService.getWriter(copyNodeRef, ContentModel.PROP_CONTENT, true); writer.setMimetype(mimetype); // new mimetype writer.setEncoding(reader.getEncoding()); // original encoding // Try and transform the content using the supplied delegate transformedNode = transformer.transform(contentService, copyNodeRef, reader, writer); } return transformedNode; } /** * Transform an image to a new image format. A copy of the image document is made and the extension changed to * match the new mimetype, then the transformation is applied. * * @param mimetype Mimetype destination for the transformation * * @return Node representing the newly transformed image. */ public ScriptNode transformImage(String mimetype) { return transformImage(mimetype, null, getPrimaryParentAssoc().getParentRef()); } /** * Transform an image to a new image format. A copy of the image document is made and the extension changed to * match the new mimetype, then the transformation is applied. * * @param mimetype Mimetype destination for the transformation * @param options Image convert command options * * @return Node representing the newly transformed image. */ public ScriptNode transformImage(String mimetype, String options) { return transformImage(mimetype, options, getPrimaryParentAssoc().getParentRef()); } /** * Transform an image to a new image mimetype format. A copy of the image document is made in the specified * destination folder and the extension changed to match the newmimetype, then then transformation is applied. * * @param mimetype Mimetype destination for the transformation * @param destination Destination folder location * * @return Node representing the newly transformed image. */ public ScriptNode transformImage(String mimetype, ScriptNode destination) { ParameterCheck.mandatory("Destination Node", destination); return transformImage(mimetype, null, destination.getNodeRef()); } /** * Transform an image to a new image mimetype format. A copy of the image document is made in the specified * destination folder and the extension changed to match the new * mimetype, then then transformation is applied. * * @param mimetype Mimetype destination for the transformation * @param options Image convert command options * @param destination Destination folder location * * @return Node representing the newly transformed image. */ public ScriptNode transformImage(String mimetype, String options, ScriptNode destination) { ParameterCheck.mandatory("Destination Node", destination); return transformImage(mimetype, options, destination.getNodeRef()); } private ScriptNode transformImage(String mimetype, final String options, NodeRef destination) { ParameterCheck.mandatoryString("Mimetype", mimetype); // the delegate definition for transforming an image Transformer transformer = new Transformer() { public ScriptNode transform(ContentService contentService, NodeRef nodeRef, ContentReader reader, ContentWriter writer) { ImageTransformationOptions imageOptions = new ImageTransformationOptions(); if (options != null) { imageOptions.setCommandOptions(options); } contentService.getImageTransformer().transform(reader, writer, imageOptions); return newInstance(nodeRef, services, scope); } }; return transformNode(transformer, mimetype, destination); } /** * Process a FreeMarker Template against the current node. * * @param template Node of the template to execute * * @return output of the template execution */ public String processTemplate(ScriptNode template) { ParameterCheck.mandatory("Template Node", template); return processTemplate(template.getContent(), null, null); } /** * Process a FreeMarker Template against the current node. * * @param template Node of the template to execute * @param args Scriptable object (generally an associative array) containing the name/value pairs of * arguments to be passed to the template * * @return output of the template execution */ public String processTemplate(ScriptNode template, Object args) { ParameterCheck.mandatory("Template Node", template); return processTemplate(template.getContent(), null, (ScriptableObject)args); } /** * Process a FreeMarker Template against the current node. * * @param template The template to execute * * @return output of the template execution */ public String processTemplate(String template) { ParameterCheck.mandatoryString("Template", template); return processTemplate(template, null, null); } /** * Process a FreeMarker Template against the current node. * * @param template The template to execute * @param args Scriptable object (generally an associative array) containing the name/value pairs of * arguments to be passed to the template * * @return output of the template execution */ public String processTemplate(String template, Object args) { ParameterCheck.mandatoryString("Template", template); return processTemplate(template, null, (ScriptableObject)args); } private String processTemplate(String template, NodeRef templateRef, ScriptableObject args) { Object person = (Object)scope.get("person", scope); Object companyhome = (Object)scope.get("companyhome", scope); Object userhome = (Object)scope.get("userhome", scope); // build default model for the template processing Map model = this.services.getTemplateService().buildDefaultModel( (person.equals(UniqueTag.NOT_FOUND)) ? null : ((ScriptNode)((Wrapper)person).unwrap()).getNodeRef(), (companyhome.equals(UniqueTag.NOT_FOUND)) ? null : ((ScriptNode)((Wrapper)companyhome).unwrap()).getNodeRef(), (userhome.equals(UniqueTag.NOT_FOUND)) ? null : ((ScriptNode)((Wrapper)userhome).unwrap()).getNodeRef(), templateRef, null); // add the current node as either the document/space as appropriate if (this.getIsDocument()) { model.put("document", this.nodeRef); model.put("space", getPrimaryParentAssoc().getParentRef()); } else { model.put("space", this.nodeRef); } // add the supplied args to the 'args' root object if (args != null) { // we need to get all the keys to the properties provided // and convert them to a Map of QName to Serializable objects Object[] propIds = args.getIds(); Map templateArgs = new HashMap(propIds.length); for (int i = 0; i < propIds.length; i++) { // work on each key in turn Object propId = propIds[i]; // we are only interested in keys that are formed of Strings i.e. QName.toString() if (propId instanceof String) { // get the value out for the specified key - make sure it is Serializable Object value = args.get((String) propId, args); value = getValueConverter().convertValueForRepo((Serializable)value); if (value != null) { templateArgs.put((String) propId, value.toString()); } } } // add the args to the model as the 'args' root object model.put("args", templateArgs); } // execute template! // TODO: check that script modified nodes are reflected... return this.services.getTemplateService().processTemplateString(null, template, model); } // ------------------------------------------------------------------------------ // Helper methods /** * Override Object.toString() to provide useful debug output */ public String toString() { if (this.nodeService.exists(nodeRef)) { if (this.services.getPermissionService().hasPermission(nodeRef, PermissionService.READ_PROPERTIES) == AccessStatus.ALLOWED) { // TODO: DC: Allow debug output of property values - for now it's disabled as this could potentially // follow a large network of nodes. Unfortunately, JBPM issues unprotected debug statements // where node.toString is used - will request this is fixed in next release of JBPM. return "Node Type: " + getType() + ", Node Aspects: " + getAspectsSet().toString(); } else { return "Access denied to node " + nodeRef; } } else { return "Node no longer exists: " + nodeRef; } } /** * Helper to create a QName from either a fully qualified or short-name QName string * * @param s Fully qualified or short-name QName string * * @return QName */ private QName createQName(String s) { QName qname; if (s.indexOf(NAMESPACE_BEGIN) != -1) { qname = QName.createQName(s); } else { qname = QName.createQName(s, this.services.getNamespaceService()); } return qname; } /** * Reset the Node cached state */ public void reset() { this.name = null; this.type = null; this.properties = null; this.aspects = null; this.targetAssocs = null; this.sourceAssocs = null; this.childAssocs = null; this.children = null; this.displayPath = null; this.isDocument = null; this.isContainer = null; this.parent = null; this.primaryParentAssoc = null; } /** * @return helper object to perform cross repository copy of JavaScript Node objects */ protected CrossRepositoryCopy getCrossRepositoryCopyHelper() { return (CrossRepositoryCopy)this.services.getService( QName.createQName("", CrossRepositoryCopy.BEAN_NAME)); } /** * Return a list or a single Node from executing an xpath against the parent Node. * * @param xpath XPath to execute * @param firstOnly True to return the first result only * * @return Object[] can be empty but never null */ private Object[] getChildrenByXPath(String xpath, QueryParameterDefinition[] params, boolean firstOnly) { Object[] result = null; if (xpath.length() != 0) { if (logger.isDebugEnabled()) { logger.debug("Executing xpath: " + xpath); if (params != null) { for (int i=0; i