| /* |
| * 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; |
| } |
| |
| } |
| } |