You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 09:55:39 UTC

[sling-org-apache-sling-models-jacksonexporter] 03/07: SLING-6295 - provide custom Jackson serialization mechanism for Resource objects

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

rombert pushed a commit to annotated tag org.apache.sling.models.jacksonexporter-1.0.2
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-models-jacksonexporter.git

commit 82f5c85d459de780a2500da4ee08ded3d90d0059
Author: Justin Edelson <ju...@apache.org>
AuthorDate: Thu Nov 17 01:56:59 2016 +0000

    SLING-6295 - provide custom Jackson serialization mechanism for Resource objects
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/models/jackson-exporter@1770102 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |  13 ++
 .../models/jacksonexporter/ModuleProvider.java     |  30 +++
 .../jacksonexporter/impl/JacksonExporter.java      |  25 +++
 .../impl/ResourceModelProvider.java                |  64 +++++++
 .../jacksonexporter/impl/ResourceSerializer.java   | 211 +++++++++++++++++++++
 .../sling/models/jacksonexporter/package-info.java |  21 ++
 6 files changed, 364 insertions(+)

diff --git a/pom.xml b/pom.xml
index ae2473b..c1a9287 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,6 +42,7 @@
                 <configuration>
                     <instructions>
                         <Embed-Dependency>*;scope=compile</Embed-Dependency>
+                        <Conditional-Package>org.apache.sling.commons.osgi</Conditional-Package>
                     </instructions>
                 </configuration>
             </plugin>
@@ -102,6 +103,18 @@
             <artifactId>commons-lang</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.4.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.osgi</artifactId>
+            <version>2.4.0</version>
+            <scope>provided</scope>
+        </dependency>
         <!-- *************************************************************** -->
         <!-- JACKSON -->
         <!-- *************************************************************** -->
diff --git a/src/main/java/org/apache/sling/models/jacksonexporter/ModuleProvider.java b/src/main/java/org/apache/sling/models/jacksonexporter/ModuleProvider.java
new file mode 100644
index 0000000..01d9b0a
--- /dev/null
+++ b/src/main/java/org/apache/sling/models/jacksonexporter/ModuleProvider.java
@@ -0,0 +1,30 @@
+/*
+ * 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.jacksonexporter;
+
+import aQute.bnd.annotation.ConsumerType;
+import com.fasterxml.jackson.databind.Module;
+
+/**
+ * Extension interface which allows for plugging in Jackson Modules
+ * into the Jackson Exporter
+ */
+@ConsumerType
+public interface ModuleProvider {
+
+    Module getModule();
+}
diff --git a/src/main/java/org/apache/sling/models/jacksonexporter/impl/JacksonExporter.java b/src/main/java/org/apache/sling/models/jacksonexporter/impl/JacksonExporter.java
index 71492e1..8c149ad 100644
--- a/src/main/java/org/apache/sling/models/jacksonexporter/impl/JacksonExporter.java
+++ b/src/main/java/org/apache/sling/models/jacksonexporter/impl/JacksonExporter.java
@@ -25,7 +25,12 @@ import java.util.Map;
 import com.fasterxml.jackson.databind.MapperFeature;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
 import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.osgi.Order;
+import org.apache.sling.commons.osgi.RankedServices;
 import org.apache.sling.models.export.spi.ModelExporter;
 import org.apache.sling.models.factory.ExportException;
 
@@ -35,9 +40,14 @@ import com.fasterxml.jackson.core.SerializableString;
 import com.fasterxml.jackson.core.io.CharacterEscapes;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
+import org.apache.sling.models.jacksonexporter.ModuleProvider;
+import org.apache.sling.models.spi.Injector;
+import org.apache.sling.models.spi.injectorspecific.InjectAnnotationProcessorFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.annotation.Nonnull;
+
 @Component
 @Service
 public class JacksonExporter implements ModelExporter {
@@ -52,6 +62,10 @@ public class JacksonExporter implements ModelExporter {
 
     private static final int MAPPER_FEATURE_PREFIX_LENGTH = MAPPER_FEATURE_PREFIX.length();
 
+    @Reference(name = "moduleProvider", referenceInterface = ModuleProvider.class,
+            cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+    private final @Nonnull RankedServices<ModuleProvider> moduleProviders = new RankedServices<ModuleProvider>(Order.ASCENDING);
+
     @Override
     public boolean isSupported(Class<?> clazz) {
         return clazz.equals(String.class) || clazz.equals(Map.class);
@@ -81,6 +95,9 @@ public class JacksonExporter implements ModelExporter {
                 }
             }
         }
+        for (ModuleProvider moduleProvider : moduleProviders) {
+            mapper.registerModule(moduleProvider.getModule());
+        }
 
         if (clazz.equals(Map.class)) {
             return (T) mapper.convertValue(model, Map.class);
@@ -111,6 +128,14 @@ public class JacksonExporter implements ModelExporter {
         }
     }
 
+    protected void bindModuleProvider(final ModuleProvider moduleProvider, final Map<String, Object> props) {
+        moduleProviders.bind(moduleProvider, props);
+    }
+
+    protected void unbindModuleProvider(final ModuleProvider moduleProvider, final Map<String, Object> props) {
+        moduleProviders.unbind(moduleProvider, props);
+    }
+
     @Override
     public String getName() {
         return "jackson";
diff --git a/src/main/java/org/apache/sling/models/jacksonexporter/impl/ResourceModelProvider.java b/src/main/java/org/apache/sling/models/jacksonexporter/impl/ResourceModelProvider.java
new file mode 100644
index 0000000..43fa303
--- /dev/null
+++ b/src/main/java/org/apache/sling/models/jacksonexporter/impl/ResourceModelProvider.java
@@ -0,0 +1,64 @@
+/*
+ * 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.jacksonexporter.impl;
+
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.module.SimpleSerializers;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.models.jacksonexporter.ModuleProvider;
+import org.osgi.framework.Constants;
+
+import java.util.Map;
+
+@Component(metatype = true, label = "Apache Sling Models Jackson Exporter - Resource object support",
+    description = "Provider of a Jackson Module which enables support for proper serialization of Resource objects")
+@Service
+@Property(name = Constants.SERVICE_RANKING, intValue = 0, propertyPrivate = true)
+public class ResourceModelProvider implements ModuleProvider {
+
+    private static final int DEFAULT_MAX_RECURSION_LEVELS = -1;
+
+    @Property(label = "Maximum Recursion Levels",
+            description = "Maximum number of levels of child resources which will be exported for each resource. Specify -1 for infinite.",
+            intValue = DEFAULT_MAX_RECURSION_LEVELS)
+    private static final String PROP_MAX_RECURSION_LEVELS = "max.recursion.levels";
+
+    private int maxRecursionLevels;
+    private SimpleModule moduleInstance;
+
+    @Activate
+    private void activate(Map<String, Object> props) {
+        this.maxRecursionLevels = PropertiesUtil.toInteger(props.get(PROP_MAX_RECURSION_LEVELS), DEFAULT_MAX_RECURSION_LEVELS);
+        this.moduleInstance = new SimpleModule();
+        SimpleSerializers serializers = new SimpleSerializers();
+        serializers.addSerializer(Resource.class, new ResourceSerializer(maxRecursionLevels));
+        moduleInstance.setSerializers(serializers);
+
+    }
+
+    @Override
+    public Module getModule() {
+        return moduleInstance;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/models/jacksonexporter/impl/ResourceSerializer.java b/src/main/java/org/apache/sling/models/jacksonexporter/impl/ResourceSerializer.java
new file mode 100644
index 0000000..72bab6a
--- /dev/null
+++ b/src/main/java/org/apache/sling/models/jacksonexporter/impl/ResourceSerializer.java
@@ -0,0 +1,211 @@
+/*
+ * 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.jacksonexporter.impl;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.ResolvableSerializer;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.Map;
+
+import static javax.xml.bind.JAXBIntrospector.getValue;
+
+public class ResourceSerializer extends JsonSerializer<Resource> implements ResolvableSerializer {
+
+    private final int maxRecursionLevels;
+    private JsonSerializer<Object> calendarSerializer;
+
+    public ResourceSerializer(int maxRecursionLevels) {
+        this.maxRecursionLevels = maxRecursionLevels;
+    }
+
+    @Override
+    public void serialize(final Resource value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
+        create(value, jgen, 0, provider);
+    }
+
+    /** Dump given resource in JSON, optionally recursing into its objects */
+    private void create(final Resource resource, final JsonGenerator jgen, final int currentRecursionLevel,
+                                     final SerializerProvider provider) throws IOException {
+        jgen.writeStartObject();
+
+        final ValueMap valueMap = resource.adaptTo(ValueMap.class);
+
+        final Map propertyMap = (valueMap != null) ? valueMap : resource.adaptTo(Map.class);
+
+        if (propertyMap == null) {
+
+            // no map available, try string
+            final String value = resource.adaptTo(String.class);
+            if (value != null) {
+
+                // single value property or just plain String resource or...
+                jgen.writeStringField(resource.getName(), value);
+
+            } else {
+
+                // Try multi-value "property"
+                final String[] values = resource.adaptTo(String[].class);
+                if (values != null) {
+                    jgen.writeArrayFieldStart(resource.getName());
+                    for (final String s : values) {
+                        jgen.writeString(s);
+                    }
+                    jgen.writeEndArray();
+                }
+
+            }
+
+        } else {
+
+            @SuppressWarnings("unchecked")
+            final Iterator<Map.Entry> props = propertyMap.entrySet().iterator();
+
+            // the node's actual properties
+            while (props.hasNext()) {
+                final Map.Entry prop = props.next();
+
+                if (prop.getValue() != null) {
+                    createProperty(jgen, valueMap, prop.getKey().toString(), prop.getValue(), provider);
+                }
+            }
+        }
+
+        // the child nodes
+        if (recursionLevelActive(currentRecursionLevel)) {
+            for (final Resource n : resource.getChildren()) {
+                jgen.writeObjectFieldStart(n.getName());
+                create(n, jgen, currentRecursionLevel + 1, provider);
+            }
+        }
+
+        jgen.writeEndObject();
+    }
+
+    /**
+     * Write a single property
+     */
+    private void createProperty(final JsonGenerator jgen, final ValueMap valueMap, final String key, final Object value,
+                                final SerializerProvider provider)
+            throws IOException {
+        Object[] values = null;
+        if (value.getClass().isArray()) {
+            final int length = Array.getLength(value);
+            // write out empty array
+            if ( length == 0 ) {
+                jgen.writeArrayFieldStart(key);
+                jgen.writeEndArray();
+                return;
+            }
+            values = new Object[Array.getLength(value)];
+            for(int i=0; i<length; i++) {
+                values[i] = Array.get(value, i);
+            }
+        }
+
+        // special handling for binaries: we dump the length and not the data!
+        if (value instanceof InputStream
+                || (values != null && values[0] instanceof InputStream)) {
+            // TODO for now we mark binary properties with an initial colon in
+            // their name
+            // (colon is not allowed as a JCR property name)
+            // in the name, and the value should be the size of the binary data
+            if (values == null) {
+                jgen.writeNumberField(":" + key, getLength(valueMap, -1, key, (InputStream)value));
+            } else {
+                jgen.writeArrayFieldStart(":" + key);
+                for (int i = 0; i < values.length; i++) {
+                    jgen.writeNumber(getLength(valueMap, i, key, (InputStream)values[i]));
+                }
+                jgen.writeEndArray();
+            }
+            return;
+        }
+
+        if (!value.getClass().isArray()) {
+            jgen.writeFieldName(key);
+            writeValue(jgen, value, provider);
+        } else {
+            jgen.writeArrayFieldStart(key);
+            for (Object v : values) {
+                writeValue(jgen, v, provider);
+            }
+            jgen.writeEndArray();
+        }
+    }
+
+    /** true if the current recursion level is active */
+    private boolean recursionLevelActive(final int currentRecursionLevel) {
+        return maxRecursionLevels < 0 || currentRecursionLevel < maxRecursionLevels;
+    }
+
+    private long getLength(final ValueMap valueMap, final int index, final String key, final InputStream stream) {
+        try {
+            stream.close();
+        } catch (IOException ignore) {}
+
+        long length = -1;
+        if ( valueMap != null ) {
+            if ( index == -1 ) {
+                length = valueMap.get(key, length);
+            } else {
+                Long[] lengths = valueMap.get(key, Long[].class);
+                if ( lengths != null && lengths.length > index ) {
+                    length = lengths[index];
+                }
+            }
+        }
+        return length;
+    }
+
+    /** Dump only a value in the correct format */
+    private void writeValue(final JsonGenerator jgen, final Object value, final SerializerProvider provider) throws IOException {
+        if (value instanceof InputStream) {
+            // input stream is already handled
+            jgen.writeNumber(0);
+        } else if (value instanceof Calendar) {
+            calendarSerializer.serialize(value, jgen, provider);
+        } else if (value instanceof Boolean) {
+            jgen.writeBoolean(((Boolean)value).booleanValue());
+        } else if (value instanceof Long) {
+            jgen.writeNumber(((Long)value).longValue());
+        } else if (value instanceof Integer) {
+            jgen.writeNumber(((Integer)value).intValue());
+        } else if (value instanceof Double) {
+            jgen.writeNumber(((Double)value).doubleValue());
+        } else if (value != null) {
+            jgen.writeString(value.toString());
+        } else {
+            jgen.writeString(""); // assume empty string
+        }
+    }
+
+    @Override
+    public void resolve(SerializerProvider provider) throws JsonMappingException {
+        this.calendarSerializer = provider.findValueSerializer(Calendar.class, null);
+    }
+}
diff --git a/src/main/java/org/apache/sling/models/jacksonexporter/package-info.java b/src/main/java/org/apache/sling/models/jacksonexporter/package-info.java
new file mode 100644
index 0000000..58da044
--- /dev/null
+++ b/src/main/java/org/apache/sling/models/jacksonexporter/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+@Version("1.0.0")
+package org.apache.sling.models.jacksonexporter;
+
+import aQute.bnd.annotation.Version;
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.