All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.oneandone.troilus.BeanMapper Maven / Gradle / Ivy

There is a newer version: 0.18
Show newest version
/*
 * Copyright 1&1 Internet AG, https://github.com/1and1/
 * 
 * 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 net.oneandone.troilus;



import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import com.google.common.base.Optional;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;


/**
 * bean mapper
 *
 */
class BeanMapper {
    
    private final LoadingCache, PropertiesMapper> propertiesMapperCache = CacheBuilder.newBuilder()
                                                                                               .build(new PropertiesMapperLoader());
    
    
    private static final class PropertiesMapper {
        private final Class clazz;
        private final ImmutableMap propertyWriters;
        private final ImmutableMap propertyReaders;
        
    
        public PropertiesMapper(ImmutableMap propertyReaders,  ImmutableMap propertyWriters, Class clazz) {
            this.propertyReaders = propertyReaders;
            this.propertyWriters = propertyWriters;
            this.clazz = clazz;
        }
     
      
        public ImmutableMap> toValues(Object entity, ImmutableSet namesToMap) {
            Map> values = Maps.newHashMap();
            
            for (Entry entry : propertyReaders.entrySet()) {
                if (namesToMap.isEmpty() || namesToMap.contains(entry.getKey())) {
                    Map.Entry> pair = entry.getValue().readProperty(entity);
                    values.put(pair.getKey(), pair.getValue());
                }
            }

            return ImmutableMap.copyOf(values);
        }

        
        @SuppressWarnings("unchecked")
        public  T fromValues(PropertiesSource datasource, ImmutableSet namesToMap) {
            try {
                T bean = newInstance((Constructor) clazz.getDeclaredConstructor());
                
                for (Entry entry : propertyWriters.entrySet()) {
                    if (namesToMap.isEmpty() || namesToMap.contains(entry.getKey())) {
                        entry.getValue().writeProperty(bean, datasource);
                    }
                }
                    
                return bean;
            } catch (ReflectiveOperationException e) {
                throw new RuntimeException(e);
            }
        }
        
        
        private  T newInstance(final Constructor constructor) {
            try {
                return (T) constructor.newInstance();
            } catch (ReflectiveOperationException e) {
                AccessController.doPrivileged(new SetConstructorAccessible<>(constructor));

                try {
                    return (T) constructor.newInstance();
                } catch (ReflectiveOperationException e2) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    
    
    private static final class SetConstructorAccessible implements PrivilegedAction {
        private final Constructor constructor;
        
        public SetConstructorAccessible(Constructor constructor) {
            this.constructor = constructor;
        }
        
        @Override
        public Object run() {
            constructor.setAccessible(true);
            return null;
        }
    }
    
    private static final class SetFieldAccessible implements PrivilegedAction {
        private final Field field;
        
        public SetFieldAccessible(Field field) {
            this.field = field;
        }
        
        @Override
        public Object run() {
            field.setAccessible(true);
            return null;
        }
    }  

    /**
     * @param entity       the entity to map
     * @param namesToMap   the properties names to consider 
     * @return the extracted name-value pairs
     */
    public ImmutableMap> toValues(Object entity, ImmutableSet namesToMap) {
        return getPropertiesMapper(entity.getClass()).toValues(entity, namesToMap);
    }

    /**
     * @param clazz         the object type
     * @param datasource    the data source to fetch the property values
     * @param propertyNames the property names to be considered 
     * @return the object instance
     */
    public  T fromValues(Class clazz, PropertiesSource datasource, ImmutableSet propertyNames) {
        return getPropertiesMapper(clazz).fromValues(datasource, propertyNames);
    }
    
    
    private PropertiesMapper getPropertiesMapper(Class clazz) {
        try {
            return propertiesMapperCache.get(clazz);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }
    
        
    private static final class PropertiesMapperLoader extends CacheLoader, PropertiesMapper> {

        @Override
        public PropertiesMapper load(Class clazz) throws Exception {

            // readers
            Map propertyReaders = Maps.newHashMap();
            propertyReaders.putAll(fetchJEEFieldReaders(ImmutableSet.copyOf(clazz.getFields())));
            propertyReaders.putAll(fetchJEEFieldReaders(ImmutableSet.copyOf(clazz.getDeclaredFields())));
            propertyReaders.putAll(fetchCassandraMapperFieldReaders(ImmutableSet.copyOf(clazz.getFields())));
            propertyReaders.putAll(fetchCassandraMapperFieldReaders(ImmutableSet.copyOf(clazz.getDeclaredFields())));
            propertyReaders.putAll(fetchFieldReaders(ImmutableSet.copyOf(clazz.getFields())));
            propertyReaders.putAll(fetchFieldReaders(ImmutableSet.copyOf(clazz.getDeclaredFields())));
     
            // writers
            Map propertyWriters = Maps.newHashMap();
            propertyWriters.putAll(fetchJEEFieldWriters(ImmutableSet.copyOf(clazz.getFields())));
            propertyWriters.putAll(fetchJEEFieldWriters(ImmutableSet.copyOf(clazz.getDeclaredFields())));
            propertyWriters.putAll(fetchCassandraMapperFieldWriters(ImmutableSet.copyOf(clazz.getFields())));
            propertyWriters.putAll(fetchCassandraMapperFieldWriters(ImmutableSet.copyOf(clazz.getDeclaredFields())));
            propertyWriters.putAll(fetchFieldWriters(ImmutableSet.copyOf(clazz.getFields())));
            propertyWriters.putAll(fetchFieldWriters(ImmutableSet.copyOf(clazz.getDeclaredFields())));
                   
            
            return new PropertiesMapper(ImmutableMap.copyOf(propertyReaders), ImmutableMap.copyOf(propertyWriters), clazz);
        }
     
        
        private static ImmutableMap fetchFieldReaders(ImmutableSet beanFields) {
            Map propertyReaders = Maps.newHashMap();
            
            for (Field beanField : beanFields) {
                final net.oneandone.troilus.Field field = beanField.getAnnotation(net.oneandone.troilus.Field.class);
                if (field != null) {
                    propertyReaders.put(field.name(), new PropertyReader(field.name(), beanField));
                }
            }
            
            return ImmutableMap.copyOf(propertyReaders);
        }
        
        
        private static ImmutableMap fetchJEEFieldReaders(ImmutableSet beanFields) {
            Map propertyReaders = Maps.newHashMap();
            
            for (Field beanField : beanFields) {
                for (Annotation annotation : beanField.getAnnotations()) {
                    
                    if (annotation.annotationType().getName().equals("javax.persistence.Column")) {
                        for (Method attributeMethod : annotation.annotationType().getDeclaredMethods()) {
                            if (attributeMethod.getName().equalsIgnoreCase("name")) {
                                try {
                                    final String columnName = (String) attributeMethod.invoke(annotation);
                                    if (columnName != null) {
                                        propertyReaders.put(columnName, new PropertyReader(columnName, beanField));
                                    }
                                    break;

                                } catch (ReflectiveOperationException ignore) { }
                            }
                        }
                    }
                }
            }
            
            return ImmutableMap.copyOf(propertyReaders);
        }
        

        private static ImmutableMap fetchCassandraMapperFieldReaders(ImmutableSet beanFields) {
            Map propertyReaders = Maps.newHashMap();
            
            for (Field beanField : beanFields) {
                for (Annotation annotation : beanField.getAnnotations()) {
                    
                    if (annotation.annotationType().getName().equals("com.datastax.driver.mapping.annotations.Field")) {
                        for (Method attributeMethod : annotation.annotationType().getDeclaredMethods()) {
                            if (attributeMethod.getName().equalsIgnoreCase("name")) {
                                try {
                                    final String columnName = (String) attributeMethod.invoke(annotation);
                                    if (columnName != null) {
                                        propertyReaders.put(columnName, new PropertyReader(columnName, beanField));
                                    }
                                    break;

                                } catch (ReflectiveOperationException ignore) { }
                            }
                        }
                    }
                }
            }
            
            return ImmutableMap.copyOf(propertyReaders);

        }
        
   
        private Map fetchFieldWriters(ImmutableSet beanFields) {
            Map propertyWriters = Maps.newHashMap();
            
            for (Field beanField : beanFields) {
                
                final net.oneandone.troilus.Field field = beanField.getAnnotation(net.oneandone.troilus.Field.class);
                if (field != null) {
                    propertyWriters.put(field.name(), new PropertyWriter(field.name(), beanField));
                }
            }
            
            return ImmutableMap.copyOf(propertyWriters);
        }
        
        
        private Map fetchJEEFieldWriters(ImmutableSet beanFields) {
            Map propertyWriters = Maps.newHashMap();
            
            for (Field beanField : beanFields) {
                for (Annotation annotation : beanField.getAnnotations()) {
                    
                    if (annotation.annotationType().getName().equals("javax.persistence.Column")) {
                        for (Method attributeMethod : annotation.annotationType().getDeclaredMethods()) {
                            if (attributeMethod.getName().equalsIgnoreCase("name")) {
                                try {
                                    String columnName = (String) attributeMethod.invoke(annotation);
                                    propertyWriters.put(columnName, new PropertyWriter(columnName, beanField));
                                } catch (ReflectiveOperationException ignore) { }
                            }
                        }
                    }
                }
            }
            
            return ImmutableMap.copyOf(propertyWriters);
        }

        
                
        private Map fetchCassandraMapperFieldWriters(ImmutableSet beanFields) {
            Map propertyWriters = Maps.newHashMap();

            for (Field beanField : beanFields) {
                for (Annotation annotation : beanField.getAnnotations()) {
                    
                    if (annotation.annotationType().getName().equals("com.datastax.driver.mapping.annotations.Field")) {
                        for (Method attributeMethod : annotation.annotationType().getDeclaredMethods()) {
                            if (attributeMethod.getName().equalsIgnoreCase("name")) {
                                try {
                                    String columnName = (String) attributeMethod.invoke(annotation);
                                    propertyWriters.put(columnName, new PropertyWriter(columnName, beanField));
                                } catch (ReflectiveOperationException ignore) { }
                            }
                        }
                    }
                }
            }
            
            return ImmutableMap.copyOf(propertyWriters);
        }    
    }    
    

    private static class PropertyReader {
        
        private final String fieldName;
        private final java.lang.reflect.Field field;
        private final OptionalWrapper optionalWrapper;
        
        public PropertyReader(String fieldName, java.lang.reflect.Field field) {
            this.fieldName = fieldName;

            this.field = field;
            AccessController.doPrivileged(new SetFieldAccessible(field));
            
            if (Optional.class.isAssignableFrom(field.getType())) {
                this.optionalWrapper = new GuavaOptionalWrapper();
                
            } else if (field.getType().getName().equals("java.util.Optional")) {
                getActualTypeArgument(field.getType(), 0);

                this.optionalWrapper = new JavaOptionalWrapper();
                
            } else {
                this.optionalWrapper = new NonOptionalWrapper();
            }
        }
        
        
        public Entry> readProperty(Object bean) {
            
            Object value = null;
            try {
                value = field.get(bean);
            } catch (IllegalArgumentException | IllegalAccessException e) { }
            
            return  Maps.immutableEntry(fieldName, optionalWrapper.wrap(value));
        }

        
    
        
        private static interface OptionalWrapper {
            
            Optional wrap(Object obj);
        }
        private static final class NonOptionalWrapper implements OptionalWrapper {
            
            public Optional wrap(Object obj) {
                return Optional.fromNullable(obj);
            }
        }
        
        
        private static final class GuavaOptionalWrapper implements OptionalWrapper {
            
            @SuppressWarnings("unchecked")
            public Optional wrap(Object obj) {
                if (obj == null) {
                    return Optional.absent();
                } else {
                    return (Optional) obj;
                }
            }
        }

        
        
        private static final class JavaOptionalWrapper implements OptionalWrapper {
            
            private final Method meth;
            
            public JavaOptionalWrapper() {
                try {
                    meth = Class.forName("java.util.Optional").getMethod("get");
                } catch (NoSuchMethodException | SecurityException | ClassNotFoundException e) {
                    throw new RuntimeException(e);
                }
            }
            
            public Optional wrap(Object obj) {
                try {
                    Object o = meth.invoke(obj);
                    return Optional.fromNullable(o);
                } catch (InvocationTargetException | IllegalAccessException |  SecurityException e) {
                    return Optional.absent();
                }
            }
        }
    }
    

    
    private static Type getActualTypeArgument(Type type, int argIndex) {
        if (type instanceof ParameterizedType) {
            ParameterizedType paramizedType = (ParameterizedType) type;
            Type[] types = paramizedType.getActualTypeArguments();
            if ((types != null) && (types.length > argIndex)) {
                return types[argIndex];
            }
        }
        
        return Object.class;
    }
    
    
    private static class PropertyWriter {
        
        private final String fieldName;
        private final java.lang.reflect.Field field;
        private final OptionalWrapper optionalWrapper;
        
        private Class javaOptionalClass;
        
        public PropertyWriter(String fieldName, java.lang.reflect.Field field) {
            this.fieldName = fieldName;

            this.field = field;
            AccessController.doPrivileged(new SetFieldAccessible(field));

            
            if (Optional.class.isAssignableFrom(field.getType())) {
                this.optionalWrapper = new GuavaOptionalWrapper();
                
           } else if (field.getType().getName().equals("java.util.Optional")) {
                this.optionalWrapper = new JavaOptionalWrapper();

            } else {
                this.optionalWrapper = new NonOptionalWrapper();
            }

            
            Class cl = null;
            try {
                cl = Class.forName("java.util.Optional");
            } catch (ClassNotFoundException | RuntimeException e) { }
            
            javaOptionalClass = cl;
        }

        
        void writeProperty(Object bean, PropertiesSource datasource) {
            
            Optional optionalValue = readValue(field.getType(), datasource);

            if (optionalValue == null) {
                return;
            }
            
            try {
                field.set(bean, optionalWrapper.unwrap(optionalValue));
            } catch (IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
      
        
        @SuppressWarnings({ "unchecked", "rawtypes" })
        private Optional readValue(Class clazz, PropertiesSource datasource) {
            Optional value = Optional.absent();
            
            Type type = field.getGenericType();
            
            if (isOptional(clazz)) {
                type = getActualTypeArgument(type, 0);
            }
                
            if (ImmutableSet.class.isAssignableFrom(clazz)) {
                value = datasource.read(fieldName, (Class) getActualTypeArgument(type, 0));
                if (value.isPresent()) {
                    return Optional.of(ImmutableSet.copyOf((Collection) value.get()));
                }

            } else if (ImmutableList.class.isAssignableFrom(clazz)) {
                value =  datasource.read(fieldName, (Class) getActualTypeArgument(type, 0));
                if (value.isPresent()) {
                    return Optional.of(ImmutableList.copyOf((Collection) value.get()));
                }


            } else if (ImmutableMap.class.isAssignableFrom(clazz)) {
                value = datasource.read(fieldName, (Class) getActualTypeArgument(type, 0), (Class) getActualTypeArgument(field.getGenericType(), 1));
                if (value.isPresent()) {
                    return Optional.of(ImmutableMap.copyOf((Map) value.get()));
                }

            } else {
                value = datasource.read(fieldName, (Class) type);
            }
            
            return value;
        }
        
        
        private boolean isOptional(Class clazz) {
            return Optional.class.isAssignableFrom(clazz) || ((javaOptionalClass != null) && (javaOptionalClass.isAssignableFrom(clazz)));
        }
        
        
        private static interface OptionalWrapper {
            
            Object unwrap(Optional obj);
        }
        
        private static final class NonOptionalWrapper implements OptionalWrapper {
            
            public Object unwrap(Optional obj) {
                return obj.orNull();
            }
        }

        
        private static final class GuavaOptionalWrapper implements OptionalWrapper {
            
            public Object unwrap(Optional obj) {
                return Optional.fromNullable(emptyToNull(obj.orNull()));
            }
        }

        private static Object emptyToNull(Object obj) {
            if (obj == null) {
                return null;
            }
            
            if (List.class.isAssignableFrom(obj.getClass())) {
                if (((List) obj).isEmpty()) {
                    obj = null;
                }
            } else if (Set.class.isAssignableFrom(obj.getClass())) {
                if (((Set) obj).isEmpty()) {
                    obj = null;
                }
            } else if (Map.class.isAssignableFrom(obj.getClass())) {
                if (((Map) obj).isEmpty()) {
                    obj = null;
                }
            } else if (byte[].class.isAssignableFrom(obj.getClass())) {
                if (((byte[]) obj).length == 0) {
                    obj = null;
                }
            }
            
            return obj;
        }


        private static final class JavaOptionalWrapper implements OptionalWrapper {
            
            private final Method meth;
            
            public JavaOptionalWrapper() {
                try {
                    meth = Class.forName("java.util.Optional").getDeclaredMethod("ofNullable", Object.class);
                } catch (NoSuchMethodException | SecurityException | ClassNotFoundException e) {
                    throw new RuntimeException(e);
                }
            }
            
            @Override
            public Object unwrap(Optional obj) { 
                try {
                    Object o = emptyToNull(obj.orNull());
                    return meth.invoke(null, o);
                } catch (InvocationTargetException | IllegalAccessException |  SecurityException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}