Java tutorial
/* * 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()); } }