| 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 <code>false</code>. 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 <a href="mailto:gk@apache.org">Georg Kallidis</a> |
| * @version $Id$ |
| * |
| */ |
| 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<String, String> annotationInspectors = null; |
| private Map<String, Boolean> features = null; |
| private Map<String, String> featureTypes = null; |
| |
| private String dateFormat; |
| |
| /** |
| * Default dateformat is <code>MM/dd/yyyy</code>, 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 <T> String ser(Object src, Class<T> 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> T deSer(String json, Class<T> 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> T deSer(Object src, Class<T> 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 <T> List<T> deSerList(String json, Class<? extends List> targetList, Class<T> elementType) throws Exception { |
| return mapper.readValue(json, mapper.getTypeFactory().constructParametricType(targetList, elementType)); |
| } |
| |
| public <T, U> Map<T, U> deSerMap(String json, Class<? extends Map> mapClass, Class<T> keyClass, Class<U> valueClass) |
| throws Exception { |
| return mapper.readValue(json, mapper.getTypeFactory().constructMapType(mapClass, keyClass, valueClass)); |
| } |
| |
| public <T> Collection<T> deSerCollectionWithTypeReference(String json, TypeReference<T> collectionType) |
| throws Exception { |
| return (Collection<T>) mapper.readValue(json, collectionType); |
| } |
| |
| public <T> Collection<T> deSerCollectionWithType(String json, Class<? extends Collection> collectionClass, |
| Class<T> type) throws Exception { |
| return mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(collectionClass, type)); |
| } |
| |
| @Override |
| public <T> Collection<T> deSerCollection(String json, Object collectionType, Class<T> elementType) |
| throws Exception { |
| if (collectionType instanceof TypeReference) { |
| return deSerCollectionWithTypeReference(json, (TypeReference<T>) collectionType); |
| } else { |
| return mapper.readValue(json, mapper.getTypeFactory() |
| .constructCollectionType(((Collection<T>) 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 <T> String serCollectionWithTypeReference(Collection<T> 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<?>, 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 <T> String serializeAllExceptFilter(Object src, Class<T>[] filterClasses, String... filterAttr) |
| throws Exception { |
| return serializeAllExceptFilter(src, filterClasses, true, filterAttr); |
| } |
| |
| @Override |
| public synchronized <T> String serializeAllExceptFilter(Object src, Class<T> filterClass, String... filterAttr) |
| throws Exception { |
| return serializeAllExceptFilter(src, filterClass, true, filterAttr); |
| } |
| |
| @Override |
| public <T> String serializeAllExceptFilter(Object src, Class<T> 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 <T> 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 <T> String serializeAllExceptFilter(Object src, Class<T>[] 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 <T> String serializeOnlyFilter(Object src, Class<T> filterClass, String... filterAttr) |
| throws Exception { |
| return serializeOnlyFilter(src, filterClass, true, filterAttr); |
| } |
| |
| @Override |
| public synchronized <T> String serializeOnlyFilter(Object src, Class<T> filterClass, Boolean refresh, |
| String... filterAttr) throws Exception { |
| return serializeOnlyFilter(src, new Class[] { filterClass }, refresh, filterAttr); |
| } |
| |
| public synchronized <T> String serializeOnlyFilter(Object src, Class<T>[] 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 <T> String ser(Object src, Class<T> 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 <code>true</code> does not reuse the filter object |
| * (no cashing). |
| * @return The serialized Object as String |
| * @throws Exception |
| */ |
| private <T> String filter(Object src, Class<?>[] filterClasses, Class<T>[] 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 <T> SimpleFilterProvider retrieveFilter(PropertyFilter pf, Class<?> filterClass, |
| Class<T>[] 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 |
| * <li>Adding filterClass into |
| * {@link SimpleNameIntrospector#setFilteredClass(Class)} |
| * enables the filtering process. |
| * @param externalFilterIds |
| * <li>Adding externalFilterIs to |
| * {@link SimpleNameIntrospector#setExternalFilterExcludeClasses(Class...)} |
| * excludes these classes. |
| */ |
| private <T> void setCustomIntrospectorWithExternalFilterId(Class<?> filterClass, |
| Class<T>[] 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<T> filterClazz : externalFilterClassIds) { |
| getLogger().debug("added class for filters " + filterClazz); |
| } |
| ((SimpleNameIntrospector) primary).setExternalFilterExcludeClasses(externalFilterClassIds); |
| } |
| } |
| } |
| |
| public Jackson2MapperService registerModule(Module module) { |
| mapper.registerModule(module); |
| return this; |
| } |
| |
| public <T> void addSimpleModule(SimpleModule module, Class<T> type, JsonSerializer<T> ser) { |
| module.addSerializer(type, ser); |
| } |
| |
| public <T> void addSimpleModule(SimpleModule module, Class<T> type, JsonDeserializer<T> 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<Object>() { |
| @Override |
| public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException { |
| jgen.writeString(""); |
| |
| } |
| }); |
| cacheService = new CacheService(primary); |
| if (cacheService instanceof LogEnabled) { |
| cacheService.enableLogging(getLogger().getChildLogger(cacheService.getClass().getSimpleName())); |
| getLogger().info("setting cacheService logger: " + cacheService.getClass().getSimpleName()); |
| } |
| |
| if (useJsonPath) { |
| // set it before runtime |
| DefaultJsonPathWrapper djpw = null; |
| try { |
| djpw = new DefaultJsonPathWrapper(this.mapper); |
| getLogger().debug("******** initialized new jsonPath defaults: " + djpw.getJsonPathDefault()); |
| } catch (Exception e) { |
| throw new AssertionError( |
| "JsonMapperService: Error instantiating " + djpw + " using useJsonPath=" + useJsonPath); |
| } |
| |
| } |
| } |
| |
| private void initDefaultTyping() { |
| if (defaultTypeDefs != null && defaultTypeDefs.length == 2) { |
| DefaultTyping defaultTyping = DefaultTyping.valueOf(defaultTypeDefs[0]); |
| mapper.enableDefaultTypingAsProperty(defaultTyping, defaultTypeDefs[1]); |
| getLogger().info("default typing is " + defaultTypeDefs[0] + " with key:" + defaultTypeDefs[1]); |
| } |
| } |
| |
| private void initFeatures() throws Exception { |
| if (features != null && !features.isEmpty()) { |
| features.entrySet().stream().forEach( entry -> { |
| String featureKey = entry.getKey(); |
| Boolean featureValue = entry.getValue(); |
| String featureType = featureTypes.get(featureKey); |
| Class<?> configFeature = null; |
| try { |
| getLogger().debug("initializing featureType: " + featureType); |
| configFeature = loadClass(featureType); |
| } catch (ClassNotFoundException e) { |
| throw new AssertionError("JsonMapperService: Error instantiating " + featureType + " for " + featureKey, e); |
| } |
| ConfigFeature feature = null; |
| if (!StringUtils.isEmpty(featureKey) && featureValue != null) { |
| try |
| { |
| if (configFeature.equals(SerializationFeature.class)) { |
| feature = SerializationFeature.valueOf(featureKey); |
| mapper.configure((SerializationFeature) feature, featureValue); |
| assert mapper.getSerializationConfig() |
| .isEnabled((SerializationFeature) feature) == featureValue; |
| getLogger().info("initialized serconfig mapper feature: " + feature + " with " |
| + mapper.getSerializationConfig().isEnabled((SerializationFeature) feature)); |
| } else if (configFeature.equals(DeserializationFeature.class)) { |
| feature = DeserializationFeature.valueOf(featureKey); |
| mapper.configure((DeserializationFeature) feature, featureValue); |
| assert mapper.getDeserializationConfig() |
| .isEnabled((DeserializationFeature) feature) == featureValue; |
| getLogger().info("initialized deserconfig mapper feature: " + feature + " with " |
| + mapper.getDeserializationConfig().isEnabled((DeserializationFeature) feature)); |
| } else if (configFeature.equals(MapperFeature.class)) { |
| feature = MapperFeature.valueOf(featureKey); |
| mapper.configure((MapperFeature) feature, featureValue); |
| assert mapper.getDeserializationConfig().isEnabled((MapperFeature) feature) == featureValue; |
| assert mapper.getSerializationConfig().isEnabled((MapperFeature) feature) == featureValue; |
| getLogger().info("initialized serconfig mapper feature: " + feature + " with " |
| + mapper.getDeserializationConfig().isEnabled((MapperFeature) feature)); |
| getLogger().info("initialized deserconfig mapper feature: " + feature + " with " |
| + mapper.getSerializationConfig().isEnabled((MapperFeature) feature)); |
| } else if (configFeature.equals(JsonParser.class)) { |
| Feature parserFeature = JsonParser.Feature.valueOf(featureKey); |
| getLogger().info("initializing parser feature: " + parserFeature + " with " + featureValue); |
| mapper.configure(parserFeature, featureValue); |
| } else if (configFeature.equals(JsonGenerator.class)) { |
| com.fasterxml.jackson.core.JsonGenerator.Feature genFeature = JsonGenerator.Feature |
| .valueOf(featureKey); |
| getLogger().info("initializing parser feature: " + genFeature + " with " + featureValue); |
| mapper.configure(genFeature, featureValue); |
| } |
| } catch (Exception e) { |
| throw new RuntimeException("JsonMapperService: Error instantiating feature " + featureKey + " with " |
| + featureValue , e); |
| } |
| |
| } |
| }); |
| } |
| } |
| |
| private void initAnnotationInspectors() throws Exception { |
| for (Entry<String, String> entry : annotationInspectors.entrySet()) { |
| String key = entry.getKey(); |
| String avClass = entry.getValue(); |
| if (key.equals("primary") && !StringUtils.isEmpty(avClass)) { |
| try { |
| primary = (AnnotationIntrospector) Class.forName(avClass).getConstructor().newInstance(); |
| } catch (Exception e) { |
| throw new InstantiationException("JsonMapperService: Error instantiating " + avClass + " for " + key); |
| } |
| } else if (key.equals("secondary") && avClass != null) { |
| try { |
| secondary = (AnnotationIntrospector) Class.forName(avClass).getConstructor().newInstance(); |
| } catch (Exception e) { |
| throw new InstantiationException("JsonMapperService: Error instantiating " + avClass + " for " + key); |
| } |
| } |
| } |
| if (primary == null) { |
| primary = new JacksonAnnotationIntrospector(); // support default |
| getLogger().info("using default introspector:" + primary.getClass().getName()); |
| mapper.setAnnotationIntrospector(primary); |
| } else if (primary != null && secondary != null) { |
| AnnotationIntrospector pair = new AnnotationIntrospectorPair(primary, secondary); |
| mapper.setAnnotationIntrospector(pair); |
| } else { |
| mapper.setAnnotationIntrospector(primary); |
| } |
| |
| if (primary instanceof LogEnabled) { |
| ((LogEnabled) primary).enableLogging(getLogger().getChildLogger(primary.getClass().getSimpleName())); |
| getLogger().info("setting primary introspector logger: " + primary.getClass().getSimpleName()); |
| } |
| if (secondary instanceof LogEnabled) { |
| ((LogEnabled) secondary).enableLogging(getLogger().getChildLogger(secondary.getClass().getSimpleName())); |
| getLogger().info("setting secondary introspector logger: " + secondary.getClass().getSimpleName()); |
| } |
| } |
| |
| /** |
| * Loads the named class using the default class loader. |
| * |
| * @param className the name of the class to load. |
| * @return {@inheritDoc} the loaded class. |
| * @throws ClassNotFoundException if the class was not found. |
| */ |
| @SuppressWarnings("unchecked") |
| protected <T> Class<T> loadClass(String className) throws ClassNotFoundException { |
| ClassLoader loader = this.getClass().getClassLoader(); |
| try { |
| Class<T> clazz; |
| |
| if (loader != null) { |
| clazz = (Class<T>) loader.loadClass(className); |
| } else { |
| clazz = (Class<T>) Class.forName(className); |
| } |
| |
| return clazz; |
| } catch (ClassNotFoundException x) { |
| /* Give up. */ |
| throw x; |
| } |
| } |
| |
| public ObjectMapper getMapper() { |
| return mapper; |
| } |
| |
| public void setMapper(ObjectMapper mapper) { |
| this.mapper = mapper; |
| } |
| |
| public boolean isCacheFilters() { |
| return cacheFilters; |
| } |
| |
| public void setCacheFilters(boolean cacheFilters) { |
| this.cacheFilters = cacheFilters; |
| if (!cacheFilters) |
| mapper.configure(SerializationFeature.FLUSH_AFTER_WRITE_VALUE, true); |
| } |
| |
| static CharacterEscapes characterEscapes = new CharacterEscapes() { |
| private static final long serialVersionUID = 1L; |
| private final int[] asciiEscapes; |
| { // instance init |
| int[] esc = standardAsciiEscapesForJSON(); |
| // this avoids to get evaluated immediately |
| esc['<'] = CharacterEscapes.ESCAPE_STANDARD; |
| esc['>'] = CharacterEscapes.ESCAPE_STANDARD; |
| esc['&'] = CharacterEscapes.ESCAPE_STANDARD; |
| esc['\''] = CharacterEscapes.ESCAPE_STANDARD; |
| // esc['/'] = '/'; //CharacterEscapes.ESCAPE_CUSTOM; |
| asciiEscapes = esc; |
| } |
| |
| @Override |
| public int[] getEscapeCodesForAscii() { |
| return asciiEscapes; |
| } |
| |
| @Override |
| public SerializableString getEscapeSequence(final int ch) { |
| // if ( ch == '/') { |
| // return new SerializedString("\\\\/"); |
| // } else { |
| return null; |
| // } |
| } |
| }; |
| } |