SLING-4953 - Enhance the JavaScript API provided by the org.apache.sling.scripting.javascript bundle
* added a wrapper for Map objects so that Map keys are mapped as object properties for the JavaScript object
representing the map
* added a 'properties' property for Javascript objects representing resources
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1696254 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/sling/scripting/javascript/internal/RhinoJavaScriptEngineFactory.java b/src/main/java/org/apache/sling/scripting/javascript/internal/RhinoJavaScriptEngineFactory.java
index d2924b0..8938205 100644
--- a/src/main/java/org/apache/sling/scripting/javascript/internal/RhinoJavaScriptEngineFactory.java
+++ b/src/main/java/org/apache/sling/scripting/javascript/internal/RhinoJavaScriptEngineFactory.java
@@ -42,6 +42,7 @@
import org.apache.sling.scripting.javascript.helper.SlingWrapFactory;
import org.apache.sling.scripting.javascript.wrapper.ScriptableCalendar;
import org.apache.sling.scripting.javascript.wrapper.ScriptableItemMap;
+import org.apache.sling.scripting.javascript.wrapper.ScriptableMap;
import org.apache.sling.scripting.javascript.wrapper.ScriptableNode;
import org.apache.sling.scripting.javascript.wrapper.ScriptablePrintWriter;
import org.apache.sling.scripting.javascript.wrapper.ScriptableProperty;
@@ -99,7 +100,7 @@
ScriptableResource.class, ScriptableNode.class,
ScriptableProperty.class, ScriptableItemMap.class,
ScriptablePrintWriter.class, ScriptableVersionHistory.class,
- ScriptableVersion.class, ScriptableCalendar.class
+ ScriptableVersion.class, ScriptableCalendar.class, ScriptableMap.class
};
/**
diff --git a/src/main/java/org/apache/sling/scripting/javascript/wrapper/ScriptableMap.java b/src/main/java/org/apache/sling/scripting/javascript/wrapper/ScriptableMap.java
new file mode 100644
index 0000000..d644e53
--- /dev/null
+++ b/src/main/java/org/apache/sling/scripting/javascript/wrapper/ScriptableMap.java
@@ -0,0 +1,88 @@
+/*
+ * 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.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.scripting.javascript.SlingWrapper;
+import org.mozilla.javascript.Scriptable;
+import org.mozilla.javascript.Undefined;
+
+/**
+ * The {@code ScriptableMap} wrapper provides easier access to a map's values by setting the map's keys as properties to the JavaScript
+ * object representing the {@link Map}.
+ */
+public class ScriptableMap extends ScriptableBase implements SlingWrapper {
+
+ public static final String CLASSNAME = "Map";
+ private static final Class<?> [] WRAPPED_CLASSES = { Map.class };
+
+ private Map<String, Object> map = new HashMap<String, Object>();
+
+ public void jsConstructor(Object map) {
+ this.map = (Map) map;
+ }
+
+ @Override
+ public Object get(String name, Scriptable start) {
+ final Object fromSuperClass = super.get(name, start);
+ if (fromSuperClass != Scriptable.NOT_FOUND) {
+ return fromSuperClass;
+ }
+
+ if (map == null) {
+ return Undefined.instance;
+ }
+
+ Object result = map.get(name);
+ if (result == null) {
+ result = getNative(name, start);
+ }
+ return result;
+ }
+
+ @Override
+ public Object getDefaultValue(Class<?> typeHint) {
+ return map;
+ }
+
+ @Override
+ protected Object getWrappedObject() {
+ return map;
+ }
+
+ @Override
+ protected Class<?> getStaticType() {
+ return Map.class;
+ }
+
+ @Override
+ public String getClassName() {
+ return CLASSNAME;
+ }
+
+ @Override
+ public Class<?>[] getWrappedClasses() {
+ return WRAPPED_CLASSES;
+ }
+
+ @Override
+ public Object unwrap() {
+ return map;
+ }
+}
diff --git a/src/main/java/org/apache/sling/scripting/javascript/wrapper/ScriptableResource.java b/src/main/java/org/apache/sling/scripting/javascript/wrapper/ScriptableResource.java
index 5b3f57c..93d64ff 100644
--- a/src/main/java/org/apache/sling/scripting/javascript/wrapper/ScriptableResource.java
+++ b/src/main/java/org/apache/sling/scripting/javascript/wrapper/ScriptableResource.java
@@ -18,6 +18,7 @@
import org.apache.commons.collections.IteratorUtils;
import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.scripting.javascript.SlingWrapper;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
@@ -51,6 +52,7 @@
* <li>[Resource[]] getChildren()</li>
* <li>[Resource[]] listChildren()</li>
* <li>[Boolean] isResourceType(String)</li>
+ * <li>[Object] properties</li>
* </ul>
*/
public class ScriptableResource extends ScriptableObject implements
@@ -63,6 +65,7 @@
private static final Class<?>[] WRAPPED_CLASSES = { Resource.class };
private Resource resource;
+ private ValueMap properties;
public ScriptableResource() {
}
@@ -307,6 +310,13 @@
return Undefined.instance;
}
+ public Object jsGet_properties() {
+ if (properties == null) {
+ properties = resource.adaptTo(ValueMap.class);
+ }
+ return properties;
+ }
+
// --------- ScriptableObject API
@Override
diff --git a/src/test/java/org/apache/sling/scripting/javascript/wrapper/ScriptableMapTest.java b/src/test/java/org/apache/sling/scripting/javascript/wrapper/ScriptableMapTest.java
new file mode 100644
index 0000000..55a49c3
--- /dev/null
+++ b/src/test/java/org/apache/sling/scripting/javascript/wrapper/ScriptableMapTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.util.HashMap;
+import javax.script.ScriptException;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.scripting.RepositoryScriptingTestBase;
+import org.apache.sling.scripting.javascript.internal.ScriptEngineHelper;
+import org.junit.After;
+import org.junit.Before;
+
+public class ScriptableMapTest extends RepositoryScriptingTestBase {
+
+ private ValueMap valueMap;
+ private ScriptEngineHelper.Data data;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ valueMap = new ValueMapDecorator(new HashMap<String, Object>() {{
+ put("a", "a");
+ put("b", 1);
+ }});
+ data = new ScriptEngineHelper.Data();
+ data.put("properties", valueMap);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ valueMap.clear();
+ data.clear();
+ super.tearDown();
+ }
+
+ public void testPropertyAccess() throws ScriptException {
+ assertEquals("a", script.eval("properties['a']", data));
+ assertEquals("a", script.eval("properties.a", data));
+ assertEquals(1, script.eval("properties['b']", data));
+ assertEquals(1, script.eval("properties.b", data));
+ assertEquals(null, script.eval("properties['c']", data));
+ }
+
+ public void testJavaMethods() throws ScriptException {
+ assertEquals(2, script.eval("properties.size()", data));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/scripting/wrapper/ScriptableResourceTest.java b/src/test/java/org/apache/sling/scripting/wrapper/ScriptableResourceTest.java
index a9959b5..225da14 100644
--- a/src/test/java/org/apache/sling/scripting/wrapper/ScriptableResourceTest.java
+++ b/src/test/java/org/apache/sling/scripting/wrapper/ScriptableResourceTest.java
@@ -19,25 +19,37 @@
package org.apache.sling.scripting.wrapper;
import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
-
+import java.util.Map;
import javax.jcr.Item;
import javax.jcr.NamespaceException;
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 org.apache.jackrabbit.JcrConstants;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceMetadata;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.sling.commons.testing.sling.MockResourceResolver;
import org.apache.sling.jcr.resource.JcrResourceConstants;
import org.apache.sling.scripting.RepositoryScriptingTestBase;
import org.apache.sling.scripting.javascript.internal.ScriptEngineHelper;
import org.mozilla.javascript.Wrapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class ScriptableResourceTest extends RepositoryScriptingTestBase {
@@ -49,6 +61,8 @@
private static final String RESOURCE_SUPER_TYPE = "testWrappedResourceSuperType";
+ private static final Logger LOGGER = LoggerFactory.getLogger(ScriptableResourceTest.class);
+
@Override
protected void setUp() throws Exception {
super.setUp();
@@ -215,6 +229,17 @@
assertEquals(true, script.eval("resource.adaptTo(Packages.java.util.Date) == undefined", data));
}
+ public void testProperties() throws Exception {
+ final ScriptEngineHelper.Data data = new ScriptEngineHelper.Data();
+ Calendar date = new GregorianCalendar();
+ node.setProperty(JcrConstants.JCR_LASTMODIFIED, date);
+ node.setProperty("test", "testProperties");
+ node.getSession().save();
+ data.put("resource", new TestResource(node));
+ assertEquals(date.getTimeInMillis(), script.eval("(resource.properties['jcr:lastModified']).getTimeInMillis()", data));
+ assertEquals("testProperties", script.eval("resource.properties.test", data));
+ }
+
private void assertEquals(Node expected, Object actual) {
while (actual instanceof Wrapper) {
actual = ((Wrapper) actual).unwrap();
@@ -271,6 +296,75 @@
public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
if (type == Node.class || type == Item.class) {
return (AdapterType) node;
+ } else if (type == ValueMap.class) {
+ try {
+
+ PropertyIterator iterator = node.getProperties();
+ Map<String, Object> properties = new HashMap<String, Object>();
+ while (iterator.hasNext()) {
+ Property prop = iterator.nextProperty();
+ if (prop.isMultiple()) {
+ Value[] values = prop.getValues();
+ Object[] array = new Object[values.length];
+ int index = 0;
+ for (Value value : values) {
+ switch (value.getType()) {
+ case PropertyType.BINARY:
+ array[index++] = value.getBinary();
+ break;
+ case PropertyType.BOOLEAN:
+ array[index++] = value.getBoolean();
+ break;
+ case PropertyType.DATE:
+ array[index++] = value.getDate();
+ break;
+ case PropertyType.DECIMAL:
+ array[index++] = value.getDecimal();
+ break;
+ case PropertyType.DOUBLE:
+ array[index++] = value.getDouble();
+ break;
+ case PropertyType.LONG:
+ array[index++] = value.getLong();
+ break;
+ default:
+ array[index++] = value.getString();
+ break;
+ }
+ }
+ properties.put(prop.getName(), array);
+
+ } else {
+ Value value = prop.getValue();
+ switch (value.getType()) {
+ case PropertyType.BINARY:
+ properties.put(prop.getName(), value.getBinary());
+ break;
+ case PropertyType.BOOLEAN:
+ properties.put(prop.getName(), value.getBoolean());
+ break;
+ case PropertyType.DATE:
+ properties.put(prop.getName(), value.getDate());
+ break;
+ case PropertyType.DECIMAL:
+ properties.put(prop.getName(), value.getDecimal());
+ break;
+ case PropertyType.DOUBLE:
+ properties.put(prop.getName(), value.getDouble());
+ break;
+ case PropertyType.LONG:
+ properties.put(prop.getName(), value.getLong());
+ break;
+ default:
+ properties.put(prop.getName(), value.getString());
+ break;
+ }
+ }
+ }
+ return ((AdapterType) (new ValueMapDecorator(properties)));
+ } catch (RepositoryException e) {
+ LOGGER.error("Unable to adapt resource " + getPath() + " to a ValueMap.", e);
+ }
}
return null;