com.github.jknack.extend.Extend.java Source code

Java tutorial

Introduction

Here is the source code for com.github.jknack.extend.Extend.java

Source

/**
 * Copyright (c) 2013 Edgar Espina
 *
 * This file is part of amd4j (https://github.com/jknack/amd4j)
 *
 * 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 com.github.jknack.extend;

import static org.apache.commons.lang3.Validate.notEmpty;
import static org.apache.commons.lang3.Validate.notNull;

import java.lang.reflect.Method;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;

import net.sf.cglib.beans.BeanGenerator;
import net.sf.cglib.beans.BeanMap;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * <p>
 * Extends JavaBean or convert them to {@link Map} and add new properties at runtime.
 * </p>
 * <p>
 * Usage:
 * </p>
 *
 * <pre>
 * import static com.github.jknack.extend.Extend.*;
 *
 * ...
 *
 * MyObject extended =
 *   extend(object,
 *     $("propertyA", value),
 *     $("propertyB", value),
 *     ...
 *   );
 *
 * </pre>
 * <p>
 * Derived Properties:
 * </p>
 *
 * <pre>
 * import static com.github.jknack.extend.Extend.*;
 *
 * ...
 *
 * MyObject extended =
 *   extend(object,
 *     $("propertyA", new Function<MyObject, Integer>() {
 *       public Integer apply(MyObject object) {
 *         return ...;
 *       }
 *     })
 *   );
 *
 * </pre>
 *
 * <p>
 * Convert a JavaBean to a Map:
 * </p>
 *
 * <pre>
 * import static com.github.jknack.extend.Extend.*;
 *
 * ...
 *
 * Map<String, Object> extended =
 *   map(object,
 *     $("propertyA", value)
 *   );
 *
 * </pre>
 *
 * @author edgar.espina
 * @since 0.1.0
 */
public final class Extend {

    /**
     * The method interceptor.
     *
     * @author edgar.espina
     *
     */
    static class ExtendInterceptor implements MethodInterceptor {

        /**
         * The source object.
         */
        private Object source;

        /**
         * Extra properties.
         */
        private Map<String, Object> properties;

        /**
         * Internal use only.
         */
        private static final Object UNRESOLVED = new Object();

        /**
         * Creates a new {@link ExtendInterceptor}.
         *
         * @param source The object source.
         * @param properties The extra properties.
         */
        public ExtendInterceptor(final Object source, final Map<String, Object> properties) {
            this.source = source;
            this.properties = properties;
        }

        /**
         * Resolve a value using one of the provided resolvers.
         *
         * @param resolvers A resolver list.
         * @return A resolved value or null.
         * @throws Exception If a method invocation fails.
         */
        private Object resolve(final Callable<Object>... resolvers) throws Exception {
            for (Callable<Object> resolver : resolvers) {
                Object value = resolver.call();
                if (value != UNRESOLVED) {
                    return value;
                }
            }
            return null;
        }

        /**
         * Creates a resolve around a method invocation.
         *
         * @param method The target method.
         * @param source The object source.
         * @param args The method arguments.
         * @return A new resolver.
         */
        private static Callable<Object> asMethod(final Method method, final Object source, final Object[] args) {
            return new Callable<Object>() {
                @Override
                public Object call() throws Exception {
                    return method.invoke(source, args);
                }
            };
        }

        /**
         * Creates a new resolver around the extra properties.
         *
         * @param methodName The method's name.
         * @param args The argument list.
         * @return A new resolver.
         */
        private Callable<Object> asMapEntry(final String methodName, final Object... args) {
            return new Callable<Object>() {
                @Override
                public Object call() throws Exception {
                    String[] prefixes = { "get", "is" };
                    for (String prefix : prefixes) {
                        if (methodName.startsWith(prefix)) {
                            StringBuilder buffer = new StringBuilder(methodName.substring(prefix.length()));
                            buffer.setCharAt(0, Character.toLowerCase(buffer.charAt(0)));
                            String propertyName = buffer.toString();
                            if (properties.containsKey(propertyName)) {
                                return properties.get(propertyName);
                            }
                            return UNRESOLVED;
                        }
                    }
                    return UNRESOLVED;
                }
            };
        }

        @SuppressWarnings({ "rawtypes", "unchecked" })
        @Override
        public Object intercept(final Object extended, final Method method, final Object[] args,
                final MethodProxy proxy) throws Throwable {
            String methodName = method.getName();
            if (methodName.startsWith("set")) {
                return null;
            }
            if ("hashCode".equals(methodName)) {
                return source.hashCode();
            }
            if ("equals".equals(methodName)) {
                return source.equals(args[0]);
            }
            Callable[] resolvers = { asMapEntry(methodName, args), asMethod(method, source, args) };

            Object value = resolve(resolvers);
            if (value instanceof Function) {
                value = ((Function) value).apply(source);
            }
            return value;
        }

    }

    /**
     * Helper class for simplify object property creation.
     * Use {@link Extend#$(String, Object)} factory method.
     *
     * @author edgar.espina
     * @since 0.1.0
     * @see Extend#$(String, Object)
     */
    public static final class Property {

        /**
         * The property's name. Required.
         */
        public final String name;

        /**
         * The property's value. Required.
         */
        public final Object value;

        /**
         * Creates a new {@link Property}.
         *
         * @param name The property's name. Required.
         * @param value The property's value. Required.
         */
        private Property(final String name, final Object value) {
            this.name = notEmpty(name, "The name is required.");
            this.value = notNull(value, "The value is required.");
        }

    }

    /**
     * Denied!
     */
    private Extend() {
    }

    /**
     * Factory method for {@link Property}.
     *
     * @param name The property's name. Required.
     * @param value The property's value. Required.
     * @return A new {@link Property}.
     */
    public static Property $(final String name, final Object value) {
        return new Property(name, value);
    }

    /**
     * Extends each object with the given properties.
     *
     * @param sources The objects to extend. Required.
     * @param properties The extra property set. Required.
     * @param <T> The source type.
     * @return A list of extended and immutable objects.
     */
    public static <T> List<T> extend(final Iterable<T> sources, final Property... properties) {
        notNull(sources, "The source object is required.");
        notEmpty(properties, "The properties is required");
        List<T> result = new ArrayList<T>();
        for (T source : sources) {
            result.add(extend(source, properties));
        }
        return result;
    }

    /**
     * Extends an object with the given properties.
     *
     * @param source The object to extend. Required.
     * @param properties The extra property set. Required.
     * @param <T> The source type.
     * @return A new extended and immutable object.
     */
    public static <T> T extend(final T source, final Property... properties) {
        notNull(source, "The source object is required.");
        notEmpty(properties, "The properties is required");
        return extend(source, toMap(properties));
    }

    /**
     * Extends each object with the given properties.
     *
     * @param sources The objects to extend. Required.
     * @param properties The extra property set. Required.
     * @param <T> The source type.
     * @return A list of extended and immutable objects.
     */
    public static <T> List<T> extend(final Iterable<T> sources, final Map<String, Object> properties) {
        notNull(sources, "The sources object is required.");
        notEmpty(properties, "The properties is required");
        List<T> result = new ArrayList<T>();
        for (T source : sources) {
            result.add(extend(source, properties));
        }
        return result;
    }

    /**
     * Extends an object with the given properties.
     *
     * @param source The object to extend. Required.
     * @param properties The extra property set. Required.
     * @param <T> The source type.
     * @return A new extended and immutable object.
     */
    @SuppressWarnings("unchecked")
    public static <T> T extend(final T source, final Map<String, Object> properties) {
        notNull(source, "The source object is required.");
        notEmpty(properties, "The properties is required");
        final BeanGenerator beanGenerator = new BeanGenerator();
        beanGenerator.setSuperclass(source.getClass());

        for (Entry<String, Object> property : properties.entrySet()) {
            Object value = property.getValue();
            if (value != null) {
                Class<?> propertyType = value.getClass();
                if (value instanceof Function) {
                    propertyType = Object.class;
                }
                beanGenerator.addProperty(property.getKey(), propertyType);
            }
        }

        Class<T> extendedClass = (Class<T>) beanGenerator.createClass();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(extendedClass);
        enhancer.setCallback(new ExtendInterceptor(source, properties));
        return (T) enhancer.create();
    }

    /**
     * Converts an object to a {@link Map}. The resulting map is the union of the bean properties and
     * all the extra properties.
     *
     * @param source The object to extend. Required.
     * @param properties The extra property set. Required.
     * @param <T> The source type.
     * @return A new immutable map.
     */
    public static <T> Map<String, Object> map(final T source, final Property... properties) {
        return map(source, toMap(properties));
    }

    /**
     * Converts each object to a {@link Map}. The resulting map is the union of the bean properties
     * and all the extra properties.
     *
     * @param sources The objects to extend. Required.
     * @param properties The extra property set. Required.
     * @param <T> The source type.
     * @return A new immutable map.
     */
    public static <T> List<Map<String, Object>> map(final Iterable<T> sources, final Property... properties) {
        return map(sources, toMap(properties));
    }

    /**
     * Converts each object to a {@link Map}. The resulting map is the union of the bean properties
     * and all the extra properties.
     *
     * @param sources The objects to extend. Required.
     * @param properties The extra property set. Required.
     * @param <T> The source type.
     * @return A new immutable map.
     */
    public static <T> List<Map<String, Object>> map(final Iterable<T> sources,
            final Map<String, Object> properties) {
        notNull(sources, "The sources object is required.");
        List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
        for (T source : sources) {
            result.add(map(source, properties));
        }
        return result;
    }

    /**
     * Converts an object to a {@link Map}. The resulting map is the union of the bean properties and
     * all the extra properties.
     *
     * @param source The object to extend. Required.
     * @param properties The extra property set. Required.
     * @param <T> The source type.
     * @return A new immutable map.
     */
    @SuppressWarnings("unchecked")
    public static <T> Map<String, Object> map(final T source, final Map<String, Object> properties) {
        notNull(source, "The source object is required.");
        notNull(properties, "The properties is required");
        final BeanMap beanMap = BeanMap.create(source);
        return new AbstractMap<String, Object>() {
            @Override
            public Object put(final String key, final Object value) {
                return null;
            }

            @Override
            public Object get(final Object key) {
                Object value = properties.get(key);
                return value == null ? beanMap.get(key) : value;
            }

            @Override
            public Set<Entry<String, Object>> entrySet() {
                LinkedHashMap<String, Object> mergedProperties = new LinkedHashMap<String, Object>(beanMap);
                mergedProperties.putAll(properties);
                return mergedProperties.entrySet();
            }
        };
    }

    /**
     * Convert a {@link Property array} to a {@link Map}.
     *
     * @param properties A property's array.
     * @return A new {@link Map}.
     */
    private static Map<String, Object> toMap(final Property... properties) {
        Map<String, Object> hash = new LinkedHashMap<String, Object>();
        for (Property property : properties) {
            hash.put(property.name, property.value);
        }
        return hash;
    }
}