/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.sling.scripting.javascript.wrapper; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Value; import javax.jcr.ValueFormatException; import javax.jcr.nodetype.NodeType; import org.apache.sling.scripting.javascript.SlingWrapper; import org.mozilla.javascript.Context; import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.Undefined; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A wrapper for JCR nodes that exposes all properties and child nodes as * properties of a Javascript object. */ @SuppressWarnings("serial") public class ScriptableNode extends ScriptableBase implements SlingWrapper { public static final String CLASSNAME = "Node"; private static final Class<?> [] WRAPPED_CLASSES = { Node.class }; /** default log */ private final Logger log = LoggerFactory.getLogger(getClass()); /** * The wrapped JCR Node instance. Will be {@code null} if the * {@link #jsConstructor(Object)} method is not called, which particularly * is the case for the Node host object prototype. */ private Node node; public void jsConstructor(Object res) { this.node = (Node) res; } @Override public String getClassName() { return CLASSNAME; } @Override public Class<?> [] getWrappedClasses() { return WRAPPED_CLASSES; } @Override protected Class<?> getStaticType() { return Node.class; } @Override protected Object getWrappedObject() { return node; } public Object jsFunction_addNode(String path, String primaryType) throws RepositoryException { Node n = null; if(primaryType == null || "undefined".equals(primaryType)) { n = node.addNode(path); } else { n = node.addNode(path, primaryType); } final Object result = ScriptRuntime.toObject(this, n); return result; } public Object jsFunction_getNode(String path) throws RepositoryException { return ScriptRuntime.toObject(this, node.getNode(path)); } public Object jsFunction_getChildren() { try { return toScriptableItemMap(node.getNodes()); } catch (RepositoryException re) { log.warn("Cannot get children of " + jsFunction_getPath(), re); return toScriptableItemMap(null); } } public Object jsFunction_getNodes(String namePattern) { try { NodeIterator iter = null; if(namePattern == null || "undefined".equals(namePattern)) { iter = node.getNodes(); } else { iter = node.getNodes(namePattern); } return toScriptableItemMap(iter); } catch (RepositoryException re) { log.warn("Cannot get children of " + jsFunction_getPath() + " with pattern " + namePattern, re); return toScriptableItemMap(null); } } public Object jsFunction_getProperties() { try { return toScriptableItemMap(node.getProperties()); } catch (RepositoryException re) { log.warn("Cannot get properties of " + jsFunction_getPath(), re); return toScriptableItemMap(null); } } public Object jsFunction_getPrimaryItem() { try { return ScriptRuntime.toObject(this, node.getPrimaryItem()); } catch (RepositoryException re) { return Undefined.instance; } } public Object jsFunction_getProperty(String name) throws RepositoryException { Object[] args = { node.getProperty(name) }; return ScriptRuntime.newObject(Context.getCurrentContext(), this, ScriptableProperty.CLASSNAME, args); } public String jsFunction_getUUID() { try { return node.getUUID(); } catch (RepositoryException re) { return ""; } } public int jsFunction_getIndex() { try { return node.getIndex(); } catch (RepositoryException re) { return 1; } } public Iterator<?> jsFunction_getReferences() { try { return node.getReferences(); } catch (RepositoryException re) { return Collections.EMPTY_LIST.iterator(); } } public Object jsFunction_getPrimaryNodeType() { try { return node.getPrimaryNodeType(); } catch (RepositoryException re) { return Undefined.instance; } } public NodeType[] jsFunction_getMixinNodeTypes() { try { return node.getMixinNodeTypes(); } catch (RepositoryException re) { return new NodeType[0]; } } public Object jsFunction_getDefinition() { try { return node.getDefinition(); } catch (RepositoryException re) { return Undefined.instance; } } public boolean jsFunction_getCheckedOut() { try { return node.isCheckedOut(); } catch (RepositoryException re) { return false; } } public Object jsFunction_getVersionHistory() { try { return ScriptRuntime.toObject(this, node.getVersionHistory()); } catch (RepositoryException re) { return Undefined.instance; } } public Object jsFunction_getBaseVersion() { try { return ScriptRuntime.toObject(this, node.getBaseVersion()); } catch (RepositoryException re) { return Undefined.instance; } } public Object jsFunction_getLock() { try { return node.getLock(); } catch (RepositoryException re) { return Undefined.instance; } } public boolean jsFunction_getLocked() { try { return node.isLocked(); } catch (RepositoryException re) { return false; } } public Object jsFunction_getSession() { try { return node.getSession(); } catch (RepositoryException re) { return Undefined.instance; } } public String jsFunction_getPath() { try { return node.getPath(); } catch (RepositoryException e) { return node.toString(); } } public String jsFunction_getName() { try { return node.getName(); } catch (RepositoryException e) { return node.toString(); } } public Object jsFunction_getParent() { try { return ScriptRuntime.toObject(this, node.getParent()); } catch (RepositoryException re) { return Undefined.instance; } } public int jsFunction_getDepth() { try { return node.getDepth(); } catch (RepositoryException re) { return -1; } } public boolean jsFunction_getNew() { return node.isNew(); } public boolean jsFunction_getModified() { return node.isModified(); } public void jsFunction_remove() throws RepositoryException { node.remove(); } public boolean jsFunction_hasNode(String path) throws RepositoryException { return node.hasNode(path); } /** * Gets the value of a (Javascript) property or child node. If there is a single single-value * JCR property of this node, return its string value. If there are multiple properties * of the same name or child nodes of the same name, return an array. */ @Override public Object get(String name, Scriptable start) { // builtin javascript properties (jsFunction_ etc.) have priority final Object fromSuperclass = super.get(name, start); if(fromSuperclass != Scriptable.NOT_FOUND) { return fromSuperclass; } if(node == null) { return Undefined.instance; } final List<Scriptable> items = new ArrayList<Scriptable>(); // Add all matching nodes to result try { NodeIterator it = node.getNodes(name); while (it.hasNext()) { items.add(ScriptRuntime.toObject(this, it.nextNode())); } } catch (RepositoryException e) { log.debug("RepositoryException while collecting Node children",e); } // Add all matching properties to result boolean isMulti = false; try { PropertyIterator it = node.getProperties(name); while (it.hasNext()) { Property prop = it.nextProperty(); if (prop.getDefinition().isMultiple()) { isMulti = true; Value[] values = prop.getValues(); for (int i = 0; i < values.length; i++) { items.add(wrap(values[i])); } } else { items.add(wrap(prop.getValue())); } } } catch (RepositoryException e) { log.debug("RepositoryException while collecting Node properties", e); } if (items.size()==0) { return getNative(name, start); } else if (items.size()==1 && !isMulti) { return items.iterator().next(); } else { NativeArray result = new NativeArray(items.toArray()); ScriptRuntime.setObjectProtoAndParent(result, this); return result; } } /** Wrap JCR Values in a simple way */ private Scriptable wrap(Value value) throws ValueFormatException, IllegalStateException, RepositoryException { Object javaObject; if (value.getType() == PropertyType.REFERENCE) { String nodeUuid = value.getString(); javaObject = node.getSession().getNodeByUUID(nodeUuid); } else { javaObject = toJavaObject(value); } return ScriptRuntime.toObject(this, javaObject); } /** * Converts a JCR Value to a corresponding Java Object * * @param value the JCR Value to convert * @return the Java Object * @throws RepositoryException if the value cannot be converted */ private static Object toJavaObject(Value value) throws RepositoryException { switch (value.getType()) { case PropertyType.DECIMAL: return value.getDecimal(); case PropertyType.BINARY: return new LazyInputStream(value); case PropertyType.BOOLEAN: return value.getBoolean(); case PropertyType.DATE: return value.getDate(); case PropertyType.DOUBLE: return value.getDouble(); case PropertyType.LONG: return value.getLong(); case PropertyType.NAME: // fall through case PropertyType.PATH: // fall through case PropertyType.REFERENCE: // fall through case PropertyType.STRING: // fall through case PropertyType.UNDEFINED: // not actually expected default: // not actually expected return value.getString(); } } @Override public Object[] getIds() { Collection<String> ids = new ArrayList<String>(); if(node != null) { try { PropertyIterator pit = node.getProperties(); while (pit.hasNext()) { ids.add(pit.nextProperty().getName()); } } catch (RepositoryException e) { //do nothing, just do not list properties } try { NodeIterator nit = node.getNodes(); while (nit.hasNext()) { ids.add(nit.nextNode().getName()); } } catch (RepositoryException e) { //do nothing, just do not list child nodes } } return ids.toArray(); } @SuppressWarnings("unchecked") @Override public Object getDefaultValue(Class typeHint) { return toString(); } @Override public boolean has(String name, Scriptable start) { if (node != null) { try { // TODO should this take into account our jsFunction_ members? return node.hasProperty(name) || node.hasNode(name); } catch (RepositoryException e) { // does not matter } } return false; } @Override public String toString() { if (node != null) { try { return node.getPath(); } catch (RepositoryException e) { return node.toString(); } } return String.valueOf((Object) null); } // ---------- Wrapper interface -------------------------------------------- // returns the wrapped node @Override public Object unwrap() { return node; } //---------- Helper ------------------------------------------------------- private Object toScriptableItemMap(Iterator<?> iter) { Object[] args = (iter != null) ? new Object[] { iter } : null; return ScriptRuntime.newObject(Context.getCurrentContext(), this, ScriptableItemMap.CLASSNAME, args); } /** * Lazily acquired InputStream which only accesses the JCR Value InputStream if * data is to be read from the stream. */ private static class LazyInputStream extends InputStream { /** The JCR Value from which the input stream is requested on demand */ private final Value value; /** The inputstream created on demand, null if not used */ private InputStream delegatee; public LazyInputStream(Value value) { this.value = value; } /** * Closes the input stream if acquired otherwise does nothing. */ @Override public void close() throws IOException { if (delegatee != null) { delegatee.close(); } } @Override public int available() throws IOException { return getStream().available(); } @Override public int read() throws IOException { return getStream().read(); } @Override public int read(byte[] b) throws IOException { return getStream().read(b); } @Override public int read(byte[] b, int off, int len) throws IOException { return getStream().read(b, off, len); } @Override public long skip(long n) throws IOException { return getStream().skip(n); } @Override public boolean markSupported() { try { return getStream().markSupported(); } catch (IOException ioe) { // ignore } return false; } @Override public synchronized void mark(int readlimit) { try { getStream().mark(readlimit); } catch (IOException ioe) { // ignore } } @Override public synchronized void reset() throws IOException { getStream().reset(); } /** Actually retrieves the input stream from the underlying JCR Value */ private InputStream getStream() throws IOException { if (delegatee == null) { try { delegatee = value.getStream(); } catch (RepositoryException re) { throw (IOException) new IOException(re.getMessage()).initCause(re); } } return delegatee; } } }