org.apache.fulcrum.json.jackson.Jackson2MapperService Maven / Gradle / Ivy
The newest version!
package org.apache.fulcrum.json.jackson;
/*
* 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
* specific language governing permissions and limitations
* under the License.
*/
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.logger.LogEnabled;
import org.apache.commons.lang3.StringUtils;
import org.apache.fulcrum.json.JsonService;
import org.apache.fulcrum.json.jackson.filters.CustomModuleWrapper;
import org.apache.fulcrum.json.jackson.jsonpath.DefaultJsonPathWrapper;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.SerializableString;
import com.fasterxml.jackson.core.io.CharacterEscapes;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.cfg.ConfigFeature;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.PropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
/**
*
*
* By default multiple serialization of the same object in a single thread is
* not supported (e.g filter + mixin or default + filter for the same bean /
* object).
*
* By default a filter is defined by its {@link Class#getName()}.
*
* Note: If using {@link SimpleNameIntrospector}, filter caches are set by class
* id. Caching is enabled by default, if not (a) by setting
* {@link #cacheFilters} to false
. By setting (b) the Boolean
* parameter clean
* {@link #serializeAllExceptFilter(Object, Class, Boolean, String...)} or
* {@link #serializeOnlyFilter(Object, Class, Boolean, String...)} you could
* clean the filter. If caching is disabled each filter will be unregistered and
* the cache cleaned.
*
* @author Georg Kallidis
* @version $Id: Jackson2MapperService.java 1890800 2021-06-15 08:33:26Z gk $
*
*/
public class Jackson2MapperService extends AbstractLogEnabled implements JsonService, Initializable, Configurable {
private static final String DEFAULT_TYPING = "defaultTyping";
private static final String CACHE_FILTERS = "cacheFilters";
private static final String DATE_FORMAT = "dateFormat";
private static final String ESCAPE_CHARS = "escapeCharsGlobal";
private static final String ESCAPE_CHAR_CLASS = "escapeCharsClass";
private static final String USE_JSON_PATH = "useJsonPath";
ObjectMapper mapper;
AnnotationIntrospector primary; // support default
AnnotationIntrospector secondary;
private static final String ANNOTATIONINSPECTOR = "annotationInspectors";
private Map annotationInspectors = null;
private Map features = null;
private Map featureTypes = null;
private String dateFormat;
/**
* Default dateformat is MM/dd/yyyy
, could be overwritten in
* {@link #setDateFormat(DateFormat)}.
*/
public static final String DEFAULTDATEFORMAT = "MM/dd/yyyy";
private boolean cacheFilters = true; // true -> this is by default true in jackson, if not using
// multiple serialization in one thread
String[] defaultTypeDefs = null;
private CacheService cacheService;
private boolean escapeCharsGlobal = false; // to be backward compatible, but should be true, then escaping to avoid
// XSS payload by default
private boolean useJsonPath = false;
private String escapeCharsClass = null;
@Override
public String ser(Object src) throws Exception {
return ser(src, false);
}
@Override
public String ser(Object src, Class type) throws Exception {
return ser(src, type, false);
}
public String ser(Object src, FilterProvider filter) throws Exception {
return ser(src, filter, false);
}
/**
*
* @param src the object to be serailized as JSON
* @param filter may be null, then sserialize without otherwise set into cache service and as filter provider.
* @param cleanCache cleans the jackson cache
* @return the serialzed JSON string
* @throws Exception exception
*/
public String ser(Object src, FilterProvider filter, Boolean cleanCache) throws Exception {
String serResult = null;
if (src == null) {
getLogger().info("no serializable object.");
return serResult;
}
if (filter == null) {
getLogger().debug("ser class::" + src.getClass() + " without filter.");
return ser(src);
} else {
getLogger().debug("add filter for cache filter Class " + src.getClass().getName());
setCustomIntrospectorWithExternalFilterId(src.getClass(), null); // filter class
if (isCacheFilters()) {
cacheService.getFilters().put(src.getClass().getName(), filter);
}
}
getLogger().debug("ser class::" + src.getClass() + " with filter " + filter);
mapper.setFilterProvider(filter);
String res = mapper.writer(filter).writeValueAsString(src);
if (cleanCache) {
cacheService.cleanSerializerCache(mapper);
}
return res;
}
@Override
public T deSer(String json, Class type) throws Exception {
ObjectReader reader = null;
if (type != null)
reader = mapper.readerFor(type);
else
reader = mapper.reader();
return reader.readValue(json);
}
/**
* basically wrapper for {@link ObjectMapper#convertValue(Object, Class)}.
*
* @param src Object
* @param type target Object
* @return
*/
public T deSer(Object src, Class type) {
return mapper.convertValue(src, type);
}
/**
* Add a named module or a {@link Module}.
*
* @param name Name of the module, optional. Could be null, if module is a
* {@link Module}.
*
* @param target Target class, optional. Could be null, if module is a
* {@link Module}.
*
* @param module Either an Jackson Module @link {@link Module} or an custom
* wrapper @link CustomModuleWrapper.
*/
@Override
public JsonService addAdapter(String name, Class target, Object module)
throws Exception {
if (module instanceof CustomModuleWrapper) {
CustomModuleWrapper cmw = (CustomModuleWrapper) module;
Module cm = new CustomModule(name, target, cmw.getSer(),
cmw.getDeSer());
getLogger().debug("registering custom module " + cm + " for: " + target);
mapper.registerModule(cm);
} else if (module instanceof Module) {
getLogger().debug(
"registering module " + module );
mapper.registerModule((Module) module);
} else {
throw new ClassCastException("expecting module type " + Module.class);
}
return this;
}
public Class> showMixinForClass(Class target) {
Class> mixin = mapper.findMixInClassFor( target );
getLogger().debug("find mixin for target " + target + " -> mixin: " + mixin);
return mixin;
}
public List deSerList(String json, Class extends List> targetList, Class elementType) throws Exception {
return mapper.readValue(json, mapper.getTypeFactory().constructParametricType(targetList, elementType));
}
public Map deSerMap(String json, Class extends Map> mapClass, Class keyClass, Class valueClass)
throws Exception {
return mapper.readValue(json, mapper.getTypeFactory().constructMapType(mapClass, keyClass, valueClass));
}
public Collection deSerCollectionWithTypeReference(String json, TypeReference collectionType)
throws Exception {
return (Collection) mapper.readValue(json, collectionType);
}
public Collection deSerCollectionWithType(String json, Class extends Collection> collectionClass,
Class type) throws Exception {
return mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(collectionClass, type));
}
@Override
public Collection deSerCollection(String json, Object collectionType, Class elementType)
throws Exception {
if (collectionType instanceof TypeReference) {
return deSerCollectionWithTypeReference(json, (TypeReference) collectionType);
} else {
return mapper.readValue(json, mapper.getTypeFactory()
.constructCollectionType(((Collection) collectionType).getClass(), elementType));
}
}
/**
*
* @param src the collection to be serialized
* @param collectionType the {@link TypeReference}.
* @param cleanCache cleans jackson serializer cache
* @return the serialized JSON string
* @throws JsonProcessingException JSON processing error
*/
public String serCollectionWithTypeReference(Collection src, TypeReference collectionType,
Boolean cleanCache) throws JsonProcessingException {
String res = mapper.writerFor(collectionType).writeValueAsString(src);
if (cleanCache) {
cacheService.cleanSerializerCache(mapper);
}
return res;
}
/**
* @param name name of the module
* @param target target class
* @param mixin provide mixin as class. Deregistering module could be only done
* by setting this parameter to null.
*
* @see #addAdapter(String, Class, Object)
*/
@Override
public JsonService addAdapter(String name, Class target, Class mixin) throws Exception {
getLogger().debug(
"registering unversioned simple mixin module named " + name + " of type " + mixin + " for: " + target);
mapper.addMixIn(target, mixin);
return this;
}
/**
* set a single mixin. convenience method, calls
* {@link ObjectMapper#registerModule(Module)}
*
* @param src he object to be serialized
* @param name the name for the mixin
* @param target the target class for the mixin
* @param mixin the mixin class
* @return serialized result
* @throws JsonProcessingException if not properly processed
*/
@SuppressWarnings("rawtypes")
public String withMixinModule(Object src, String name, Class target, Class mixin) throws JsonProcessingException {
Module mx = new MixinModule(name, target, mixin);
getLogger().debug("registering module " + mx + ", mixin: " + mixin);
return mapper.registerModule(mx).writer().writeValueAsString(src);
}
/**
* This is a convenience method with read, but the old mixins will be cleared
* {@link ObjectMapper#setMixIns(Map)}
*
* @param src the object to be serialized
* @param target the target class for the mixin
* @param mixin the mixin class
* @return serialized result
* @throws JsonProcessingException if fail
*/
@SuppressWarnings("rawtypes")
public String withSetMixins(Object src, Class target, Class mixin) throws JsonProcessingException {
return setMixins(target, mixin).writer().writeValueAsString(src);
}
/**
* @param target The target class
* @param mixin the mixin class
* @return an objectmapper
*/
@SuppressWarnings("rawtypes")
public ObjectMapper setMixins(Class target, Class mixin) {
Map, Class>> sourceMixins = null;
if (target != null) {
sourceMixins = new HashMap<>(1);
sourceMixins.put(target, mixin);
}
getLogger().debug("complete reset mixins for target " + target + ", mixin: " + mixin);
return mapper.setMixIns(sourceMixins);
}
@Override
public String serializeAllExceptFilter(Object src, String... filterAttr) throws Exception {
return serializeAllExceptFilter(src, src.getClass(), true, filterAttr);
}
@Override
public synchronized String serializeAllExceptFilter(Object src, Boolean cache, String... filterAttr)
throws Exception {
return serializeAllExceptFilter(src, src.getClass(), cache, filterAttr);
}
public synchronized String serializeAllExceptFilter(Object src, Class[] filterClasses, String... filterAttr)
throws Exception {
return serializeAllExceptFilter(src, filterClasses, true, filterAttr);
}
@Override
public synchronized String serializeAllExceptFilter(Object src, Class filterClass, String... filterAttr)
throws Exception {
return serializeAllExceptFilter(src, filterClass, true, filterAttr);
}
@Override
public String serializeAllExceptFilter(Object src, Class filterClass, Boolean cleanFilter,
String... filterAttr) throws Exception {
return serializeAllExceptFilter(src, new Class[] { filterClass }, cleanFilter, filterAttr);
}
/**
*
* @param src the object to be serailized, may be a list
* @param filterClasses the same object class or a detail class, which should be
* filtered
* @param class type
* @param clean cleaning the cache after serialization
* @param filterAttr attributes to be filtered for filtered class
* @return the serailized string
* @throws Exception generic exception
*/
public synchronized String serializeAllExceptFilter(Object src, Class[] filterClasses, Boolean clean,
String... filterAttr) throws Exception {
PropertyFilter pf = null;
if (filterAttr != null)
pf = SimpleBeanPropertyFilter.serializeAllExcept(filterAttr);
else if (filterClasses == null) { // no filter
return ser(src, clean);
// should be better:
// return filter(src, new Class>[] { src.getClass() }, filterClasses, pf,
// clean);
}
return filter(src, new Class>[] { filterClasses[0] }, filterClasses, pf, clean);
}
@Override
public String serializeOnlyFilter(Object src, String... filterAttrs) throws Exception {
return serializeOnlyFilter(src, src.getClass(), true, filterAttrs);
}
@Override
public synchronized String serializeOnlyFilter(Object src, Boolean cache, String... filterAttr) throws Exception {
return serializeOnlyFilter(src, src.getClass(), cache, filterAttr);
}
@Override
public synchronized String serializeOnlyFilter(Object src, Class filterClass, String... filterAttr)
throws Exception {
return serializeOnlyFilter(src, filterClass, true, filterAttr);
}
@Override
public synchronized String serializeOnlyFilter(Object src, Class filterClass, Boolean refresh,
String... filterAttr) throws Exception {
return serializeOnlyFilter(src, new Class[] { filterClass }, refresh, filterAttr);
}
public synchronized String serializeOnlyFilter(Object src, Class[] filterClasses, Boolean refresh,
String... filterAttr) throws Exception {
PropertyFilter pf = null;
if (filterAttr != null && filterAttr.length > 0 && !"".equals(filterAttr[0])) {
pf = SimpleBeanPropertyFilter.filterOutAllExcept(filterAttr);
getLogger().debug("setting filteroutAllexcept filter for size of filterAttr: " + filterAttr.length);
} else {
getLogger().warn("no filter attributes set!");
pf = SimpleBeanPropertyFilter.filterOutAllExcept("dummy");
}
if (filterClasses == null)
throw new AssertionError("You have to provide some class to apply the filtering!");
return filter(src, filterClasses, null, pf, refresh);
}
@Override
public String ser(Object src, Boolean cleanCache) throws Exception {
if (isCacheFilters() && cacheService.getFilters().containsKey(src.getClass().getName())) {
getLogger().warn("Found registered filter - using instead of default view filter for class:"
+ src.getClass().getName());
SimpleFilterProvider filter = (SimpleFilterProvider) cacheService.getFilters()
.get(src.getClass().getName());
return ser(src, filter, cleanCache);// mapper.writerWithView(src.getClass()).writeValueAsString(src);
}
String res = mapper.writerWithView(Object.class).writeValueAsString(src);
if (cleanCache != null && cleanCache) {
cacheService.cleanSerializerCache(mapper);
}
return res;
}
@Override
public String ser(Object src, Class type, Boolean cleanCache) throws Exception {
getLogger().info("serializing object:" + src + " for type " + type);
if (isCacheFilters() && src != null && cacheService.getFilters().containsKey(src.getClass().getName())) {
getLogger().warn("Found registered filter - could not use custom view and custom filter for class:"
+ src.getClass().getName());
// throw new
// Exception("Found registered filter - could not use custom view and custom
// filter for class:"+
// src.getClass().getName());
SimpleFilterProvider filter = (SimpleFilterProvider) cacheService.getFilters()
.get(src.getClass().getName());
return ser(src, filter);
}
String res = (type != null) ? mapper.writerWithView(type).writeValueAsString(src)
: mapper.writeValueAsString(src);
if (cleanCache) {
cacheService.cleanSerializerCache(mapper);
}
return res;
}
/**
*
* @param src The source Object to be filtered.
* @param filterClass This Class array contains at least one element. If no
* class is provided it is the class type of the source
* object. The filterClass is to become the key of the
* filter object cache.
* @param excludeClasses The classes to be excluded, optionally used only for
* methods like
* {@link #serializeAllExceptFilter(Object, Class[], String...)}.
* @param pf Expecting a property filter from e.g @link
* {@link SimpleBeanPropertyFilter}.
* @param clean if true
does not reuse the filter object
* (no cashing).
* @return The serialized Object as String
* @throws Exception
*/
private String filter(Object src, Class>[] filterClasses, Class[] excludeClasses, PropertyFilter pf,
Boolean clean) throws Exception {
FilterProvider filter = null;
if (filterClasses.length > 0) {
filter = retrieveFilter(pf, filterClasses[0], excludeClasses);
}
getLogger().info("filtering with filter " + filter);
String serialized = ser(src, filter, clean);
if (!isCacheFilters() || clean) {
if (filterClasses.length > 0) {
boolean exclude = (excludeClasses != null) ? true : false;
cacheService.removeFilter(filterClasses[0], exclude);
}
}
return serialized;
}
private SimpleFilterProvider retrieveFilter(PropertyFilter pf, Class> filterClass,
Class[] excludeClasses) {
SimpleFilterProvider filter = null;
if (pf != null) {
filter = new SimpleFilterProvider();
filter.setDefaultFilter(pf);
}
if (isCacheFilters()) {
if (!cacheService.getFilters().containsKey(filterClass.getName())) {
getLogger().debug("add filter for cache filter Class " + filterClass.getName());
setCustomIntrospectorWithExternalFilterId(filterClass, excludeClasses); // filter class
if (pf != null) {
cacheService.getFilters().put(filterClass.getName(), filter);
}
} else {
filter = (SimpleFilterProvider) cacheService.getFilters().get(filterClass.getName());
// setCustomIntrospectorWithExternalFilterId(filterClass); // filter
// class
}
}
getLogger().debug("set filter:" + filter);
return filter;
}
/**
* @param filterClass
* Adding filterClass into
* {@link SimpleNameIntrospector#setFilteredClass(Class)}
* enables the filtering process.
* @param externalFilterIds
* Adding externalFilterIs to
* {@link SimpleNameIntrospector#setExternalFilterExcludeClasses(Class...)}
* excludes these classes.
*/
private void setCustomIntrospectorWithExternalFilterId(Class> filterClass,
Class[] externalFilterClassIds) {
if (primary instanceof SimpleNameIntrospector) {
// first one is required that we get to the PropertyFilter
((SimpleNameIntrospector) primary).setFilteredClasses(filterClass);
if (externalFilterClassIds != null) {
((SimpleNameIntrospector) primary).setIsExludeType(true);
for (Class filterClazz : externalFilterClassIds) {
getLogger().debug("added class for filters " + filterClazz);
}
((SimpleNameIntrospector) primary).setExternalFilterExcludeClasses(externalFilterClassIds);
}
}
}
public Jackson2MapperService registerModule(Module module) {
mapper.registerModule(module);
return this;
}
public void addSimpleModule(SimpleModule module, Class type, JsonSerializer ser) {
module.addSerializer(type, ser);
}
public void addSimpleModule(SimpleModule module, Class type, JsonDeserializer deSer) {
module.addDeserializer(type, deSer);
}
/**
* Default Dateformat: {@link #DEFAULTDATEFORMAT}
*/
@Override
public void setDateFormat(final DateFormat df) {
mapper.setDateFormat(df);
}
/**
* Avalon component lifecycle method
*/
@Override
public void configure(Configuration conf) throws ConfigurationException {
getLogger().debug("conf.getName()" + conf.getName());
this.annotationInspectors = new HashMap<>();
final Configuration configuredAnnotationInspectors = conf.getChild(ANNOTATIONINSPECTOR, false);
if (configuredAnnotationInspectors != null) {
Configuration[] nameVal = configuredAnnotationInspectors.getChildren();
Arrays.stream( nameVal).forEach(c->
{
String key = c.getName();
getLogger().debug("configured key: " + key);
if (key.equals("features")) {
this.features = new HashMap<>();
this.featureTypes = new HashMap<>();
Arrays.stream( c.getChildren() ).forEach( lf -> {
boolean featureValue = lf.getAttributeAsBoolean("value", false);
String featureType = null;
String feature = null;
try {
featureType = lf.getAttribute("type");
feature = lf.getValue();
getLogger().debug("configuredAnnotationInspectors " + feature + ":" + featureValue);
this.features.put(feature, featureValue);
this.featureTypes.put(feature, featureType);
} catch (ConfigurationException e) {
throw new RuntimeException(e);
}
});
} else {
String val;
try {
val = c.getValue();
getLogger().debug("configuredAnnotationInspectors " + key + ":" + val);
this.annotationInspectors.put(key, val);
} catch (ConfigurationException e) {
throw new RuntimeException(e);
}
}
});
}
final Configuration configuredDateFormat = conf.getChild(DATE_FORMAT, true);
this.dateFormat = configuredDateFormat.getValue(DEFAULTDATEFORMAT);
final Configuration configuredKeepFilter = conf.getChild(CACHE_FILTERS, false);
if (configuredKeepFilter != null) {
setCacheFilters( configuredKeepFilter.getValueAsBoolean());
}
final Configuration configuredEscapeChars = conf.getChild(ESCAPE_CHARS, false);
if (configuredEscapeChars != null) {
this.escapeCharsGlobal = configuredEscapeChars.getValueAsBoolean();
}
final Configuration configuredEscapeCharClass = conf.getChild(ESCAPE_CHAR_CLASS, false);
if (configuredEscapeCharClass != null) {
this.escapeCharsClass = configuredEscapeCharClass.getValue();
}
final Configuration configuredDefaultType = conf.getChild(DEFAULT_TYPING, false);
if (configuredDefaultType != null) {
defaultTypeDefs = new String[] { configuredDefaultType.getAttribute("type"),
configuredDefaultType.getAttribute("key") };
}
final Configuration configuredjsonPath = conf.getChild(USE_JSON_PATH, false);
if (configuredjsonPath != null) {
this.useJsonPath = configuredjsonPath.getValueAsBoolean();
}
}
@Override
public void initialize() throws Exception {
mapper = new ObjectMapper(null, null, null);// add configurable JsonFactory,.. later?
initAnnotationInspectors();
initFeatures();
initDefaultTyping();
getLogger().info("setting date format to:" + dateFormat);
getLogger().info("cacheFilters is:" + isCacheFilters());
if (!isCacheFilters()) {
mapper.configure(SerializationFeature.FLUSH_AFTER_WRITE_VALUE, true);
}
mapper.setDateFormat(new SimpleDateFormat(dateFormat));
if (escapeCharsGlobal) {
mapper.getFactory().setCharacterEscapes(characterEscapes);
}
if (escapeCharsClass != null) {
try {
characterEscapes = (CharacterEscapes) Class.forName(escapeCharsClass).getConstructor().newInstance();
} catch (Exception e) {
throw new InstantiationException(
"JsonMapperService: Error instantiating " + escapeCharsClass + " for " + ESCAPE_CHAR_CLASS);
}
}
getLogger().debug("initialized mapper:" + mapper);
mapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer