blob: 009c570c9f94b0f4d5f210c13a8b250a367a95a8 [file] [log] [blame]
/*
* 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;
}
}
}