com.erudika.para.core.ParaObjectUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.erudika.para.core.ParaObjectUtils.java

Source

/*
 * Copyright 2013-2015 Erudika. http://erudika.com
 *
 * 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.
 *
 * For issues and patches go to: https://github.com/erudika
 */
package com.erudika.para.core;

import com.erudika.para.annotations.Locked;
import com.erudika.para.annotations.Stored;
import com.erudika.para.utils.Config;
import com.erudika.para.utils.Utils;
import static com.erudika.para.utils.Utils.getAllDeclaredFields;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.bidimap.DualHashBidiMap;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.util.ClassUtils;

/**
 * Contains methods for object/grid mapping, JSON serialization, class scanning and resolution.
 * @author Alex Bogdanovski [alex@erudika.com]
 */
@SuppressWarnings("unchecked")
public final class ParaObjectUtils {

    private static final Logger logger = LoggerFactory.getLogger(ParaObjectUtils.class);
    // maps plural to singular type definitions
    private static final Map<String, String> coreTypes = new DualHashBidiMap();
    // maps lowercase simple names to class objects
    private static final Map<String, Class<? extends ParaObject>> coreClasses = new DualHashBidiMap();
    private static final CoreClassScanner scanner = new CoreClassScanner();
    private static final ObjectMapper jsonMapper = new ObjectMapper();
    private static final ObjectReader jsonReader;
    private static final ObjectWriter jsonWriter;

    static {
        jsonMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        jsonMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        jsonMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
        jsonMapper.enable(SerializationFeature.INDENT_OUTPUT);
        jsonMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        jsonMapper.disable(SerializationFeature.WRITE_NULL_MAP_VALUES);
        jsonReader = jsonMapper.reader();
        jsonWriter = jsonMapper.writer();
    }

    private ParaObjectUtils() {
    }

    /**
    * A Jackson {@code ObjectMapper}.
    *
    * @return JSON object mapper
    */
    public static ObjectMapper getJsonMapper() {
        return jsonMapper;
    }

    /**
     * A Jackson JSON reader.
     *
     * @param type the type to read
     * @return JSON object reader
     */
    public static ObjectReader getJsonReader(Class<?> type) {
        return jsonReader.forType(type);
    }

    /**
     * A Jackson JSON writer. Pretty print is on.
     *
     * @return JSON object writer
     */
    public static ObjectWriter getJsonWriter() {
        return jsonWriter;
    }

    /**
     * A Jackson JSON writer. Pretty print is off.
     *
     * @return JSON object writer with indentation disabled
     */
    public static ObjectWriter getJsonWriterNoIdent() {
        return jsonWriter.without(SerializationFeature.INDENT_OUTPUT);
    }

    /////////////////////////////////////////////
    //        OBJECT MAPPING & CLASS UTILS
    /////////////////////////////////////////////

    /**
     * Populates an object with an array of query parameters (dangerous!).
     * <b>This method might be deprecated in the future.</b>
     *
     * @param <P> the object type
     * @param transObject an object
     * @param paramMap a query parameters map
     */
    public static <P extends ParaObject> void populate(P transObject, Map<String, String[]> paramMap) {
        if (transObject == null || paramMap == null || paramMap.isEmpty()) {
            return;
        }
        Class<Locked> locked = (paramMap.containsKey(Config._ID)) ? Locked.class : null;
        Map<String, Object> fields = getAnnotatedFields(transObject, locked);
        Map<String, Object> data = new HashMap<String, Object>();
        // populate an object with converted param values from param map.
        try {
            for (Map.Entry<String, String[]> ks : paramMap.entrySet()) {
                String param = ks.getKey();
                String[] values = ks.getValue();
                String value = (values.length > 1) ? getJsonWriter().writeValueAsString(values) : values[0];
                if (fields.containsKey(param)) {
                    data.put(param, value);
                }
            }
            setAnnotatedFields(transObject, data, locked);
        } catch (Exception ex) {
            logger.error(null, ex);
        }
    }

    /**
     * Returns a map of the core data types.
     * @return a map of type plural - type singular form
     */
    public static Map<String, String> getCoreTypes() {
        if (coreTypes.isEmpty()) {
            try {
                for (Class<? extends ParaObject> clazz : ParaObjectUtils.getCoreClassesMap().values()) {
                    ParaObject p = clazz.newInstance();
                    coreTypes.put(p.getPlural(), p.getType());
                }
            } catch (Exception ex) {
                logger.error(null, ex);
            }
        }
        return Collections.unmodifiableMap(coreTypes);
    }

    /**
     * Returns a map of all registered types.
     * @param app the app to search for custom types
     * @return a map of plural - singular form of type names
     */
    public static Map<String, String> getAllTypes(App app) {
        Map<String, String> map = new HashMap<String, String>(getCoreTypes());
        if (app != null) {
            map.putAll(app.getDatatypes());
        }
        return map;
    }

    /**
     * Checks if the type of an object matches its real Class name.
     *
     * @param so an object
     * @return true if the types match
     */
    public static boolean typesMatch(ParaObject so) {
        return (so == null) ? false : so.getClass().equals(toClass(so.getType()));
    }

    /**
     * @see #getAnnotatedFields(com.erudika.para.core.ParaObject, java.lang.Class, boolean)
     * @param <P> the object type
     * @param pojo the object to convert to a map
     * @return a map of fields and their values
     */
    public static <P extends ParaObject> Map<String, Object> getAnnotatedFields(P pojo) {
        return getAnnotatedFields(pojo, null);
    }

    /**
     * @see #getAnnotatedFields(com.erudika.para.core.ParaObject, java.lang.Class, boolean)
     * @param <P> the object type
     * @param pojo the object to convert to a map
     * @param filter a filter annotation. fields that have it will be skipped
     * @return a map of fields and their values
     */
    public static <P extends ParaObject> Map<String, Object> getAnnotatedFields(P pojo,
            Class<? extends Annotation> filter) {
        return getAnnotatedFields(pojo, filter, true);
    }

    /**
     * Returns a map of annotated fields of a domain object. Only annotated fields are returned. This method forms the
     * basis of an Object/Grid Mapper. It converts an object to a map of key/value pairs. That map can later be
     * persisted to a data store.
     * <br>
     * If {@code convertNestedToJsonString} is true all field values that are objects (i.e. not primitive types or
     * wrappers) are converted to a JSON string otherwise they are left as they are and will be serialized as regular
     * JSON objects later (structure is preserved). Null is considered a primitive type. Transient fields and
     * serialVersionUID are skipped.
     *
     * @param <P> the object type
     * @param pojo the object to convert to a map
     * @param filter a filter annotation. fields that have it will be skipped
     * @param convertNestedToJsonString true if you want to flatten the nested objects to a JSON string.
     * @return a map of fields and their values
     */
    public static <P extends ParaObject> Map<String, Object> getAnnotatedFields(P pojo,
            Class<? extends Annotation> filter, boolean convertNestedToJsonString) {
        HashMap<String, Object> map = new HashMap<String, Object>();
        if (pojo == null) {
            return map;
        }
        try {
            List<Field> fields = getAllDeclaredFields(pojo.getClass());
            // filter transient fields and those without annotations
            for (Field field : fields) {
                boolean dontSkip = ((filter == null) ? true : !field.isAnnotationPresent(filter));
                if (field.isAnnotationPresent(Stored.class) && dontSkip) {
                    String name = field.getName();
                    Object value = PropertyUtils.getProperty(pojo, name);
                    if (!Utils.isBasicType(field.getType()) && convertNestedToJsonString) {
                        value = getJsonWriterNoIdent().writeValueAsString(value);
                    }
                    map.put(name, value);
                }
            }
        } catch (Exception ex) {
            logger.error(null, ex);
        }

        return map;
    }

    /**
     * @see #setAnnotatedFields(com.erudika.para.core.ParaObject, java.util.Map, java.lang.Class)
     * @param <P> the object type
     * @param data the map of fields/values
     * @return the populated object
     */
    public static <P extends ParaObject> P setAnnotatedFields(Map<String, Object> data) {
        return setAnnotatedFields(null, data, null);
    }

    /**
     * Converts a map of fields/values to a domain object. Only annotated fields are populated. This method forms the
     * basis of an Object/Grid Mapper.
     * <br>
     * Map values that are JSON objects are converted to their corresponding Java types. Nulls and primitive types are
     * preserved.
     *
     * @param <P> the object type
     * @param pojo the object to populate with data
     * @param data the map of fields/values
     * @param filter a filter annotation. fields that have it will be skipped
     * @return the populated object
     */
    public static <P extends ParaObject> P setAnnotatedFields(P pojo, Map<String, Object> data,
            Class<? extends Annotation> filter) {
        if (data == null || data.isEmpty()) {
            return null;
        }
        try {
            if (pojo == null) {
                // try to find a declared class in the core package
                pojo = (P) toClass((String) data.get(Config._TYPE)).getConstructor().newInstance();
            }
            List<Field> fields = getAllDeclaredFields(pojo.getClass());
            Map<String, Object> props = new HashMap<String, Object>(data);
            for (Field field : fields) {
                boolean dontSkip = ((filter == null) ? true : !field.isAnnotationPresent(filter));
                String name = field.getName();
                Object value = data.get(name);
                if (field.isAnnotationPresent(Stored.class) && dontSkip) {
                    // try to read a default value from the bean if any
                    if (value == null && PropertyUtils.isReadable(pojo, name)) {
                        value = PropertyUtils.getProperty(pojo, name);
                    }
                    // handle complex JSON objects deserialized to Maps, Arrays, etc.
                    if (!Utils.isBasicType(field.getType()) && value instanceof String) {
                        // in this case the object is a flattened JSON string coming from the DB
                        value = getJsonReader(field.getType()).readValue(value.toString());
                    }
                    field.setAccessible(true);
                    BeanUtils.setProperty(pojo, name, value);
                }
                props.remove(name);
            }
            // handle unknown (user-defined) fields
            if (!props.isEmpty() && pojo instanceof Sysprop) {
                for (Map.Entry<String, Object> entry : props.entrySet()) {
                    String name = entry.getKey();
                    Object value = entry.getValue();
                    // handle the case where we have custom user-defined properties
                    // which are not defined as Java class fields
                    if (!PropertyUtils.isReadable(pojo, name)) {
                        if (value == null) {
                            ((Sysprop) pojo).removeProperty(name);
                        } else {
                            ((Sysprop) pojo).addProperty(name, value);
                        }
                    }
                }
            }
        } catch (Exception ex) {
            logger.error(null, ex);
            pojo = null;
        }
        return pojo;
    }

    /**
     * Constructs a new instance of a core object.
     *
     * @param <P> the object type
     * @param type the simple name of a class
     * @return a new instance of a core class. Defaults to {@link com.erudika.para.core.Sysprop}.
     * @see #toClass(java.lang.String)
     */
    public static <P extends ParaObject> P toObject(String type) {
        try {
            return (P) toClass(type).getConstructor().newInstance();
        } catch (Exception ex) {
            logger.error(null, ex);
            return null;
        }
    }

    /**
     * Converts a class name to a real Class object.
     *
     * @param type the simple name of a class
     * @return the Class object or {@link com.erudika.para.core.Sysprop} if the class was not found.
     * @see java.lang.Class#forName(java.lang.String)
     */
    public static Class<? extends ParaObject> toClass(String type) {
        return toClass(type, Sysprop.class);
    }

    /**
     * Converts a class name to a real {@link com.erudika.para.core.ParaObject} subclass. Defaults to
     * {@link com.erudika.para.core.Sysprop} if the class was not found in the core package path.
     *
     * @param type the simple name of a class
     * @param defaultClass returns this type if the requested class was not found on the classpath.
     * @return the Class object. Returns null if defaultClass is null.
     * @see java.lang.Class#forName(java.lang.String)
     * @see com.erudika.para.core.Sysprop
     */
    public static Class<? extends ParaObject> toClass(String type, Class<? extends ParaObject> defaultClass) {
        Class<? extends ParaObject> returnClass = defaultClass;
        if (StringUtils.isBlank(type) || !getCoreClassesMap().containsKey(type)) {
            return returnClass;
        }
        return getCoreClassesMap().get(type);
    }

    /**
     * Searches through the Para core package and {@code Config.CORE_PACKAGE_NAME} package for {@link ParaObject}
     * subclasses and adds their names them to the map.
     *
     * @return a map of simple class names (lowercase) to class objects
     */
    public static Map<String, Class<? extends ParaObject>> getCoreClassesMap() {
        if (coreClasses.isEmpty()) {
            try {
                Set<Class<? extends ParaObject>> s = scanner
                        .getComponentClasses(ParaObject.class.getPackage().getName());
                if (!Config.CORE_PACKAGE_NAME.isEmpty()) {
                    Set<Class<? extends ParaObject>> s2 = scanner.getComponentClasses(Config.CORE_PACKAGE_NAME);
                    s.addAll(s2);
                }

                for (Class<? extends ParaObject> coreClass : s) {
                    boolean isAbstract = Modifier.isAbstract(coreClass.getModifiers());
                    boolean isInterface = Modifier.isInterface(coreClass.getModifiers());
                    boolean isCoreObject = ParaObject.class.isAssignableFrom(coreClass);
                    if (isCoreObject && !isAbstract && !isInterface) {
                        coreClasses.put(coreClass.getSimpleName().toLowerCase(), coreClass);
                    }
                }
                logger.debug("Found {} ParaObject classes: {}", coreClasses.size(), coreClasses);
            } catch (Exception ex) {
                logger.error(null, ex);
            }
        }
        return Collections.unmodifiableMap(coreClasses);
    }

    /**
     * Helper class that lists all classes contained in a given package.
     */
    private static class CoreClassScanner extends ClassPathScanningCandidateComponentProvider {

        public CoreClassScanner() {
            super(false);
            addIncludeFilter(new AssignableTypeFilter(ParaObject.class));
        }

        public final Set<Class<? extends ParaObject>> getComponentClasses(String basePackage) {
            basePackage = (basePackage == null) ? "" : basePackage;
            Set<Class<? extends ParaObject>> classes = new HashSet<Class<? extends ParaObject>>();
            for (BeanDefinition candidate : findCandidateComponents(basePackage)) {
                try {
                    Class<? extends ParaObject> cls = (Class<? extends ParaObject>) ClassUtils
                            .resolveClassName(candidate.getBeanClassName(), ClassUtils.getDefaultClassLoader());
                    classes.add(cls);
                } catch (Exception ex) {
                    logger.error(null, ex);
                }
            }
            return classes;
        }
    }

    /**
     * Converts a JSON string to a domain object. If we can't match the JSON to a core object, we fall back to
     * {@link com.erudika.para.core.Sysprop}.
     *
     * @param <P> type of object to convert
     * @param json the JSON string
     * @return a core domain object or null if the string was blank
     */
    public static <P extends ParaObject> P fromJSON(String json) {
        if (StringUtils.isBlank(json)) {
            return null;
        }
        try {
            Map<String, Object> map = getJsonReader(Map.class).readValue(json);
            return setAnnotatedFields(map);
        } catch (Exception e) {
            logger.error(null, e);
        }
        return null;
    }

    /**
     * Converts a domain object to JSON.
     *
     * @param <P> type of object to convert
     * @param obj a domain object
     * @return the JSON representation of that object
     */
    public static <P extends ParaObject> String toJSON(P obj) {
        if (obj == null) {
            return "{}";
        }
        try {
            return getJsonWriter().writeValueAsString(obj);
        } catch (Exception e) {
            logger.error(null, e);
        }
        return "{}";
    }

}