You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by bd...@apache.org on 2018/06/21 12:33:22 UTC

[sling-org-apache-sling-capabilities] 01/01: Initial commit, moving from sling-whiteboard

This is an automated email from the ASF dual-hosted git repository.

bdelacretaz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-capabilities.git

commit e50900fb113e36303023aae73e4fb97f13689823
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Thu Jun 21 14:25:30 2018 +0200

    Initial commit, moving from sling-whiteboard
---
 .gitignore                                         |   8 ++
 README.md                                          |  48 ++++++++
 pom.xml                                            | 124 +++++++++++++++++++++
 .../sling/capabilities/CapabilitiesSource.java     |  43 +++++++
 .../capabilities/internal/CapabilitiesServlet.java |  61 ++++++++++
 .../internal/JSONCapabilitiesWriter.java           |  61 ++++++++++
 .../internal/CapabilitesServletTest.java           |  78 +++++++++++++
 .../internal/JSONCapabilitiesWriterTest.java       |  76 +++++++++++++
 .../sling/capabilities/internal/MockSource.java    |  51 +++++++++
 9 files changed, 550 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b018844
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.project
+.classpath
+.settings
+target
+bin
+*.iml
+.idea
+.DS_Store
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b84cb65
--- /dev/null
+++ b/README.md
@@ -0,0 +1,48 @@
+Sling Capabilities Module
+=========================
+
+The servlet provided by this module allows for creating Capabilities HTTP endpoints
+on a Sling instance: Resources that provide information on which services are available,
+version levels etc.
+
+For now, a single type of endpoint is provided: all Resources which have the
+`sling/capabilities` resource type will return the same set of Capabilities, generated
+by aggregating the output of all active `CapabilitiesSource` services.
+
+This can be easily expanded to multiple sets of Capabilities if needed later on,
+by changing the code to use service properties to group or tag the `CapabilitiesSource` services.
+
+The tests provide simple `CapabilitiesSource` examples, that API is as follows:
+
+    @ProviderType
+    public interface CapabilitiesSource {
+
+        /** @return the namespace to use to group our capabilities.
+         *  That name must be unique in a given Sling instance.
+         */
+        String getNamespace();
+
+        /** @return zero to N capabilities, each being represented by
+         *      a key/value pair.
+         * @throws Exception if the capabilities could not be computed.
+         */
+        Map<String, Object> getCapabilities() throws Exception;
+    }
+
+The `CapabilitiesServlet` produces output as in the example below, where two
+`CapabilitiesSource` services are available:
+
+    $ curl -s -u admin:admin http://localhost:8080/tmp/cap.json | jq .
+    {
+      "org.apache.sling.capabilities": {
+        "org.apache.sling.capabilities.internal.OsgiFrameworkCapabilitiesSource": {
+          "framework.bundle.symbolic.name": "org.apache.felix.framework",
+          "framework.bundle.version": "5.6.10"
+        },
+        "org.apache.sling.capabilities.internal.JvmCapabilitiesSource": {
+          "java.specification.version": "1.8",
+          "java.vm.version": "25.171-b11",
+          "java.vm.vendor": "Oracle Corporation"
+        }
+      }
+    }
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..c566b9e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  ~ 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.
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <parent>
+    <artifactId>sling</artifactId>
+    <groupId>org.apache.sling</groupId>
+    <version>30</version>
+    <relativePath/>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+  <name>Apache Sling Capabilities</name>
+  <artifactId>org.apache.sling.capabilities</artifactId>
+  <version>0.0.1-SNAPSHOT</version>
+  <packaging>bundle</packaging>
+  <properties>
+    <!-- This module can work with older versions of Sling
+         as well, so be conservative here -->
+    <sling.java.version>7</sling.java.version>
+  </properties>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-scr-plugin</artifactId>
+      </plugin>  
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <extensions>true</extensions>
+        <configuration>
+          <instructions>
+            <Embed-Dependency>
+              org.apache.felix.utils;inline=org/apache/felix/utils/json/JSONWriter.class
+            </Embed-Dependency>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>biz.aQute.bnd</groupId>
+      <artifactId>biz.aQute.bndlib</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.api</artifactId>
+      <version>2.16.4</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.scr.annotations</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.utils</artifactId>
+      <version>1.9.0</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>osgi.core</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>osgi.cmpn</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <!-- TESTING -->
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.commons.johnzon</artifactId>
+      <version>1.0.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.testing.osgi-mock</artifactId>
+      <version>2.3.8</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.testing.sling-mock</artifactId>
+      <version>2.2.18</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/capabilities/CapabilitiesSource.java b/src/main/java/org/apache/sling/capabilities/CapabilitiesSource.java
new file mode 100644
index 0000000..e6ec9d5
--- /dev/null
+++ b/src/main/java/org/apache/sling/capabilities/CapabilitiesSource.java
@@ -0,0 +1,43 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.capabilities;
+
+import java.util.Map;
+import org.osgi.annotation.versioning.ProviderType;
+
+/** A CapabilitiesSource provides capabilities, as a Map of key/value
+ *  pairs. 
+ *  Various types of CapabilitiesSources are meant
+ *  to be implemented, using Health Checks, OSGi environment status
+ *  or any suitable input to find out which capabilities are present.
+ */
+@ProviderType
+public interface CapabilitiesSource {
+    
+    /** @return the namespace to use to group our capabilities.
+     *  That name must be unique in a given Sling instance.
+     */
+    String getNamespace();
+    
+    /** @return zero to N capabilities, each being represented by
+     *      a key/value pair.
+     * @throws Exception if the capabilities could not be computed.
+     */
+    Map<String, Object> getCapabilities() throws Exception;
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/capabilities/internal/CapabilitiesServlet.java b/src/main/java/org/apache/sling/capabilities/internal/CapabilitiesServlet.java
new file mode 100644
index 0000000..35e2819
--- /dev/null
+++ b/src/main/java/org/apache/sling/capabilities/internal/CapabilitiesServlet.java
@@ -0,0 +1,61 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.capabilities.internal;
+
+import java.io.IOException;
+import java.util.List;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
+import org.apache.sling.capabilities.CapabilitiesSource;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+
+@Component(service = Servlet.class,
+property = {
+    "sling.servlet.resourceTypes=sling/capabilities",
+    "sling.servlet.methods=GET",
+    "sling.servlet.selectors=capabilities",
+    "sling.servlet.extensions=json"
+})
+
+public class CapabilitiesServlet extends SlingSafeMethodsServlet {
+    
+    @Reference(
+        policy=ReferencePolicy.DYNAMIC,
+        cardinality=ReferenceCardinality.AT_LEAST_ONE, 
+        policyOption=ReferencePolicyOption.GREEDY)
+    volatile List<CapabilitiesSource> sources;
+
+    CapabilitiesServlet() {
+    }
+    
+    @Override
+    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
+        response.setContentType("application/json");
+        response.setCharacterEncoding("UTF-8");
+        new JSONCapabilitiesWriter().writeJson(response.getWriter(), sources);
+        response.getWriter().flush();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/capabilities/internal/JSONCapabilitiesWriter.java b/src/main/java/org/apache/sling/capabilities/internal/JSONCapabilitiesWriter.java
new file mode 100644
index 0000000..3552941
--- /dev/null
+++ b/src/main/java/org/apache/sling/capabilities/internal/JSONCapabilitiesWriter.java
@@ -0,0 +1,61 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.capabilities.internal;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.felix.utils.json.JSONWriter;
+import org.apache.sling.capabilities.CapabilitiesSource;
+
+/** Create the JSON output of our servlet */
+class JSONCapabilitiesWriter {
+
+    static final String CAPS_KEY = "org.apache.sling.capabilities";
+    
+    /** Write JSON to the supplied Writer, using the supplied sources */
+    void writeJson(Writer w, Collection<CapabilitiesSource> sources) throws IOException {
+        final JSONWriter jw = new JSONWriter(w);
+        jw.object();
+        jw.key(CAPS_KEY);
+        jw.object();
+        
+        Map<String, Object> values = null;
+        for(CapabilitiesSource s : sources) {
+            try {
+                values = s.getCapabilities();
+            } catch(Exception e) {
+                values = new HashMap<>();
+                values.put("_EXCEPTION_", e.getClass().getName() + ":" + e.getMessage());
+            }
+            jw.key(s.getNamespace());
+            jw.object();
+            for(Map.Entry<String, Object> e : values.entrySet()) {
+                jw.key(e.getKey());
+                jw.value(e.getValue());
+            }
+            jw.endObject();
+        }
+        
+        jw.endObject();
+        jw.endObject();
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/capabilities/internal/CapabilitesServletTest.java b/src/test/java/org/apache/sling/capabilities/internal/CapabilitesServletTest.java
new file mode 100644
index 0000000..80017e0
--- /dev/null
+++ b/src/test/java/org/apache/sling/capabilities/internal/CapabilitesServletTest.java
@@ -0,0 +1,78 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.capabilities.internal;
+
+import java.io.IOException;
+import java.io.StringReader;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.servlet.ServletException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
+import org.apache.sling.capabilities.CapabilitiesSource;
+import org.apache.sling.servlethelpers.MockSlingHttpServletRequest;
+import org.apache.sling.servlethelpers.MockSlingHttpServletResponse;
+import org.apache.sling.testing.mock.osgi.MockOsgi;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.apache.sling.testing.mock.sling.MockSling;
+import static org.junit.Assert.assertEquals;
+import org.osgi.framework.BundleContext;
+
+/** Test the JSONCapabilitiesWriter */
+public class CapabilitesServletTest {
+
+    private SlingSafeMethodsServlet servlet;
+
+    @Rule
+    public final OsgiContext context = new OsgiContext();
+    
+    private BundleContext bundleContext;
+    
+    @Before
+    public void setup() {
+        servlet = new CapabilitiesServlet();
+        bundleContext = MockOsgi.newBundleContext();
+        
+        bundleContext.registerService(CapabilitiesSource.class.getName(), new MockSource("F", 2), null);
+        bundleContext.registerService(CapabilitiesSource.class.getName(), new MockSource("G", 43), null);
+        
+        MockOsgi.injectServices(servlet, bundleContext);
+    }
+    
+    @Test
+    public void testServlet() throws ServletException, IOException {
+        final ResourceResolver resolver = MockSling.newResourceResolver(bundleContext);
+        MockSlingHttpServletRequest req = new MockSlingHttpServletRequest(resolver);
+        MockSlingHttpServletResponse resp = new MockSlingHttpServletResponse();
+        
+        servlet.service(req, resp);
+
+        // Just verify that both sources are taken into account
+        // the JSON format details are tested elsewhere
+        final JsonReader r = Json.createReader(new StringReader(resp.getOutputAsString()));
+        final JsonObject rootJson = r.readObject();
+        final JsonObject json = rootJson.getJsonObject(JSONCapabilitiesWriter.CAPS_KEY);
+        assertEquals("VALUE_1_F", json.getJsonObject("F").getString("KEY_1_F"));
+        assertEquals("VALUE_42_G", json.getJsonObject("G").getString("KEY_42_G"));
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/capabilities/internal/JSONCapabilitiesWriterTest.java b/src/test/java/org/apache/sling/capabilities/internal/JSONCapabilitiesWriterTest.java
new file mode 100644
index 0000000..dd7227a
--- /dev/null
+++ b/src/test/java/org/apache/sling/capabilities/internal/JSONCapabilitiesWriterTest.java
@@ -0,0 +1,76 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.capabilities.internal;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import org.apache.sling.capabilities.CapabilitiesSource;
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+/** Test the JSONCapabilitiesWriter */
+public class JSONCapabilitiesWriterTest {
+
+    @Test
+    public void testWithTwoSources() throws IOException {
+        final List<CapabilitiesSource> sources = new ArrayList<>();
+        sources.add(new MockSource("A", 2));
+        sources.add(new MockSource("B", 1));
+        
+        final StringWriter w = new StringWriter();
+        new JSONCapabilitiesWriter().writeJson(w, sources);
+        
+        final JsonReader r = Json.createReader(new StringReader(w.toString()));
+        final JsonObject rootJson = r.readObject();
+        final JsonObject json = rootJson.getJsonObject(JSONCapabilitiesWriter.CAPS_KEY);
+        assertEquals("VALUE_0_A", json.getJsonObject("A").getString("KEY_0_A"));
+        assertEquals("VALUE_1_A", json.getJsonObject("A").getString("KEY_1_A"));
+        assertEquals("VALUE_0_B", json.getJsonObject("B").getString("KEY_0_B"));
+        
+        assertEquals("Expecting 1 root key", 1, rootJson.keySet().size());
+        assertEquals("Expecting 2 keys at A", 2, json.getJsonObject("A").keySet().size());
+        assertEquals("Expecting 1 key at B", 1, json.getJsonObject("B").keySet().size());
+    }
+
+    @Test
+    public void testWithException() throws IOException {
+        final List<CapabilitiesSource> sources = new ArrayList<>();
+        sources.add(new MockSource("A", 1));
+        sources.add(new MockSource("EXCEPTION", 2));
+        sources.add(new MockSource("B", 1));
+        
+        final StringWriter w = new StringWriter();
+        new JSONCapabilitiesWriter().writeJson(w, sources);
+        
+        final JsonReader r = Json.createReader(new StringReader(w.toString()));
+        final JsonObject rootJson = r.readObject();
+        final JsonObject json = rootJson.getJsonObject(JSONCapabilitiesWriter.CAPS_KEY);
+        assertEquals("VALUE_0_A", json.getJsonObject("A").getString("KEY_0_A"));
+        assertEquals("java.lang.IllegalArgumentException:Simulating a problem", json.getJsonObject("EXCEPTION").getString("_EXCEPTION_"));
+        assertEquals("VALUE_0_B", json.getJsonObject("B").getString("KEY_0_B"));
+        
+        assertEquals("Expecting 1 key at EXCEPTION", 1, json.getJsonObject("EXCEPTION").keySet().size());
+   }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/capabilities/internal/MockSource.java b/src/test/java/org/apache/sling/capabilities/internal/MockSource.java
new file mode 100644
index 0000000..50b8652
--- /dev/null
+++ b/src/test/java/org/apache/sling/capabilities/internal/MockSource.java
@@ -0,0 +1,51 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.capabilities.internal;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.sling.capabilities.CapabilitiesSource;
+
+class MockSource implements CapabilitiesSource {
+
+    private final String namespace;
+    private final Map<String, Object> props = new HashMap<>();
+
+    MockSource(String namespace, int propsCount) {
+        this.namespace = namespace;
+        for (int i = 0; i < propsCount; i++) {
+            props.put("KEY_" + i + "_" + namespace, "VALUE_" + i + "_" + namespace);
+        }
+    }
+
+    @Override
+    public Map<String, Object> getCapabilities() throws Exception {
+        if (namespace.contains("EXCEPTION")) {
+            throw new IllegalArgumentException("Simulating a problem");
+        }
+        return Collections.unmodifiableMap(props);
+    }
+
+    @Override
+    public String getNamespace() {
+        return namespace;
+    }
+
+}