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;