SLING-6183 - add Sling Model Exporter feature
authorJustin Edelson <justin@apache.org>
Fri, 28 Oct 2016 15:02:13 +0000 (15:02 +0000)
committerJustin Edelson <justin@apache.org>
Fri, 28 Oct 2016 15:02:13 +0000 (15:02 +0000)
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1767030 13f79535-47bb-0310-9956-ffa450edef68

src/main/java/org/apache/sling/models/impl/ExportServlet.java [new file with mode: 0644]
src/main/java/org/apache/sling/models/impl/ModelAdapterFactory.java
src/main/java/org/apache/sling/models/impl/ModelPackageBundleListener.java

diff --git a/src/main/java/org/apache/sling/models/impl/ExportServlet.java b/src/main/java/org/apache/sling/models/impl/ExportServlet.java
new file mode 100644 (file)
index 0000000..df412b9
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * 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.models.impl;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
+import org.apache.sling.models.factory.ExportException;
+import org.apache.sling.models.factory.MissingExporterException;
+import org.apache.sling.models.factory.ModelFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressWarnings("serial")
+class ExportServlet extends SlingSafeMethodsServlet {
+
+    private static final Logger logger = LoggerFactory.getLogger(ExportServlet.class);
+
+    private final String exporterName;
+    private final String registeredSelector;
+    private final ModelFactory modelFactory;
+    private final ExportedObjectAccessor accessor;
+    private final Map<String, String> baseOptions;
+
+    public ExportServlet(ModelFactory modelFactory, String registeredSelector, String exporterName, ExportedObjectAccessor accessor,
+                         Map<String, String> baseOptions) {
+        this.modelFactory = modelFactory;
+        this.registeredSelector = registeredSelector;
+        this.exporterName = exporterName;
+        this.accessor = accessor;
+        this.baseOptions = baseOptions;
+    }
+
+    @Override
+    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
+            throws ServletException, IOException {
+        Map<String, String> options = createOptionMap(request);
+
+        String exported;
+        try {
+            exported = accessor.getExportedString(request, options, modelFactory, exporterName);
+        } catch (ExportException e) {
+            logger.error("Could not get serializer requested by model.");
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            return;
+        } catch (MissingExporterException e) {
+            logger.error("Could not get serialize model to JSON.", e);
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            return;
+        }
+        if (exported == null) {
+            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+        response.setContentType(request.getResponseContentType());
+        response.getWriter().write(exported);
+    }
+
+    private Map<String, String> createOptionMap(SlingHttpServletRequest request) {
+        Map<String, String[]> parameterMap = request.getParameterMap();
+        String[] selectors = request.getRequestPathInfo().getSelectors();
+        Map<String, String> result = new HashMap<String, String>(baseOptions.size() + parameterMap.size() + selectors.length - 1);
+        result.putAll(baseOptions);
+        for (String selector : selectors) {
+            if (!selector.equals(registeredSelector)) {
+                result.put(selector, "true");
+            }
+        }
+
+        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
+            if (entry.getValue().length == 0) {
+                result.put(entry.getKey(), Boolean.TRUE.toString());
+            } else {
+                result.put(entry.getKey(), entry.getValue()[0]);
+            }
+        }
+        return result;
+    }
+
+    public interface ExportedObjectAccessor {
+        String getExportedString(SlingHttpServletRequest request, Map<String, String> options, ModelFactory modelFactory, String exporterName) throws ExportException, MissingExporterException;
+    }
+
+    public static final ExportedObjectAccessor RESOURCE = new ExportedObjectAccessor() {
+        @Override
+        public String getExportedString(SlingHttpServletRequest request, Map<String, String> options, ModelFactory modelFactory, String exporterName) throws ExportException, MissingExporterException {
+            return modelFactory.exportModelForResource(request.getResource(), exporterName, String.class, options);
+        }
+    };
+
+    public static final ExportedObjectAccessor REQUEST = new ExportedObjectAccessor() {
+        @Override
+        public String getExportedString(SlingHttpServletRequest request, Map<String, String> options, ModelFactory modelFactory, String exporterName) throws ExportException, MissingExporterException {
+            return modelFactory.exportModelForRequest(request, exporterName, String.class, options);
+        }
+    };
+
+}
index 4003ceb..d7de3f5 100644 (file)
@@ -62,10 +62,13 @@ import org.apache.sling.commons.osgi.PropertiesUtil;
 import org.apache.sling.commons.osgi.RankedServices;
 import org.apache.sling.models.annotations.Model;
 import org.apache.sling.models.annotations.ValidationStrategy;
+import org.apache.sling.models.export.spi.ModelExporter;
+import org.apache.sling.models.factory.ExportException;
 import org.apache.sling.models.factory.InvalidAdaptableException;
 import org.apache.sling.models.factory.InvalidModelException;
 import org.apache.sling.models.factory.MissingElementException;
 import org.apache.sling.models.factory.MissingElementsException;
+import org.apache.sling.models.factory.MissingExporterException;
 import org.apache.sling.models.factory.ModelClassException;
 import org.apache.sling.models.factory.ModelFactory;
 import org.apache.sling.models.factory.PostConstructException;
@@ -173,6 +176,10 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
     @Reference(cardinality=ReferenceCardinality.OPTIONAL_UNARY, policyOption=ReferencePolicyOption.GREEDY)
     private ModelValidation modelValidation = null;
 
+    @Reference(name = "modelExporter", cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC,
+            referenceInterface = ModelExporter.class)
+    private final @Nonnull RankedServices<ModelExporter> modelExporters = new RankedServices<ModelExporter>();
+
     ModelPackageBundleListener listener;
 
     final AdapterImplementations adapterImplementations = new AdapterImplementations();
@@ -1010,6 +1017,18 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
         }
     }
 
+    protected void bindModelExporter(final ModelExporter s, final Map<String, Object> props) {
+        synchronized (modelExporters) {
+            modelExporters.bind(s, props);
+        }
+    }
+
+    protected void unbindModelExporter(final ModelExporter s, final Map<String, Object> props) {
+        synchronized (modelExporters) {
+            modelExporters.unbind(s, props);
+        }
+    }
+
     @Nonnull Collection<Injector> getInjectors() {
         return sortedInjectors.get();
     }
@@ -1066,4 +1085,58 @@ public class ModelAdapterFactory implements AdapterFactory, Runnable, ModelFacto
         }
     }
 
+    @Override
+    public <T> T exportModel(Object model, String name, Class<T> targetClass, Map<String, String> options)
+            throws ExportException, MissingExporterException {
+        for (ModelExporter exporter : modelExporters) {
+            if (exporter.getName().equals(name) && exporter.isSupported(targetClass)) {
+                T resultObject = exporter.export(model, targetClass, options);
+                return resultObject;
+            } else {
+                throw new MissingExporterException(name, targetClass);
+            }
+        }
+        throw new MissingExporterException(name, targetClass);
+    }
+
+    @Override
+    public <T> T exportModelForResource(Resource resource, String name, Class<T> targetClass, Map<String, String> options)
+            throws ExportException, MissingExporterException {
+        Class<?> clazz = this.adapterImplementations.getModelClassForResource(resource);
+        if (clazz == null) {
+            throw new ModelClassException("Could find model registered for resource type: " + resource.getResourceType());
+        }
+        Result<?> result = internalCreateModel(resource, clazz);
+        return handleAndExportResult(result, name, targetClass, options);
+    }
+
+    @Override
+    public <T> T exportModelForRequest(SlingHttpServletRequest request, String name, Class<T> targetClass, Map<String, String> options)
+            throws ExportException, MissingExporterException {
+        Class<?> clazz = this.adapterImplementations.getModelClassForRequest(request);
+        if (clazz == null) {
+            throw new ModelClassException("Could find model registered for request path: " + request.getServletPath());
+        }
+        Result<?> result = internalCreateModel(request, clazz);
+        handleAndExportResult(result, name, targetClass, options);
+        // unreachable
+        return null;
+    }
+
+    private <T> T handleAndExportResult(Result<?> result, String name, Class<T> targetClass, Map<String, String> options) throws ExportException, MissingExporterException {
+        if (result.wasSuccessful()) {
+            for (ModelExporter exporter : modelExporters) {
+                if (exporter.getName().equals(name) && exporter.isSupported(targetClass)) {
+                    T resultObject = exporter.export(result.getValue(), targetClass, options);
+                    return resultObject;
+                } else {
+                    throw new MissingExporterException(name, targetClass);
+                }
+            }
+            throw new MissingExporterException(name, targetClass);
+        } else {
+            throw result.getThrowable();
+        }
+    }
+
 }
index 3345f14..78e4bed 100644 (file)
@@ -18,14 +18,22 @@ package org.apache.sling.models.impl;
 
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Dictionary;
 import java.util.Enumeration;
+import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.commons.lang.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.api.adapter.AdapterFactory;
+import org.apache.sling.api.resource.Resource;
 import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.models.annotations.Exporter;
+import org.apache.sling.models.annotations.ExporterOption;
+import org.apache.sling.models.annotations.Exporters;
 import org.apache.sling.models.annotations.Model;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
@@ -37,6 +45,8 @@ import org.osgi.util.tracker.BundleTrackerCustomizer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.servlet.Servlet;
+
 public class ModelPackageBundleListener implements BundleTrackerCustomizer {
 
     static final String HEADER = "Sling-Model-Packages";
@@ -117,6 +127,23 @@ public class ModelPackageBundleListener implements BundleTrackerCustomizer {
                                 if (StringUtils.isNotEmpty(resourceType)) {
                                     for (Class<?> adaptable : annotation.adaptables()) {
                                         adapterImplementations.registerModelToResourceType(bundle, resourceType, adaptable, implType);
+                                        ExportServlet.ExportedObjectAccessor accessor = null;
+                                        if (adaptable == Resource.class) {
+                                            accessor = ExportServlet.RESOURCE;
+                                        } else if (adaptable == SlingHttpServletRequest.class) {
+                                            accessor = ExportServlet.REQUEST;
+                                        }
+                                        Exporter exporterAnnotation = implType.getAnnotation(Exporter.class);
+                                        if (exporterAnnotation != null) {
+                                            registerExporter(bundle, implType, resourceType, exporterAnnotation, regs, accessor);
+                                        }
+                                        Exporters exportersAnnotation = implType.getAnnotation(Exporters.class);
+                                        if (exportersAnnotation != null) {
+                                            for (Exporter ann : exportersAnnotation.value()) {
+                                                registerExporter(bundle, implType, resourceType, ann, regs, accessor);
+                                            }
+                                        }
+
                                     }
                                 }
                             }
@@ -142,9 +169,11 @@ public class ModelPackageBundleListener implements BundleTrackerCustomizer {
             for (ServiceRegistration reg : (ServiceRegistration[]) object) {
                 ServiceReference ref = reg.getReference();
                 String[] adapterTypeNames = PropertiesUtil.toStringArray(ref.getProperty(AdapterFactory.ADAPTER_CLASSES));
-                String implTypeName = PropertiesUtil.toString(ref.getProperty(PROP_IMPLEMENTATION_CLASS), null);
-                for (String adapterTypeName : adapterTypeNames) {
-                    adapterImplementations.remove(adapterTypeName, implTypeName);
+                if (adapterTypeNames != null) {
+                    String implTypeName = PropertiesUtil.toString(ref.getProperty(PROP_IMPLEMENTATION_CLASS), null);
+                    for (String adapterTypeName : adapterTypeNames) {
+                        adapterImplementations.remove(adapterTypeName, implTypeName);
+                    }
                 }
                 reg.unregister();
             }
@@ -210,5 +239,36 @@ public class ModelPackageBundleListener implements BundleTrackerCustomizer {
         }
         return bundleContext.registerService(AdapterFactory.SERVICE_NAME, factory, registrationProps);
     }
-    
+
+
+    private void registerExporter(Bundle bundle, Class<?> implType, String resourceType, Exporter exporterAnnotation, List<ServiceRegistration> regs,
+                                  ExportServlet.ExportedObjectAccessor accessor) {
+        if (accessor != null) {
+            Map<String, String> baseOptions = getOptions(exporterAnnotation);
+            ExportServlet servlet = new ExportServlet(factory, exporterAnnotation.selector(), exporterAnnotation.name(), accessor, baseOptions);
+            Dictionary<String, Object> registrationProps = new Hashtable<String, Object>();
+            registrationProps.put("sling.servlet.resourceTypes", resourceType);
+            registrationProps.put("sling.servlet.selectors", exporterAnnotation.selector());
+            registrationProps.put("sling.servlet.extensions", exporterAnnotation.extensions());
+
+            log.info("registering servlet for {}, {}, {}", new Object[]{resourceType, exporterAnnotation.selector(), exporterAnnotation.extensions()});
+
+            ServiceRegistration reg = bundleContext.registerService(Servlet.class.getName(), servlet, registrationProps);
+            regs.add(reg);
+        }
+    }
+
+    private Map<String, String> getOptions(Exporter annotation) {
+        ExporterOption[] options = annotation.options();
+        if (options.length == 0) {
+            return Collections.emptyMap();
+        } else {
+            Map<String, String> map = new HashMap<String, String>(options.length);
+            for (ExporterOption option : options) {
+                map.put(option.name(), option.value());
+            }
+            return map;
+        }
+    }
+
 }