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 <[email protected]> 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 "[email protected]" <[email protected]>.
