diff --git a/source/java/org/alfresco/repo/jscript/Node.java b/source/java/org/alfresco/repo/jscript/Node.java
index a338e4571c..bb0d9b58c4 100644
--- a/source/java/org/alfresco/repo/jscript/Node.java
+++ b/source/java/org/alfresco/repo/jscript/Node.java
@@ -23,6 +23,7 @@ import java.text.MessageFormat;
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.ContentModel;
@@ -37,6 +38,7 @@ import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.TemplateImageResolver;
+import org.alfresco.service.cmr.repository.TemplateNode;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.apache.commons.logging.Log;
@@ -44,11 +46,12 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.util.StringUtils;
/**
- * Node class specific for use by Template pages that support Bean objects as part of the model.
- * The default template engine FreeMarker can use these objects and they are provided to support it.
- * A single method is completely freemarker specific - getXmlNodeModel()
+ * Node class implementation specific for use by ScriptService as part of the object model.
*
- * The class exposes Node properties, children as dynamically populated maps and lists.
+ *
+ * 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 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.
@@ -113,8 +116,6 @@ public final class Node implements Serializable
this.id = nodeRef.getId();
this.services = services;
this.imageResolver = resolver;
-
- this.properties = new ScriptableQNameMap(this.services.getNamespaceService());
}
/**
@@ -125,6 +126,11 @@ public final class Node implements Serializable
return this.id;
}
+ public String jsGet_id()
+ {
+ return getId();
+ }
+
/**
* @return Returns the NodeRef this Node object represents
*/
@@ -133,6 +139,11 @@ public final class Node implements Serializable
return this.nodeRef;
}
+ public NodeRef jsGet_nodeRef()
+ {
+ return getNodeRef();
+ }
+
/**
* @return Returns the type.
*/
@@ -146,6 +157,11 @@ public final class Node implements Serializable
return type;
}
+ public QName jsGet_type()
+ {
+ return getType();
+ }
+
/**
* @return The display name for the node
*/
@@ -174,6 +190,16 @@ public final class Node implements Serializable
return this.name;
}
+ public String jsGet_name()
+ {
+ return getName();
+ }
+
+ public void jsSet_name(String name)
+ {
+ this.getProperties().put(ContentModel.PROP_NAME.toString(), name);
+ }
+
/**
* @return The children of this Node as Node wrappers
*/
@@ -194,21 +220,54 @@ public final class Node implements Serializable
return this.children;
}
+ public Node[] jsGet_children()
+ {
+ return getChildren();
+ }
+
/**
- * @return A map capable of returning the Node at the specified Path as a child of this node.
+ * @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 Map getChildByNamePath()
+ public Node childByNamePath(String path)
+ {
+ // convert the name based path to a valid XPath query
+ StringBuilder xpath = new StringBuilder(path.length() << 1);
+ for (StringTokenizer t = new StringTokenizer(path, "/"); t.hasMoreTokens(); /**/)
+ {
+ if (xpath.length() != 0)
+ {
+ xpath.append('/');
+ }
+ xpath.append("*[@cm:name='")
+ .append(t.nextToken()) // TODO: use QueryParameterDefinition see FileFolderService.search()
+ .append("']");
+ }
+
+ Node[] nodes = getChildrenByXPath(xpath.toString(), true);
+
+ return (nodes.length != 0) ? nodes[0] : null;
+ }
+
+ // TODO: find out why this doesn't work - the function defs do not seem to get found
+ //public Node jsFunction_childByNamePath(String path)
//{
- // return new NamePathResultsMap(this, this.services);
+ // return getChildByNamePath(path);
//}
/**
- * @return A map capable of returning a List of Node objects from an XPath query
- * as children of this node.
+ * @return Returns the Nodes at the specified XPath walking the children of this Node.
+ * So a valid call might be mynode.childrenByXPath("/*[@cm:name='Testing']/*");
*/
- //public Map getChildrenByXPath()
+ public Node[] childrenByXPath(String xpath)
+ {
+ return getChildrenByXPath(xpath, false);
+ }
+
+ // TODO: find out why this doesn't work - the function defs do not seem to get found
+ //public Node[] jsFunction_childrenByXPath(String xpath)
//{
- // return new XPathResultsMap(this, this.services);
+ // return childrenByXPath(xpath);
//}
/**
@@ -242,8 +301,10 @@ public final class Node implements Serializable
{
if (this.assocs == null)
{
- List refs = this.services.getNodeService().getTargetAssocs(this.nodeRef, RegexQNamePattern.MATCH_ALL);
+ // this Map implements the Scriptable interface for native JS syntax property access
this.assocs = new ScriptableQNameMap(this.services.getNamespaceService());
+
+ List refs = this.services.getNodeService().getTargetAssocs(this.nodeRef, RegexQNamePattern.MATCH_ALL);
for (AssociationRef ref : refs)
{
String qname = ref.getTypeQName().toString();
@@ -268,6 +329,11 @@ public final class Node implements Serializable
return this.assocs;
}
+ public Map jsGet_assocs()
+ {
+ return getAssocs();
+ }
+
/**
* Return all the properties known about this node.
*
@@ -281,8 +347,10 @@ public final class Node implements Serializable
{
if (this.propsRetrieved == false)
{
- Map props = this.services.getNodeService().getProperties(this.nodeRef);
+ // this Map implements the Scriptable interface for native JS syntax property access
+ this.properties = new ScriptableQNameMap(this.services.getNamespaceService());
+ Map props = this.services.getNodeService().getProperties(this.nodeRef);
for (QName qname : props.keySet())
{
Serializable propValue = props.get(qname);
@@ -307,6 +375,11 @@ public final class Node implements Serializable
return this.properties;
}
+ public Map jsGet_properties()
+ {
+ return getProperties();
+ }
+
/**
* @return true if this Node is a container (i.e. a folder)
*/
@@ -322,6 +395,11 @@ public final class Node implements Serializable
return isContainer.booleanValue();
}
+ public boolean jsGet_isContainer()
+ {
+ return isContainer();
+ }
+
/**
* @return true if this Node is a Document (i.e. with content)
*/
@@ -336,6 +414,11 @@ public final class Node implements Serializable
return isDocument.booleanValue();
}
+ public boolean jsGet_isDocument()
+ {
+ return isDocument();
+ }
+
/**
* @return The list of aspects applied to this node
*/
@@ -349,6 +432,11 @@ public final class Node implements Serializable
return this.aspects;
}
+ public QName[] jsGet_aspects()
+ {
+ return getAspects().toArray(new QName[getAspects().size()]);
+ }
+
/**
* @param aspect The aspect name to test for
*
@@ -400,6 +488,11 @@ public final class Node implements Serializable
return displayPath;
}
+ public String jsGet_displayPath()
+ {
+ return getDisplayPath();
+ }
+
/**
* @return the small icon image for this node
*/
@@ -422,6 +515,11 @@ public final class Node implements Serializable
}
}
+ public String jsGet_icon16()
+ {
+ return getIcon16();
+ }
+
/**
* @return the large icon image for this node
*/
@@ -452,6 +550,11 @@ public final class Node implements Serializable
}
}
+ public String jsGet_icon32()
+ {
+ return getIcon32();
+ }
+
/**
* @return true if the node is currently locked
*/
@@ -471,6 +574,11 @@ public final class Node implements Serializable
return locked;
}
+ public boolean jsGet_isLocked()
+ {
+ return isLocked();
+ }
+
/**
* @return the parent node
*/
@@ -489,6 +597,11 @@ public final class Node implements Serializable
return parent;
}
+ public Node jsGet_parent()
+ {
+ return getParent();
+ }
+
/**
*
* @return the primary parent association so we can get at the association QName and the association type QName.
@@ -502,6 +615,11 @@ public final class Node implements Serializable
return primaryParentAssoc;
}
+ public ChildAssociationRef jsGet_primaryParentAssoc()
+ {
+ return getPrimaryParentAssoc();
+ }
+
/**
* @return the content String for this node from the default content property
* (@see ContentModel.PROP_CONTENT)
@@ -513,6 +631,11 @@ public final class Node implements Serializable
return (reader != null && reader.exists()) ? reader.getContentString() : "";
}
+ public String jsGet_content()
+ {
+ return getContent();
+ }
+
/**
* @return For a content document, this method returns the URL to the content stream for
* the default content property (@see ContentModel.PROP_CONTENT)
@@ -545,6 +668,11 @@ public final class Node implements Serializable
}
}
+ public String jsGet_url()
+ {
+ return getUrl();
+ }
+
/**
* @return The mimetype encoding for content attached to the node from the default content property
* (@see ContentModel.PROP_CONTENT)
@@ -563,6 +691,11 @@ public final class Node implements Serializable
return mimetype;
}
+ public String jsGet_mimetype()
+ {
+ return getMimetype();
+ }
+
/**
* @return The size in bytes of the content attached to the node from the default content property
* (@see ContentModel.PROP_CONTENT)
@@ -581,6 +714,11 @@ public final class Node implements Serializable
return size != null ? size.longValue() : 0L;
}
+ public long jsGet_size()
+ {
+ return getSize();
+ }
+
/**
* @return the image resolver instance used by this node
*/
@@ -606,6 +744,63 @@ public final class Node implements Serializable
}
}
+ /**
+ * 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 Node[] can be empty but never null
+ */
+ private Node[] getChildrenByXPath(String xpath, boolean firstOnly)
+ {
+ Node[] result = null;
+
+ if (xpath.length() != 0)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Executing xpath: " + xpath);
+
+ NodeRef contextRef;
+ if (getParent() != null)
+ {
+ contextRef = getParent().getNodeRef();
+ }
+ else
+ {
+ contextRef = this.services.getNodeService().getRootNode(nodeRef.getStoreRef());
+ }
+ List nodes = this.services.getSearchService().selectNodes(
+ contextRef,
+ xpath,
+ null,
+ this.services.getNamespaceService(),
+ false);
+
+ // see if we only want the first result
+ if (firstOnly == true)
+ {
+ if (nodes.size() != 0)
+ {
+ result = new Node[1];
+ result[0] = new Node(nodes.get(0), this.services, this.imageResolver);
+ }
+ }
+ // or all the results
+ else
+ {
+ result = new Node[nodes.size()];
+ for (int i=0; i model) throws ScriptException
+ public Object executeScript(String scriptClasspath, Map model)
+ throws ScriptException
{
if (scriptClasspath == null)
{
@@ -104,9 +106,10 @@ public class RhinoScriptService implements ScriptService
}
/**
- * @see org.alfresco.service.cmr.repository.ScriptService#executeScript(org.alfresco.service.cmr.repository.NodeRef, java.util.Map)
+ * @see org.alfresco.service.cmr.repository.ScriptService#executeScript(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.util.Map)
*/
- public Object executeScript(NodeRef scriptRef, Map model) throws ScriptException
+ public Object executeScript(NodeRef scriptRef, QName contentProp, Map model)
+ throws ScriptException
{
if (scriptRef == null)
{
@@ -120,7 +123,12 @@ public class RhinoScriptService implements ScriptService
{
throw new AlfrescoRuntimeException("Script Node does not exist: " + scriptRef);
}
- ContentReader cr = this.contentService.getReader(scriptRef, ContentModel.PROP_CONTENT);
+
+ if (contentProp == null)
+ {
+ contentProp = ContentModel.PROP_CONTENT;
+ }
+ ContentReader cr = this.contentService.getReader(scriptRef, contentProp);
if (cr == null || cr.exists() == false)
{
throw new AlfrescoRuntimeException("Script Node content not found: " + scriptRef);
@@ -145,7 +153,8 @@ public class RhinoScriptService implements ScriptService
/**
* @see org.alfresco.service.cmr.repository.ScriptService#executeScriptString(java.lang.String, java.util.Map)
*/
- public Object executeScriptString(String script, Map model) throws ScriptException
+ public Object executeScriptString(String script, Map model)
+ throws ScriptException
{
if (script == null || script.length() == 0)
{
@@ -193,7 +202,8 @@ public class RhinoScriptService implements ScriptService
{
for (String key : model.keySet())
{
- ScriptableObject.putProperty(scope, key, model.get(key));
+ Object jsObject = Context.javaToJS(model.get(key), scope);
+ ScriptableObject.putProperty(scope, key, jsObject);
}
}
diff --git a/source/java/org/alfresco/repo/jscript/RhinoScriptTest.java b/source/java/org/alfresco/repo/jscript/RhinoScriptTest.java
index 72132b6c90..df3f5fe57c 100644
--- a/source/java/org/alfresco/repo/jscript/RhinoScriptTest.java
+++ b/source/java/org/alfresco/repo/jscript/RhinoScriptTest.java
@@ -17,10 +17,14 @@
package org.alfresco.repo.jscript;
import java.io.InputStream;
+import java.io.Serializable;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import junit.framework.TestCase;
+import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.dictionary.DictionaryComponent;
import org.alfresco.repo.dictionary.DictionaryDAO;
import org.alfresco.repo.dictionary.M2Model;
@@ -29,11 +33,14 @@ import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.transaction.TransactionUtil;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.ContentData;
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.ScriptService;
import org.alfresco.service.cmr.repository.StoreRef;
-import org.alfresco.service.cmr.repository.TemplateService;
+import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.mozilla.javascript.Context;
@@ -49,11 +56,11 @@ public class RhinoScriptTest extends TestCase
private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
private ContentService contentService;
- private TemplateService templateService;
private NodeService nodeService;
private TransactionService transactionService;
private ServiceRegistry serviceRegistry;
private AuthenticationComponent authenticationComponent;
+ private ScriptService scriptService;
/*
* @see junit.framework.TestCase#setUp()
@@ -65,7 +72,7 @@ public class RhinoScriptTest extends TestCase
transactionService = (TransactionService)this.ctx.getBean("transactionComponent");
contentService = (ContentService)this.ctx.getBean("contentService");
nodeService = (NodeService)this.ctx.getBean("nodeService");
- templateService = (TemplateService)this.ctx.getBean("templateService");
+ scriptService = (ScriptService)this.ctx.getBean("scriptService");
serviceRegistry = (ServiceRegistry)this.ctx.getBean("ServiceRegistry");
this.authenticationComponent = (AuthenticationComponent)ctx.getBean("authenticationComponent");
@@ -98,7 +105,55 @@ public class RhinoScriptTest extends TestCase
super.tearDown();
}
- public void testRhino()
+ public void testRhinoIntegration()
+ {
+ TransactionUtil.executeInUserTransaction(
+ transactionService,
+ new TransactionUtil.TransactionWork