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

me.mywiki.configurator.ReflectiveConfigurator Maven / Gradle / Ivy

package me.mywiki.configurator;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

import com.google.common.base.Preconditions;
import com.google.common.base.Verify;

/**
 * 

* A configuration is a map from name to values, * as such the contract for configuration is to map * what names are associated with each values * and provide a convenient mechanism for instantiating full maps *

* The contract between this library * User defines 2 interfaces: the configuration interface is a read-only interface, * representing the completed (ready to consume) configuration * example: * *
 *    interface MyConfiguration {
 *        String myProperty1()
 *        int  whateverName()
 *        MyValueClass myPropety3()
 *        // A configuration most likely needs to be composited hierarchically
 *        MySubConfiguration subConfiguration()
 *        // ... and so on
 *    }
 *   
* * That's on one hand, the read-only interface defines the finished product (the sausage) * * In addition the client needs to declare how the sausage is made with a builder interface *
 *    interface MyConfigurationBuilder {
 *      
 *      // All setters method will "return this" allowing setters chaining
 *      // A setter is any method that takes one parameter and return the bvuilder type 
 *      MyConfigurationBuilder myProperty1 ( String val_ );
 *      MyConfigurationBuilder whateverName ( int val_ );
 *      MyConfigurationBuilder myProperty3(  MyValueClass val_ );
 *      MyConfigurationBuilder subConfiguration ( MySubConfiguration sub_ );
 *      // ...
 *      
 *      // special methods
 *      // done finishes the building phase, and constructs the finished product
 *      // It'll throw IllegalStateException if not all properties have been set
 *      MyConfiguration done();
 *    }
 *    
* */ public class ReflectiveConfigurator { /** * DefaultsToString can be attached to any string read-property * specifying that in case the property is not explicitly set by the builder * it will be defaulted to the anotation value */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public static @interface DefaultsToString { String val(); } /** * Declare default value for int propety that it annotates */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public static @interface DefaultsToInteger { int val(); } /** * When the configuration value is a class * provides the default value in case the property is not set in the builder */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public static @interface DefaultsToClass { Class val(); } /** * Apply a function to the supplied value */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public static @interface TransformBy { Class> _fun() default IdFun.class; } public static Builder configBuilderFor( Class readerClass, Class builderClass ) { try { return new ReflectiveBuilderImpl ( readerClass, builderClass) .makeBuilder(); } catch (Exception ex) { if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } else { throw new RuntimeException(ex); } } } public static class MissingPropertyException extends RuntimeException { private static final long serialVersionUID = 1L; public MissingPropertyException(String message) { super(message); } public MissingPropertyException(Throwable cause) { super(cause); } } private static class ReflectiveBuilderImpl { final Class builderClass; final Class readerClass; //final Map valueMap= new HashMap<>(); final Set propNames; final Map transformers; ReflectiveBuilderImpl( Class readerClass_, Class builderClass_) throws Exception { this.builderClass= builderClass_; this.readerClass= readerClass_; Pair, Map> metadataCheck= checkAgainstSpec(readerClass, builderClass); this.propNames= metadataCheck.getLeft(); this.transformers= metadataCheck.getRight(); } /** * Checks that the builder interface matches the reader interface * @return a tuple containing all the property names, and a Map from * propertyNames to the optional transforming function */ private static Pair, Map> checkAgainstSpec( Class readerClass_, Class builderClass_) throws Exception { Preconditions.checkArgument(builderClass_.isInterface(), "builder should be an interface"); Set builderPropNames= new HashSet<>(); for (Method m: builderClass_.getDeclaredMethods()) { String mName= m.getName(); if (mName.equals("done")) { Preconditions.checkArgument( 0 == m.getParameterCount(),"done is a method with 0 paramters"); Preconditions.checkArgument(m.getReturnType().equals(readerClass_), "done returns the reader object"); continue; } // all other methods are setter of form Builder propertyName(PropType val); Preconditions.checkArgument(1 == m.getParameterCount(), "setter method: "+mName ); Preconditions.checkArgument(builderClass_.equals(m.getReturnType()), "returning a builder for"+mName ); builderPropNames.add(mName); } Set readerPropNames= new HashSet(); Map transformers= new HashMap<>(); for (Method m: readerClass_.getMethods()) { String mName= m.getName(); if (mName.equals("cloneBuilder")) { Preconditions.checkArgument( 0 == m.getParameterCount(), "cloneBuilder is a method with 0 paramters" ); Preconditions.checkArgument( m.getReturnType().equals(builderClass_), "cloneBuilder returns the builder"); continue; } // all other methods are setter of form Builder propertyName(PropType val); Preconditions.checkArgument( 0== m.getParameterCount() ,"getter method has 0 params "+mName ); readerPropNames.add(mName); TransformBy transform= m.getDeclaredAnnotation(TransformBy.class); if (transform != null) { transformers.put(mName, transform._fun().newInstance()); } } Preconditions.checkArgument( readerPropNames.equals(builderPropNames), "Reader properties match builder properties"); return new ImmutablePair, Map>(readerPropNames,transformers); } @SuppressWarnings("unchecked") public Builder makeBuilder() { return (Builder) Proxy.newProxyInstance( this.getClass().getClassLoader(), new Class [] {builderClass}, new ConfigBuilderHandler()); } @SuppressWarnings("unchecked") public Builder makeBuilder(Map initialValues) { return (Builder) Proxy.newProxyInstance( this.getClass().getClassLoader(), new Class [] { builderClass }, new ConfigBuilderHandler(initialValues)); } @SuppressWarnings("unchecked") public Reader buildTheReader(Map valueMap) { // verify that we have values for all needed properties Set suppliedKeys= valueMap.keySet(); Set toBeResolved= new HashSet(propNames); toBeResolved.removeAll(suppliedKeys); //check that all properties are assigned if (toBeResolved.isEmpty()) return (Reader) Proxy.newProxyInstance( this.getClass().getClassLoader(), new Class [] { readerClass , InternalReaderAccess.class}, new ConfigReaderHandler( valueMap) ); else { //TODO: supply a list of what is missing throw new MissingPropertyException("Configuration missing the following properties: " + toBeResolved.toString()); } } private class ConfigBuilderHandler implements InvocationHandler { final Map valueMap; public ConfigBuilderHandler() { this.valueMap= new HashMap<>(); } public ConfigBuilderHandler(Map initialValues) { this.valueMap= new HashMap<>(initialValues); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String mName= method.getName(); if (mName.equals("done")) { return buildTheReader(this.valueMap); } // here we assume that all the other methods have the shape // XXXBuilder propertyName( PropertyType val_) // because the builder constructor enforces this condition if (args.length != 1 ) { throw new IllegalStateException("Expecting propety setter, of type XXXBuilder propertyName( PropertyType val_)"); } Object val= args[0]; if (transformers.containsKey(mName)) { val= transformers.get(mName).apply(val); } valueMap.put(mName,val); return proxy; } } public class ConfigReaderHandler implements InvocationHandler { final MapmyValueMap; public ConfigReaderHandler(Map valueMap) { // copy the input to avoid side effects this.myValueMap= new HashMap(valueMap); } @Override public Object invoke ( Object proxy, Method m, Object[] args) throws Throwable { String mName= m.getName(); switch (mName) { // Begin special cases of ReadOnly interface case "cloneBuilder": { return makeBuilder(this.myValueMap); } case "toString": { Verify.verify(args == null); return myValueMap.toString(); } case "__internalMap": { Verify.verify(args == null); return this.myValueMap; } case "equals" : { Verify.verify(args.length == 1); if (args[0] == null) return false; if (! (args[0] instanceof InternalReaderAccess )) return false; return (this.myValueMap.equals(((InternalReaderAccess) args[0]).__internalMap())); } case "hashCode" : { Verify.verify(args == null); return myValueMap.hashCode(); } } //End special cases // all the rest are to be considered accessor methods if (myValueMap.containsKey(mName)) { return myValueMap.get(mName); } else { throw new IllegalStateException("Value not supplied for property: "+mName); } } } /** * to be used by us in helper methods to dynamically access the internal state of our implementation */ private static interface InternalReaderAccess { Map __internalMap(); } }// end of ReflectiveBuilderImple private static class IdFun implements Function { @Override public Object apply(Object x) { return x; }} }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy