com.isotrol.impe3.core.config.ConfigurationDefinition.java Source code

Java tutorial

Introduction

Here is the source code for com.isotrol.impe3.core.config.ConfigurationDefinition.java

Source

/**
 * This file is part of Port@l
 * Port@l 3.0 - Portal Engine and Management System
 * Copyright (C) 2010  Isotrol, SA.  http://www.isotrol.com
 *
 * Port@l is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Port@l is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Port@l.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.isotrol.impe3.core.config;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Iterables.transform;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.ws.rs.DefaultValue;

import net.sf.derquinsej.Classes;
import net.sf.derquinsej.Methods;
import net.sf.derquinsej.i18n.Localized;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.isotrol.impe3.api.Category;
import com.isotrol.impe3.api.Compressed;
import com.isotrol.impe3.api.Configuration;
import com.isotrol.impe3.api.ContentType;
import com.isotrol.impe3.api.Downloadable;
import com.isotrol.impe3.api.FileBundle;
import com.isotrol.impe3.api.FileId;
import com.isotrol.impe3.api.Optional;
import com.isotrol.impe3.api.StringItemStyle;
import com.isotrol.impe3.api.StringStyle;
import com.isotrol.impe3.core.support.Definition;
import com.isotrol.impe3.core.support.Named;
import com.isotrol.impe3.core.support.NamedSupport;
import com.isotrol.impe3.core.support.SingleValueSupport;

/**
 * Configuration definition.
 * @author Andres Rodriguez.
 * 
 * @param <T> Configuration interface.
 */
public final class ConfigurationDefinition<T extends Configuration> extends Definition<T> {
    /** Cache. */
    private static final SingleValueSupport<Class<?>, ConfigurationDefinition<?>> CACHE = SingleValueSupport
            .create();

    /** Predicate to evaluate if a class is a configuration. */
    public static final Predicate<Class<?>> IS_CONFIGURATION = Classes.extendsOrImplements(Configuration.class);

    /** Predicate to evaluate if an item is required. */
    public static final Predicate<Item> IS_REQUIRED = new Predicate<Item>() {
        public boolean apply(Item input) {
            return input.isRequired();
        }
    };

    /** Predicate to evaluate if an item must be provided (is required and has no default value). */
    private static final Predicate<Item> IS_MBP = new Predicate<Item>() {
        public boolean apply(Item input) {
            return input.isMVP();
        }
    };

    /** Function to get the type of an item. */
    public static Function<Item, Class<?>> ITEM_TYPE = new Function<Item, Class<?>>() {
        public Class<?> apply(Item from) {
            return from.getType();
        }
    };

    /**
     * Returns the configuration definition for the specified configuration interface.
     * @param configClass Configuration class.
     * @return The configuration definition.
     * @throws ConfigurationException if the configuration class is invalid.
     */
    public static <T extends Configuration> ConfigurationDefinition<T> of(Class<T> configClass)
            throws ConfigurationException {
        Preconditions.checkNotNull(configClass, "A configuration class must be provided");
        @SuppressWarnings("unchecked")
        final ConfigurationDefinition<T> d1 = (ConfigurationDefinition<T>) CACHE.get(configClass);
        if (d1 != null) {
            return d1;
        }
        @SuppressWarnings("unchecked")
        final ConfigurationDefinition<T> d2 = (ConfigurationDefinition<T>) CACHE.put(configClass,
                new ConfigurationDefinition<T>(configClass));
        return d2;
    }

    /**
     * Returns the configuration definition for the specified configuration interface.
     * @param name Configuration class name.
     * @return The configuration definition.
     * @throws ConfigurationException if the configuration class is invalid.
     * @throws IllegalArgumentException if the name is not a configuration class.
     */
    public static ConfigurationDefinition<?> of(String name) throws ConfigurationException {
        Preconditions.checkNotNull(name, "A configuration class name must be provided");
        try {
            final Class<?> klass = Class.forName(name);
            final Class<? extends Configuration> configClass = klass.asSubclass(Configuration.class);
            return of(configClass);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException(e);
        } catch (ClassCastException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private static ImmutableMap<String, Item> getItems(Class<? extends Configuration> configClass)
            throws ConfigurationException {
        if (!configClass.isInterface()) {
            throw new NonInterfaceConfigurationException(configClass);
        }
        final List<Method> methods = Methods.getMethods(configClass);
        if (methods.isEmpty()) {
            throw new EmptyConfigurationException(configClass);
        }
        final Map<String, Item> map = Maps.newHashMap();
        for (Method m : methods) {
            final Item item = new Item(configClass, m);
            map.put(item.getParameter(), item);
        }
        return ImmutableMap.copyOf(map);
    }

    /** Parameters map. */
    private final ImmutableMap<String, Item> parameters;
    /** Whether there are required items. */
    private final boolean required;
    /** Whether there are MVP items. */
    private final boolean mbp;
    /** If the configuration has a content type item. */
    private final boolean contentTypes;
    /** If the configuration has a category item. */
    private final boolean categories;

    /**
     * Constructs the configuration definition for the specified configuration interface.
     * @param configClass Configuration class.
     * @throws ConfigurationException if the configuration class is invalid.
     */
    private ConfigurationDefinition(Class<T> configClass) throws ConfigurationException {
        super(configClass);
        this.parameters = getItems(configClass);
        this.required = any(this.parameters.values(), IS_REQUIRED);
        this.mbp = any(this.parameters.values(), IS_MBP);
        final Iterable<Class<?>> types = transform(this.parameters.values(), ITEM_TYPE);
        this.contentTypes = any(types, Predicates.<Class<?>>equalTo(ContentType.class));
        this.categories = any(types, Predicates.<Class<?>>equalTo(Category.class));
    }

    /**
     * Returns the configuration parameters.
     * @return The configuration parameters.
     */
    public ImmutableMap<String, Item> getParameters() {
        return parameters;
    }

    /**
     * Returns the required configuration parameters.
     * @return The required configuration parameters.
     */
    public Map<String, Item> getRequiredParameters() {
        return Maps.filterValues(parameters, IS_REQUIRED);
    }

    /**
     * Returns the configuration parameters that are required and have no default value.
     * @return The "must be provided" configuration parameters.
     */
    public Map<String, Item> getMBPParameters() {
        return Maps.filterValues(parameters, IS_MBP);
    }

    public boolean isRequired() {
        return required;
    }

    public boolean hasMBPItems() {
        return mbp;
    }

    public boolean hasContentTypes() {
        return contentTypes;
    }

    public boolean hasCategories() {
        return categories;
    }

    /**
     * Returns a new builder for the current configuration.
     * @return
     */
    public ConfigurationBuilder<T> builder() {
        return new Builder();
    }

    /**
     * A configuration item.
     * @author Andres Rodriguez.
     */
    public static final class Item implements Named {
        private final String parameter;
        private final ConfigurationType type;
        /** Default value. */
        private final Object defaultValue;
        /** Name information. */
        private final NamedSupport name;
        private final boolean required;
        private final boolean downloadable;
        private final boolean bundle;
        private final boolean compressed;
        /** String style. */
        private final StringItemStyle stringStyle;

        private Item(Class<? extends Configuration> configClass, final Method m) {
            this.parameter = m.getName();
            this.name = new NamedSupport(m);
            if (m.getParameterTypes().length > 0) {
                throw new MethodWithArgumentsConfigurationException(configClass, parameter);
            }
            final Class<?> klass = m.getReturnType();
            this.type = ConfigurationType.of(configClass, parameter, klass);
            this.required = this.type.isRequiredForced() || (m.getAnnotation(Optional.class) == null);
            if (FileId.class.equals(klass)) {
                this.downloadable = m.isAnnotationPresent(Downloadable.class);
                this.bundle = m.isAnnotationPresent(FileBundle.class);
                this.compressed = this.bundle ? false : m.isAnnotationPresent(Compressed.class);
            } else {
                this.downloadable = false;
                this.bundle = false;
                this.compressed = false;
            }
            if (String.class.equals(klass) && m.isAnnotationPresent(StringStyle.class)) {
                this.stringStyle = firstNonNull(m.getAnnotation(StringStyle.class).value(), StringItemStyle.NORMAL);
            } else {
                this.stringStyle = StringItemStyle.NORMAL;
            }
            final DefaultValue dv = m.getAnnotation(DefaultValue.class);
            try {
                this.defaultValue = this.type.parse(dv);
            } catch (RuntimeException e) {
                throw new IllegalValueConfigurationException(configClass, parameter, klass, dv.value());
            }
        }

        public String getParameter() {
            return parameter;
        }

        public Class<?> getType() {
            return type.getValueType();
        }

        /**
         * Returns the default value.
         * @return The default value.
         */
        public Object getDefaultValue() {
            return defaultValue;
        }

        /**
         * @see com.isotrol.impe3.core.support.Named#getName()
         */
        public Localized<String> getName() {
            return name.getName();
        }

        /**
         * @see com.isotrol.impe3.core.support.Named#getDescription()
         */
        public Localized<String> getDescription() {
            return name.getDescription();
        }

        public boolean isRequired() {
            return required;
        }

        public boolean isMVP() {
            return required && defaultValue == null;
        }

        public boolean isDownloadable() {
            return downloadable;
        }

        public boolean isBundle() {
            return bundle;
        }

        public boolean isCompressed() {
            return compressed;
        }

        public StringItemStyle getStringStyle() {
            return stringStyle;
        }

        public boolean isEnum() {
            return type.isEnum();
        }

        public Object fromString(String value) {
            if (value == null) {
                return null;
            }
            return type.parse(value);
        }

        public ImmutableMap<Enum<?>, NamedSupport> getChoices() {
            return type.getChoices();
        }

    }

    /**
     * Builder for configurations objects.
     * @author Andres Rodriguez.
     */
    private final class Builder implements ConfigurationBuilder<T> {
        /** Values. */
        private final Map<String, Object> values;

        private Builder() {
            values = Maps.newHashMap();
            for (Item item : parameters.values()) {
                final Object defaultValue = item.getDefaultValue();
                if (defaultValue != null) {
                    values.put(item.getParameter(), defaultValue);
                }
            }
        }

        /**
         * Sets a configuration value.
         * @param parameter Parameter name.
         * @param value Parameter value.
         * @return This builder for method chaining.
         * @throws NullPointerException if the parameter is null.
         * @throws IllegalArgumentException if the parameter does not exist or the value is of an incorrect type.
         */
        public Builder set(String parameter, Object value) {
            checkNotNull(parameter, "A parameter name must be provided");
            checkArgument(parameters.containsKey(parameter), "Invalid parameter [%s] of configuration [%s]",
                    parameter, getTypeName());
            if (value == null) {
                values.remove(parameter);
            } else {
                final Class<?> type = parameters.get(parameter).getType();
                checkArgument(type.isInstance(value), "Invalid type [%s] for parameter [%s] of configuration [%s]",
                        value.getClass().getName(), parameter, getTypeName());
                values.put(parameter, value);
            }
            return this;
        }

        /**
         * @see com.google.common.base.Supplier#get()
         */
        @SuppressWarnings("unchecked")
        public T get() {
            // Compute the difference.
            final Set<String> required = Maps.filterValues(parameters, IS_REQUIRED).keySet();
            final Set<String> diff = Sets.difference(required, values.keySet());
            checkState(diff.isEmpty(), "Missing parameters %s for configuration [%s]", diff, getTypeName());
            final InvocationHandler h = new ConfigurationProxy(ImmutableMap.copyOf(values));
            final Class<?> type = getType();
            return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[] { type }, h);
        }
    }

    private static final class ConfigurationProxy implements InvocationHandler {
        /** Values. */
        private final ImmutableMap<String, Object> values;

        private ConfigurationProxy(ImmutableMap<String, Object> values) {
            this.values = values;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method,
         * java.lang.Object[])
         */
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return values.get(method.getName());
        }
    }
}