org.fastmongo.odm.dbobject.mapping.core.ConverterHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.fastmongo.odm.dbobject.mapping.core.ConverterHelper.java

Source

/*
 * Copyright (c) 2014 Alexander Gulko <kirhog at gmail dot 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.
 */

package org.fastmongo.odm.dbobject.mapping.core;

import com.mongodb.DBObject;
import org.apache.commons.lang.StringUtils;
import org.fastmongo.odm.document.support.LazyCollectionProxy;
import org.fastmongo.odm.document.support.LazyProxy;
import org.fastmongo.odm.mapping.core.Loader;
import org.fastmongo.odm.mapping.core.Replacer;
import org.fastmongo.odm.mapping.core.proxy.AsmProxyFactory;
import org.fastmongo.odm.mapping.core.proxy.JdkProxyFactory;
import org.fastmongo.odm.mapping.core.proxy.ProxyFactory;
import org.fastmongo.odm.mapping.core.proxy.ProxyInvocationHandler;
import org.fastmongo.odm.mapping.exception.MappingException;
import org.fastmongo.odm.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * Utility methods for {@link DbObjectToDomainConverter} and {@link DomainToDbObjectConverter} converters.
 *
 * @author Alexander Gulko
 */
public abstract class ConverterHelper {
    /**
     * Id field for the domain objects.
     */
    public static final String ID_KEY = "id";

    /**
     * Used to specify the class for a document in mongo.
     */
    public static final String CLASS_KEY = "_class";

    /**
     * Points to the name of collection of referenced document.
     */
    public static final String LINK_KEY = "_link";

    /**
     * Used as a separator in path inside one mongo document.
     */
    private static final String SEPARATOR = ".";

    /**
     * MongoDB doesn't support dots as part of a field name.
     * This regex is used to replace them to '{@value #DOT_REPLACEMENT}'.
     */
    private static final Pattern DOT_REGEX = Pattern.compile("\\.");

    /**
     * Dots within a field name will be replaced to this character.
     */
    private static final char DOT_REPLACEMENT = '+';

    /**
     * Used to not converting every time char to string.
     */
    private static final String DOT_REPLACEMENT_STRING = String.valueOf(DOT_REPLACEMENT);

    /**
     * Cache for {@link #getClassFields(Class)} method.
     */
    private static final Map<Class<?>, List<Field>> CLASS_FIELDS = new ConcurrentHashMap<>();

    /**
     * Cache for {@link #loadClass(String)} method.
     */
    private static final Map<String, Class<?>> CLASS_BY_NAME = new ConcurrentHashMap<>();

    /**
     * Cache for {@link #getIdValue(Object)} method.
     */
    private static final Map<Class<?>, Field> ID_FIELD_BY_CLASS = new ConcurrentHashMap<>();

    /**
     * Cache for {@link #getFieldGenericTypes(java.lang.reflect.Field)} method.
     */
    private static final Map<Field, Type[]> GENERIC_TYPES_BY_FIELD = new ConcurrentHashMap<>();

    /**
     * Cache for {@link #getProxyFactory(Class)} method.
     */
    private static final Map<Class<?>, ProxyFactory> PROXY_FACTORY_BY_CLASS = new ConcurrentHashMap<>();

    /**
     * Prefix for class names that have different prefix than <tt>classNamePrefix</tt>.
     */
    private static final String SPECIAL_CLASS_PREFIX = "_";

    private static final boolean proxyTargetClass = !Boolean
            .parseBoolean(System.getProperty("useJdkProxy", "false"));

    private ConverterHelper() {
        throw new UnsupportedOperationException("This utility class is not for instantiation");
    }

    /**
     * Escape the specified string by replacing all dots with '{@value #DOT_REPLACEMENT}'.
     *
     * @param value the string value.
     * @return escaped string.
     */
    public static String escape(String value) {
        return DOT_REGEX.matcher(value).replaceAll(DOT_REPLACEMENT_STRING);
    }

    /**
     * Revert escaped string by replacing '{@value #DOT_REPLACEMENT}' to dots.
     * Used for field names in document.
     *
     * @param value the escaped string.
     * @return restored string.
     */
    public static String unescape(String value) {
        int length = value.length();
        for (int i = 0; i < length; ++i) {
            char c = value.charAt(i);
            if (c == DOT_REPLACEMENT) {
                // Convert to char array only if we found an escaped character.
                // This is an optimized version as usually we have field name without dots.
                return unescape(value.toCharArray(), i);
            }
        }

        return value;
    }

    /**
     * Replaces {@value #DOT_REPLACEMENT} to dots starting at the specified index.
     *
     * @param value the string that should be processed.
     * @param start the start index for processing.
     * @return the original string.
     */
    private static String unescape(char[] value, int start) {
        int length = value.length;
        for (int i = start; i < length; ++i) {
            char c = value[i];
            if (c == '+') {
                value[i] = '.';
            }
        }

        return new String(value);
    }

    /**
     * Returns the field value of the given object.
     *
     * @param field the object's field.
     * @param obj   the object with this field.
     * @return the field value of the given object.
     */
    public static Object getFieldValue(Field field, Object obj) {
        try {
            makeFieldAccessible(field);
            return field.get(obj);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Can't get value from field " + field, e);
        }
    }

    /**
     * Sets the given object field to the specified value.
     *
     * @param field the object's field.
     * @param obj   the object.
     * @param value the new value.
     */
    public static void setFieldValue(Field field, Object obj, Object value) {
        try {
            makeFieldAccessible(field);
            field.set(obj, value);
        } catch (IllegalAccessException e) {
            throw new MappingException("Can't set field " + field, e);
        }
    }

    /**
     * Make the given field accessible if it wasn't.
     *
     * @param field the field
     */
    private static void makeFieldAccessible(Field field) {
        if (!field.isAccessible()) {
            field.setAccessible(true);
        }
    }

    /**
     * Creates new instance with the given class name.
     *
     * @param className the name of the class object.
     * @param <T>       the type of the class.
     * @return the instance of the given class.
     */
    @SuppressWarnings("unchecked")
    public static <T> T newInstance(String className) {
        return (T) newInstance(loadClass(className));
    }

    /**
     * Creates new instance of the given class.
     *
     * @param clazz the class object.
     * @param <T>   the type of the class.
     * @return the instance of the given class.
     */
    public static <T> T newInstance(Class<T> clazz) {
        try {
            return clazz.newInstance();
        } catch (Exception e) {
            throw new RuntimeException("Can't instantiate class " + clazz, e);
        }
    }

    /**
     * Loads Class with the specified name.
     *
     * @param className the class name.
     * @return the loaded class object.
     */
    public static Class<?> loadClass(String className) {
        Class<?> clazz = CLASS_BY_NAME.get(className);
        if (clazz == null) {
            try {
                clazz = Class.forName(className);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException("Can't find class " + className, e);
            }
            CLASS_BY_NAME.put(className, clazz);
        }

        return clazz;
    }

    /**
     * Returns all fields of the given class.
     * <p/>
     * Skips transient and static fields.
     *
     * @param clazz the class.
     */
    public static List<Field> getClassFields(Class<?> clazz) {
        List<Field> fields = CLASS_FIELDS.get(clazz);
        if (fields == null) {
            final List<Field> newFields = new ArrayList<>();
            ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() {
                @Override
                public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
                    newFields.add(field);
                }
            }, new ReflectionUtils.FieldFilter() {
                @Override
                public boolean matches(Field field) {
                    return !Modifier.isTransient(field.getModifiers()) && !Modifier.isStatic(field.getModifiers());
                }
            });
            fields = Collections.unmodifiableList(newFields);
            CLASS_FIELDS.put(clazz, fields);
        }

        return fields;
    }

    /**
     * Returns an array of Type objects representing the actual type arguments of this field.
     *
     * @param field the parameterized field.
     * @return type arguments of the given field.
     */
    public static Type[] getFieldGenericTypes(Field field) {
        Type[] types = GENERIC_TYPES_BY_FIELD.get(field);
        if (types == null) {
            types = ((ParameterizedType) field.getGenericType()).getActualTypeArguments();
            GENERIC_TYPES_BY_FIELD.put(field, types);
        }

        return types;
    }

    /**
     * Reads value of Id field in the given object.
     *
     * @param obj the object, not <tt>null</tt>.
     * @return value of Id field.
     */
    public static Object getIdValue(Object obj) {
        Field idField = ID_FIELD_BY_CLASS.get(obj.getClass());
        if (idField == null) {
            idField = ReflectionUtils.findField(obj.getClass(), ID_KEY);
            ID_FIELD_BY_CLASS.put(obj.getClass(), idField);
        }

        return getFieldValue(idField, obj);
    }

    /**
     * Creates proxy for the field of the given class.
     * Each proxy implements the {@link LazyProxy} interface.
     * And proxies for {@link java.util.Collection}s implement the {@link LazyCollectionProxy} interface.
     * <p/>
     * The loader callback is called during all methods invocations, except the following:
     * <ul>
     * <li><tt>equals(Object)</tt></li>
     * <li><tt>hashCode()</tt></li>
     * </ul>
     * <p/>
     * Also calls <tt>getId()</tt> on domain objects and <tt>getIds()</tt> on {@link LazyCollectionProxy}
     * are redirected to {@link org.fastmongo.odm.mapping.core.ObjectLoader#getId()} and {@link org.fastmongo.odm.mapping.core.CollectionLoader#getIds()} respectively.
     * <p/>
     * The call {@link LazyProxy#unwrap()} will load and return a "real" object.
     * <p/>
     * <p/>
     * The {@link org.fastmongo.odm.mapping.core.Replacer} callback is used to remove overhead of proxies and unnecessary references.
     *
     * @param clazz    the class of the field.
     * @param loader   the callback to load actual ("real") data.
     * @param replacer the callback to replace proxied field value to loaded value. Nullable.
     * @param <T>      the type of the class.
     * @return the proxy for the class.
     * @see org.fastmongo.odm.mapping.core.proxy.ProxyInvocationHandler
     * @see org.fastmongo.odm.mapping.core.ObjectLoader
     * @see org.fastmongo.odm.mapping.core.CollectionLoader
     * @see LazyProxy
     * @see LazyCollectionProxy
     */
    @SuppressWarnings("unchecked")
    public static <T> T newProxy(final Class<T> clazz, final Loader loader, final Replacer replacer) {
        ProxyFactory factory = getProxyFactory(clazz);

        return (T) factory.newInstance(new ProxyInvocationHandler(loader, replacer));
    }

    /**
     * Returns a factory that creates proxies for the given class.
     *
     * @param clazz the class of the field for which proxy is created.
     * @return the proxy factory.
     */
    private static ProxyFactory getProxyFactory(Class<?> clazz) {
        ProxyFactory factory = PROXY_FACTORY_BY_CLASS.get(clazz);
        if (factory == null) {
            factory = proxyTargetClass ? new AsmProxyFactory(clazz) : new JdkProxyFactory(clazz);

            PROXY_FACTORY_BY_CLASS.put(clazz, factory);
        }

        return factory;
    }

    /**
     * Returns value of the given DBObject by the specified path.
     *
     * @param db   the DBObject.
     * @param path the value's path.
     * @param <T>  the type of returned value.
     * @return the value.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getValue(DBObject db, String path) {
        Object current = db;
        for (String key : DOT_REGEX.split(path)) {
            if (current == null) {
                return null;
            }

            current = ((DBObject) current).get(key);
        }

        return (T) current;
    }

    /**
     * Extract Ids from the input {@link com.mongodb.DBObject}s.
     *
     * @param dbs the collection of {@link com.mongodb.DBObject}s.
     * @param <T> the type of Id field.
     * @return list of Ids.
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> getIds(Collection<Object> dbs) {
        List<T> ids = new ArrayList<>();
        for (Object obj : dbs) {
            DBObject db = (DBObject) obj;
            ids.add((T) db.get(ID_KEY));
        }

        return ids;
    }

    /**
     * Joins the elements of the provided array into a single String containing the provided list of elements
     * separated by dot.
     *
     * @param params the array of values to join together.
     * @return the joined string.
     */
    public static String join(Object... params) {
        return StringUtils.join(params, SEPARATOR);
    }

    /**
     * Returns the given class name with removed class name prefix.
     * If the class name doesn't start with the given prefix,
     * special prefix '{@value #SPECIAL_CLASS_PREFIX}' will be used.
     *
     * @param type            the class.
     * @param classNamePrefix the class name prefix to be removed, not <tt>null</tt>.
     * @return the class name without prefix.
     */
    public static String cleanClassName(Class<?> type, String classNamePrefix) {
        String className = type.getName();
        if (className.startsWith(classNamePrefix)) {
            className = className.substring(classNamePrefix.length());
        } else {
            className = SPECIAL_CLASS_PREFIX + className;
        }

        return className;
    }

    /**
     * Returns the given class name with the specified prefix appended.
     *
     * @param className       the class name.
     * @param classNamePrefix the class name prefix to be appended.
     * @return the full class name.
     */
    public static String restoreClassName(String className, String classNamePrefix) {
        return !className.startsWith(SPECIAL_CLASS_PREFIX) ? classNamePrefix + className
                : className.substring(SPECIAL_CLASS_PREFIX.length());
    }
}