/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.kie.server.api.marshalling.json;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.lang.reflect.Member;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.regex.Pattern;

import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.databind.introspect.AnnotatedParameter;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.jsontype.impl.AsWrapperTypeDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import org.drools.core.xml.jaxb.util.JaxbListAdapter;
import org.drools.core.xml.jaxb.util.JaxbListWrapper;
import org.drools.core.xml.jaxb.util.JaxbUnknownAdapter;
import org.kie.server.api.marshalling.Marshaller;
import org.kie.server.api.marshalling.MarshallerFactory;
import org.kie.server.api.marshalling.MarshallingException;
import org.kie.server.api.marshalling.MarshallingFormat;
import org.kie.server.api.model.Wrapped;
import org.kie.server.api.model.type.JaxbByteArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JSONMarshaller implements Marshaller {

    private static final Logger logger = LoggerFactory.getLogger(MarshallerFactory.class);

    private boolean formatDate = Boolean.parseBoolean(System.getProperty("org.kie.server.json.format.date", "false"));
    private String dateFormatStr = System.getProperty("org.kie.server.json.date_format", "yyyy-MM-dd'T'hh:mm:ss.SSSZ");

    private ThreadLocal<Boolean> stripped = new ThreadLocal<Boolean>() {
        @Override
        protected Boolean initialValue() {
            return false;
        }
    };

    protected ClassLoader classLoader;
    protected ObjectMapper objectMapper;

    protected Set<Class<?>> classesSet;

    protected ObjectMapper deserializeObjectMapper;

    protected DateFormat dateFormat = new SimpleDateFormat(dateFormatStr);

    // Optional Marshaller Extension to handle new types
    private static final List<JSONMarshallerExtension> EXTENSIONS;

    // Load Marshaller Extension
    static {
        logger.info("Marshaller extensions init");

        ServiceLoader<JSONMarshallerExtension> plugins = ServiceLoader.load(JSONMarshallerExtension.class);
        List<JSONMarshallerExtension> loadedPlugins = new ArrayList<>();
        plugins.forEach(plugin -> {
            logger.info("JSONMarshallerExtension implementation found: {}", plugin.getClass().getName());
            loadedPlugins.add(plugin);
        });
        EXTENSIONS = Collections.unmodifiableList(loadedPlugins);
    }

    public JSONMarshaller(Set<Class<?>> classes, ClassLoader classLoader) {
        this.classLoader = classLoader;
        buildMarshaller(classes, classLoader);

        configureMarshaller(classes, classLoader);
    }

    protected void buildMarshaller(Set<Class<?>> classes, final ClassLoader classLoader) {

        objectMapper = new ObjectMapper();
        deserializeObjectMapper = new ObjectMapper();
    }

    protected void configureMarshaller(Set<Class<?>> classes, final ClassLoader classLoader) {
        ObjectMapper customSerializationMapper = new ObjectMapper();
        if (classes == null) {
            classes = new HashSet<Class<?>>();
        }
        // add byte array handling support to allow byte[] to be send as payload
        classes.add(JaxbByteArray.class);
        if (!formatDate) {
            classes.add(Date.class);
        }

        List<NamedType> customClasses = prepareCustomClasses(classes);
        // this is needed because we need better control of serialization and deserialization
        AnnotationIntrospector primary = new ExtendedJaxbAnnotationIntrospector(customClasses, customSerializationMapper);
        AnnotationIntrospector secondary = new JacksonAnnotationIntrospector();
        AnnotationIntrospector introspectorPair = AnnotationIntrospector.pair(primary, secondary);
        objectMapper.setConfig(
                objectMapper.getSerializationConfig()
                        .with(introspectorPair)
                        .with(SerializationFeature.INDENT_OUTPUT));

        deserializeObjectMapper.setConfig(objectMapper.getDeserializationConfig()
                .with(introspectorPair)
                .with(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
                .without(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES));

        // setup custom serialization mapper with jaxb adapters
        customSerializationMapper.setConfig(customSerializationMapper.getDeserializationConfig().with(introspectorPair));
        customSerializationMapper.setConfig(customSerializationMapper.getSerializationConfig().with(introspectorPair).with(SerializationFeature.INDENT_OUTPUT));

        // in case there are custom classes register module to deal with them both for serialization and deserialization
        // this module makes sure that only custom classes are equipped with type information
        if (classes != null && !classes.isEmpty()) {
            ObjectMapper customObjectMapper = new ObjectMapper();
            TypeResolverBuilder<?> typer = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.NON_FINAL) {
                @Override
                public boolean useForType(JavaType t) {
                    if (classesSet.contains(t.getRawClass())) {
                        return true;
                    }
                    return false;
                }
            };
            typer = typer.init(JsonTypeInfo.Id.CLASS, null);
            typer = typer.inclusion(JsonTypeInfo.As.WRAPPER_OBJECT);
            customObjectMapper.setDefaultTyping(typer);

            customObjectMapper.setConfig(customObjectMapper.getSerializationConfig().with(introspectorPair).with(SerializationFeature.INDENT_OUTPUT));

            SimpleModule mod = new SimpleModule("custom-object-mapper", Version.unknownVersion());
            CustomObjectSerializer customObjectSerializer = new CustomObjectSerializer(customObjectMapper);

            for (Class<?> clazz : classes) {
                mod.addSerializer(clazz, customObjectSerializer);
            }

            objectMapper.registerModule(mod);
            TypeResolverBuilder<?> typer2 = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.NON_FINAL) {
                @Override
                public boolean useForType(JavaType t) {
                    if (classesSet.contains(t.getRawClass())) {
                        return true;
                    }
                    return false;
                }

                @Override
                public TypeDeserializer buildTypeDeserializer(DeserializationConfig config, JavaType baseType, Collection<NamedType> subtypes) {
                    if (useForType(baseType)) {
                        if (_idType == JsonTypeInfo.Id.NONE) {
                            return null;
                        }

                        TypeIdResolver idRes = idResolver(config, baseType, subtypes, false, true);
                        switch (_includeAs) {
                            case WRAPPER_OBJECT:
                                return new CustomAsWrapperTypeDeserializer(baseType, idRes, _typeProperty, true, baseType);
                        }
                    }

                    return super.buildTypeDeserializer(config, baseType, subtypes);
                }
            };
            typer2 = typer2.init(JsonTypeInfo.Id.CLASS, null);
            typer2 = typer2.inclusion(JsonTypeInfo.As.WRAPPER_OBJECT);
            deserializeObjectMapper.setDefaultTyping(typer2);

            SimpleModule modDeser = new SimpleModule("custom-object-unmapper", Version.unknownVersion());
            modDeser.addDeserializer(Object.class, new CustomObjectDeserializer(classes));

            deserializeObjectMapper.registerModule(modDeser);
            deserializeObjectMapper.setConfig(deserializeObjectMapper.getDeserializationConfig().with(introspectorPair));
        }

        if (formatDate) {

            objectMapper.setDateFormat(dateFormat);
            customSerializationMapper.setDateFormat(dateFormat);
            deserializeObjectMapper.setDateFormat(dateFormat);

            deserializeObjectMapper.getDeserializationConfig().with(dateFormat);

            objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
            customSerializationMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        }

        JavaTimeModule javaTimeModule = new JavaTimeModule();
        objectMapper.registerModule(javaTimeModule);
        deserializeObjectMapper.registerModule(javaTimeModule);
        customSerializationMapper.registerModule(javaTimeModule);

        this.classesSet = classes;

        // Extend the marshaller with optional extensions
        for (JSONMarshallerExtension extension : EXTENSIONS) {
            extension.extend(this, objectMapper, deserializeObjectMapper);
        }
    }

    protected List<NamedType> prepareCustomClasses(Set<Class<?>> classes) {
        List<NamedType> customClasses = new ArrayList<NamedType>();
        if (classes != null) {
            for (Class<?> clazz : classes) {
                customClasses.add(new NamedType(clazz, clazz.getSimpleName()));
                customClasses.add(new NamedType(clazz, clazz.getName()));
            }
        }
        return customClasses;
    }

    @Override
    public String marshall(Object objectInput) {
        try {
            return objectMapper.writeValueAsString(wrap(objectInput));
        } catch (IOException e) {
            throw new MarshallingException("Error marshalling input", e);
        }
    }

    @Override
    public <T> T unmarshall(String serializedInput, Class<T> type) {

        try {
            Class actualType = classesSet.contains(type) ? Object.class : type;
            return (T) unwrap(deserializeObjectMapper.readValue(serializedInput, actualType));
        } catch (IOException e) {
            throw new MarshallingException("Error unmarshalling input", e);
        } finally {
            stripped.set(false);
        }
    }

    @Override
    public void dispose() {

    }

    @Override
    public MarshallingFormat getFormat() {
        return MarshallingFormat.JSON;
    }

    protected Object wrap(Object data) {
        if (data instanceof byte[]) {
            return new JaxbByteArray((byte[]) data);
        }

        return data;
    }

    protected Object unwrap(Object data) {
        if (data instanceof Wrapped) {
            return ((Wrapped) data).unwrap();
        }

        return data;
    }

    class ExtendedJaxbAnnotationIntrospector extends JaxbAnnotationIntrospector {

        private List<NamedType> customClasses;
        private ObjectMapper customObjectMapper;

        public ExtendedJaxbAnnotationIntrospector(List<NamedType> customClasses, ObjectMapper anotherCustomObjectMapper) {
            this.customClasses = customClasses;

            this.customObjectMapper = anotherCustomObjectMapper;
        }

        @Override
        public List<NamedType> findSubtypes(Annotated a) {
            List<NamedType> base = super.findSubtypes(a);

            List<NamedType> complete = new ArrayList<NamedType>();
            if (base != null) {
                complete.addAll(base);
            }
            if (customClasses != null) {
                complete.addAll(customClasses);
            }
            return complete;
        }

        @Override
        public JsonSerializer<?> findSerializer(Annotated am) {
            // replace JaxbUnknownAdapter as it breaks JSON marshaller for list and maps with wrapping serializer
            XmlJavaTypeAdapter adapterInfo = findAnnotation(XmlJavaTypeAdapter.class, am, true, false, false);
            if (adapterInfo != null && adapterInfo.value().isAssignableFrom(JaxbUnknownAdapter.class)) {
                if (findAnnotation(JsonSerialize.class, am, true, false, false) != null) {
                    // .. unless there is also an explicitly specified serializer, in such case use the specified one:
                    return super.findSerializer(am);
                }
                return new WrappingObjectSerializer(customObjectMapper);
            }

            return super.findSerializer(am);
        }

        @Override
        public Object findSerializationConverter(Annotated a) {
            Class<?> serType = _rawSerializationType(a);
            // Can apply to both container and regular type; no difference yet here
            XmlAdapter<?, ?> adapter = findAdapter(a, true, serType);
            if (adapter != null) {
                return _converter(adapter, true);
            }
            return null;
        }

        private <A extends Annotation> A findAnnotation(Class<A> annotationClass, Annotated annotated, boolean includePackage, boolean includeClass, boolean includeSuperclasses) {
            Annotation annotation = annotated.getAnnotation(annotationClass);
            if (annotation != null) {
                return (A) annotation;
            } else {
                Class memberClass = null;
                if (annotated instanceof AnnotatedParameter) {
                    memberClass = ((AnnotatedParameter) annotated).getDeclaringClass();
                } else {
                    AnnotatedElement pkg = annotated.getAnnotated();
                    if (pkg instanceof Member) {
                        memberClass = ((Member) pkg).getDeclaringClass();
                        if (includeClass) {
                            annotation = memberClass.getAnnotation(annotationClass);
                            if (annotation != null) {
                                return (A) annotation;
                            }
                        }
                    } else {
                        if (!(pkg instanceof Class)) {
                            throw new IllegalStateException("Unsupported annotated member: " + annotated.getClass().getName());
                        }

                        memberClass = (Class) pkg;
                    }
                }

                if (memberClass != null) {
                    if (includeSuperclasses) {
                        for (Class pkg1 = memberClass.getSuperclass(); pkg1 != null && pkg1 != Object.class; pkg1 = pkg1.getSuperclass()) {
                            annotation = pkg1.getAnnotation(annotationClass);
                            if (annotation != null) {
                                return (A) annotation;
                            }
                        }
                    }

                    if (includePackage) {
                        Package pkg2 = memberClass.getPackage();
                        if (pkg2 != null) {
                            return memberClass.getPackage().getAnnotation(annotationClass);
                        }
                    }
                }

                return null;
            }
        }

        private XmlAdapter<Object, Object> findAdapter(Annotated am, boolean forSerialization, Class<?> type) {
            // First of all, are we looking for annotations for class?
            if (am instanceof AnnotatedClass) {
                return findAdapterForClass((AnnotatedClass) am, forSerialization);
            }
            // Otherwise for a member. First, let's figure out type of property
            XmlJavaTypeAdapter adapterInfo = findAnnotation(XmlJavaTypeAdapter.class, am, true, false, false);
            if (adapterInfo != null) {
                XmlAdapter<Object, Object> adapter = checkAdapter(adapterInfo, type, forSerialization);
                if (adapter != null) {
                    return adapter;
                }
            }
            XmlJavaTypeAdapters adapters = findAnnotation(XmlJavaTypeAdapters.class, am, true, false, false);
            if (adapters != null) {
                for (XmlJavaTypeAdapter info : adapters.value()) {
                    XmlAdapter<Object, Object> adapter = checkAdapter(info, type, forSerialization);
                    if (adapter != null) {
                        return adapter;
                    }
                }
            }
            return null;
        }

        private final XmlAdapter<Object, Object> checkAdapter(XmlJavaTypeAdapter adapterInfo, Class<?> typeNeeded, boolean forSerialization) {
            // if annotation has no type, it's applicable; if it has, must match
            Class<?> adaptedType = adapterInfo.type();

            if (adapterInfo.value().isAssignableFrom(JaxbUnknownAdapter.class)) {
                return null;
            }

            if (adaptedType == XmlJavaTypeAdapter.DEFAULT.class) {
                JavaType[] params = _typeFactory.findTypeParameters(adapterInfo.value(), XmlAdapter.class);
                adaptedType = params[1].getRawClass();
            }
            if (adaptedType.isAssignableFrom(typeNeeded)) {
                @SuppressWarnings("rawtypes")
                Class<? extends XmlAdapter> cls = adapterInfo.value();
                return ClassUtil.createInstance(cls, true);
            }
            return null;
        }

        @SuppressWarnings("unchecked")
        private XmlAdapter<Object, Object> findAdapterForClass(AnnotatedClass ac, boolean forSerialization) {
            XmlJavaTypeAdapter adapterInfo = ac.getAnnotated().getAnnotation(XmlJavaTypeAdapter.class);
            if (adapterInfo != null) {
                @SuppressWarnings("rawtypes")
                Class<? extends XmlAdapter> cls = adapterInfo.value();
                return ClassUtil.createInstance(cls, true);
            }
            return null;
        }
    }

    /**
     * Simple utility Serializer which can be used to override replacement of JaxbUnknownAdapter with WrappingObjectSerializer
     */
    public static class PassThruSerializer extends JsonSerializer<Object> {

        @Override
        public void serialize(Object p0, JsonGenerator p1, SerializerProvider p2) throws IOException, JsonProcessingException {
            p1.writeObject(p0);
        }
    }

    class CustomObjectSerializer extends JsonSerializer<Object> {

        private ObjectMapper customObjectMapper;

        public CustomObjectSerializer(ObjectMapper customObjectMapper) {
            this.customObjectMapper = customObjectMapper;
        }

        @Override
        public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {

            String json = customObjectMapper.writeValueAsString(value);
            jgen.writeRawValue(json);
        }
    }

    class WrappingObjectSerializer extends JsonSerializer<Object> {

        private ObjectMapper customObjectMapper;

        public WrappingObjectSerializer(ObjectMapper customObjectMapper) {
            this.customObjectMapper = customObjectMapper;
        }

        @Override
        public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
            String className = value.getClass().getName();

            if (value instanceof Collection) {
                String collectionJson = writeCollection((Collection) value, customObjectMapper);
                jgen.writeRawValue(collectionJson);
            } else if (value instanceof Map) {
                String mapJson = writeMap((Map) value, customObjectMapper);
                jgen.writeRawValue(mapJson);
            } else if (value instanceof Object[] || value.getClass().isArray()) {
                String arrayJson = writeArray((Object[]) value, customObjectMapper);
                jgen.writeRawValue(arrayJson);
            } else {

                String json = customObjectMapper.writeValueAsString(value);

                // don't wrap java and javax classes as they are always available, in addition avoid double wrapping
                if (!className.startsWith("java.") && !className.startsWith("javax.") && !json.contains(className)) {
                    json = "{\"" + className + "\":" + json + "}";
                }
                jgen.writeRawValue(json);
            }
        }

        private String writeArray(Object[] value, ObjectMapper customObjectMapper) throws IOException {
            StringBuilder builder = new StringBuilder();
            builder.append("[");

            int size = Array.getLength(value);

            for (Object element : value) {
                size--;
                String elementClassName = element.getClass().getName();
                String json = customObjectMapper.writeValueAsString(element);

                // don't wrap java and javax classes as they are always available, in addition avoid double wrapping
                if (!elementClassName.startsWith("java.") && !elementClassName.startsWith("javax.") && !json.contains(elementClassName)) {
                    json = "{\"" + elementClassName + "\":" + json + "}";
                }

                builder.append(json);

                if (size > 0) {
                    builder.append(",");
                }
            }
            builder.append("]");

            return builder.toString();
        }

        private String writeMap(Map value, ObjectMapper customObjectMapper) throws IOException {
            StringBuilder builder = new StringBuilder();
            builder.append("{");

            int size = ((Map<?, ?>) value).size();

            for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
                size--;
                // handle map key
                Object key = entry.getKey();
                String keyClassName = key.getClass().getName();
                String json = customObjectMapper.writeValueAsString(key);

                // don't wrap java and javax classes as they are always available, in addition avoid double wrapping
                if (!keyClassName.startsWith("java.") && !keyClassName.startsWith("javax.") && !json.contains(keyClassName)) {
                    json = "{\"" + keyClassName + "\":" + json + "}";
                }
                // handle map value
                Object mValue = entry.getValue();
                String mValueClassName = mValue.getClass().getName();
                String jsonValue = customObjectMapper.writeValueAsString(mValue);

                // don't wrap java and javax classes as they are always available, in addition avoid double wrapping
                if (!mValueClassName.startsWith("java.") && !mValueClassName.startsWith("javax.") && !json.contains(mValueClassName)) {
                    jsonValue = "{\"" + mValueClassName + "\":" + jsonValue + "}";
                }

                // add as JSON map
                builder.append(json);
                builder.append(" : ");
                builder.append(jsonValue);

                if (size > 0) {
                    builder.append(",");
                }
            }

            builder.append("}");
            return builder.toString();
        }

        private String writeCollection(Collection collection, ObjectMapper customObjectMapper) throws IOException {

            StringBuilder builder = new StringBuilder();
            builder.append("[");

            int size = collection.size();
            Iterator it = collection.iterator();
            while (it.hasNext()) {
                size--;
                Object element = it.next();
                String elementClassName = element.getClass().getName();
                String json = customObjectMapper.writeValueAsString(element);

                // don't wrap java and javax classes as they are always available, in addition avoid double wrapping
                if (!elementClassName.startsWith("java.") && !elementClassName.startsWith("javax.") && !json.contains(elementClassName)) {
                    json = "{\"" + elementClassName + "\":" + json + "}";
                }

                builder.append(json);

                if (size > 0) {
                    builder.append(",");
                }
            }
            builder.append("]");

            return builder.toString();
        }
    }

    class CustomObjectDeserializer extends UntypedObjectDeserializer {

        private final Pattern VALID_JAVA_IDENTIFIER = Pattern.compile("(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*\\.)*\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*");

        private static final long serialVersionUID = 7764405880012867708L;

        private Map<String, Class<?>> classes = new HashMap<String, Class<?>>();

        public CustomObjectDeserializer(Set<Class<?>> classes) {

            for (Class<?> c : classes) {
                this.classes.put(c.getSimpleName(), c);
                this.classes.put(c.getName(), c);
            }
        }

        @Override
        protected Object mapObject(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            JsonToken t = jp.getCurrentToken();
            if (t == JsonToken.START_OBJECT) {
                t = jp.nextToken();
            }
            // 1.6: minor optimization; let's handle 1 and 2 entry cases separately
            if (t != JsonToken.FIELD_NAME) { // and empty one too
                // empty map might work; but caller may want to modify... so better just give small modifiable
                return new LinkedHashMap<String, Object>(4);
            }
            String field1 = jp.getText();
            jp.nextToken();
            if (classes.containsKey(field1)) {
                stripped.set(true);
                Object value = deserializeObjectMapper.readValue(jp, classes.get(field1));
                jp.nextToken();

                return value;
            } else {
                if (isFullyQualifiedClassname(field1)) {
                    try {
                        Object value = deserializeObjectMapper.readValue(jp, classLoader.loadClass(field1));
                        jp.nextToken();

                        return value;
                    } catch (ClassNotFoundException e) {
                    }
                }

                Object value1 = deserialize(jp, ctxt);
                if (jp.nextToken() != JsonToken.FIELD_NAME) { // single entry; but we want modifiable
                    LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(4);
                    result.put(field1, value1);
                    return result;
                }
                String field2 = jp.getText();
                jp.nextToken();
                Object value2 = deserialize(jp, ctxt);
                if (jp.nextToken() != JsonToken.FIELD_NAME) {
                    LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(4);
                    result.put(field1, value1);
                    result.put(field2, value2);
                    return result;
                }

                // And then the general case; default map size is 16
                LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>();
                result.put(field1, value1);
                result.put(field2, value2);
                do {
                    String fieldName = jp.getText();
                    jp.nextToken();
                    result.put(fieldName, deserialize(jp, ctxt));
                } while (jp.nextToken() != JsonToken.END_OBJECT);

                // in case nested jaxb list wrapper was not recognized automatically map it manually
                if (result.containsKey("type") && result.containsKey("componentType") && result.containsKey("element")) {
                    JaxbListWrapper wrapper = new JaxbListWrapper();
                    wrapper.setType(JaxbListWrapper.JaxbWrapperType.valueOf((String) result.get("type")));
                    wrapper.setComponentType((String) result.get("componentType"));
                    wrapper.setElements(toArray(result.get("element")));

                    try {
                        Object data = null;
                        if (wrapper.getType().equals(JaxbListWrapper.JaxbWrapperType.MAP)) {
                            Map<Object, Object> tranformed = new LinkedHashMap<Object, Object>();
                            // this is mapped to JaxbStringObjectPair
                            for (Object element : wrapper.getElements()) {
                                Map<Object, Object> map = (Map<Object, Object>) element;
                                tranformed.put(map.get("key"), map.get("value"));
                            }
                            data = tranformed;
                        } else {
                            data = new JaxbListAdapter().unmarshal(wrapper);
                        }
                        return data;
                    } catch (Exception e) {

                    }
                }
                return result;
            }
        }

        private Object[] toArray(Object element) {
            if (element != null) {

                if (element instanceof Collection) {
                    return ((Collection) element).toArray();
                }
            }

            return new Object[0];
        }

        private boolean isFullyQualifiedClassname(String classname) {

            if (!classname.contains(".")) {
                return false;
            }

            return VALID_JAVA_IDENTIFIER.matcher(classname).matches();
        }
    }

    class CustomAsWrapperTypeDeserializer extends AsWrapperTypeDeserializer {

        public CustomAsWrapperTypeDeserializer(JavaType bt, TypeIdResolver idRes, String typePropertyName, boolean typeIdVisible, JavaType defaultImpl) {
            super(bt, idRes, typePropertyName, typeIdVisible, defaultImpl);
        }

        protected CustomAsWrapperTypeDeserializer(AsWrapperTypeDeserializer src, BeanProperty property) {
            super(src, property);
        }

        @Override
        public Object deserializeTypedFromArray(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            ClassLoader current = Thread.currentThread().getContextClassLoader();
            try {
                Thread.currentThread().setContextClassLoader(_baseType.getRawClass().getClassLoader());
                JsonDeserializer<Object> deser = _findDeserializer(ctxt, baseTypeName());
                Object value = deser.deserialize(jp, ctxt);
                return value;
            } finally {
                Thread.currentThread().setContextClassLoader(current);
            }
        }

        @Override
        public Object deserializeTypedFromObject(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            ClassLoader current = Thread.currentThread().getContextClassLoader();
            try {
                Thread.currentThread().setContextClassLoader(_baseType.getRawClass().getClassLoader());
                if (classesSet.contains(_baseType.getRawClass()) && !stripped.get()) {

                    try {
                        return super.deserializeTypedFromObject(jp, ctxt);
                    } catch (Exception e) {
                        JsonDeserializer<Object> deser = _findDeserializer(ctxt, baseTypeName());
                        Object value = deser.deserialize(jp, ctxt);
                        return value;
                    }
                }
                stripped.set(false);
                JsonDeserializer<Object> deser = _findDeserializer(ctxt, baseTypeName());
                Object value = deser.deserialize(jp, ctxt);
                return value;
            } finally {
                Thread.currentThread().setContextClassLoader(current);
            }
        }

        @Override
        public Object deserializeTypedFromScalar(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            ClassLoader current = Thread.currentThread().getContextClassLoader();
            try {
                Thread.currentThread().setContextClassLoader(_baseType.getRawClass().getClassLoader());
                if (classesSet.contains(_baseType.getRawClass())) {
                    try {
                        return super.deserializeTypedFromScalar(jp, ctxt);
                    } catch (Exception e) {
                        JsonDeserializer<Object> deser = _findDeserializer(ctxt, baseTypeName());
                        Object value = deser.deserialize(jp, ctxt);
                        return value;
                    }
                }
                JsonDeserializer<Object> deser = _findDeserializer(ctxt, baseTypeName());
                Object value = deser.deserialize(jp, ctxt);
                return value;
            } finally {
                Thread.currentThread().setContextClassLoader(current);
            }
        }

        @Override
        public Object deserializeTypedFromAny(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            ClassLoader current = Thread.currentThread().getContextClassLoader();
            try {
                Thread.currentThread().setContextClassLoader(_baseType.getRawClass().getClassLoader());
                if (classesSet.contains(_baseType.getRawClass())) {
                    try {
                        return super.deserializeTypedFromAny(jp, ctxt);
                    } catch (Exception e) {
                        JsonDeserializer<Object> deser = _findDeserializer(ctxt, baseTypeName());
                        Object value = deser.deserialize(jp, ctxt);
                        return value;
                    }
                }

                if (_baseType.isMapLikeType() && jp.getCurrentToken() == JsonToken.START_ARRAY) {

                    LinkedHashMap<Object, Object> data = new LinkedHashMap<Object, Object>();
                    jp.nextToken();

                    if (jp.getCurrentToken() == JsonToken.END_ARRAY) {
                        return data;
                    }
                    JsonDeserializer<Object> deser = _findDeserializer(ctxt, LinkedHashMap.class.getName());
                    Map<Object, Object> value = (Map) deser.deserialize(jp, ctxt);
                    jp.nextToken();

                    if (value != null) {
                        Collection<Object> values = value.values();
                        if (values.size() == 2) {

                            Iterator<Object> it = values.iterator();
                            data.put(it.next(), it.next());

                            return data;
                        }
                    }
                    return value;
                } else {
                    JsonDeserializer<Object> deser = _findDeserializer(ctxt, baseTypeName());
                    Object value = deser.deserialize(jp, ctxt);

                    return value;
                }
            } finally {
                Thread.currentThread().setContextClassLoader(current);
            }
        }

        @Override
        public TypeDeserializer forProperty(BeanProperty prop) {
            if (prop != null) {
                if (useForType(prop.getType()) || useForType(prop.getType().getContentType())) {
                    return new CustomAsWrapperTypeDeserializer(this, prop);
                }
            }
            return super.forProperty(prop);
        }

        boolean useForType(JavaType t) {
            if (classesSet.contains(t.getRawClass())) {
                return true;
            }
            return false;
        }
    }

    @Override
    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    public ClassLoader getClassLoader() {
        return classLoader;
    }
}
