Package au.net.causal.projo.prefs

Source Code of au.net.causal.projo.prefs.ProjoStoreBuilder

package au.net.causal.projo.prefs;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.prefs.Preferences;

import org.apache.commons.lang3.SystemUtils;
import org.jasypt.encryption.ByteEncryptor;
import org.jasypt.encryption.pbe.PBEByteEncryptor;
import org.jasypt.encryption.pbe.StandardPBEByteEncryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import au.net.causal.projo.annotation.Secure;
import au.net.causal.projo.prefs.javapreferences.JavaPreferenceNode;
import au.net.causal.projo.prefs.security.ConsoleUiPasswordSource;
import au.net.causal.projo.prefs.security.PasswordSource;
import au.net.causal.projo.prefs.security.SourcedByteEncrypter;
import au.net.causal.projo.prefs.security.SwingUiPasswordSource;
import au.net.causal.projo.prefs.transform.AlwaysFailSecurityTransformer;
import au.net.causal.projo.prefs.transform.Base64Transformer;
import au.net.causal.projo.prefs.transform.BigDecimalToStringTransformer;
import au.net.causal.projo.prefs.transform.BigIntegerToStringTransformer;
import au.net.causal.projo.prefs.transform.BooleanToStringTransformer;
import au.net.causal.projo.prefs.transform.ByteToStringTransformer;
import au.net.causal.projo.prefs.transform.CalendarToDateTransformer;
import au.net.causal.projo.prefs.transform.CharacterToStringTransformer;
import au.net.causal.projo.prefs.transform.ColorToStringTransformer;
import au.net.causal.projo.prefs.transform.DateToStringTransformer;
import au.net.causal.projo.prefs.transform.DimensionTransformer;
import au.net.causal.projo.prefs.transform.DoubleToStringTransformer;
import au.net.causal.projo.prefs.transform.EncryptedValueTransformer;
import au.net.causal.projo.prefs.transform.EnumToStringTransformer;
import au.net.causal.projo.prefs.transform.FileToPathTransformer;
import au.net.causal.projo.prefs.transform.FloatToStringTransformer;
import au.net.causal.projo.prefs.transform.FloatingPointCastTransformer;
import au.net.causal.projo.prefs.transform.FontTransformer;
import au.net.causal.projo.prefs.transform.IntToStringTransformer;
import au.net.causal.projo.prefs.transform.ListTransformer;
import au.net.causal.projo.prefs.transform.LocaleToStringTransformer;
import au.net.causal.projo.prefs.transform.LongToStringTransformer;
import au.net.causal.projo.prefs.transform.NumericCastTransformer;
import au.net.causal.projo.prefs.transform.PathToStringTransformer;
import au.net.causal.projo.prefs.transform.PreferenceTransformer;
import au.net.causal.projo.prefs.transform.RectangleTransformer;
import au.net.causal.projo.prefs.transform.ShortToStringTransformer;
import au.net.causal.projo.prefs.transform.StringToByteTransformer;
import au.net.causal.projo.prefs.transform.TimeZoneToStringTransformer;
import au.net.causal.projo.prefs.transform.UriToStringTransformer;
import au.net.causal.projo.prefs.transform.UuidToStringTransformer;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;

/**
* Builder for creating {@link ProjoStore}s. 
* <p>
*
* Provides a fluent API for creating and configuring projo stores, which are used for loading and saving preferencces using annotated classes.  Use
* {@link #builder()} to get an instance of a builder, then call various methods to configure it and finally get a store instance by using {@link #build()}.
* <p>
*
*   e.g.
* <code>ProjoStore store = ProjoStoreBuilder.builder().defaults().node(node).build();</code>
* <p>
*
* <h3>Typical configuration of a builder</h3>
*
* <ol>
*   <li>Preference store node - use {@link #defaultNode()} or {@link #node(PreferenceNode)} to configure where settings will be stored</li>
*   <li>Data type transforms - use {@link #defaultTransformers()}, {@link #addTransformer(PreferenceTransformer, TransformPhase)} and {@link #replaceTransformer(Class, PreferenceTransformer, TransformPhase)} where needed.</li>
*   <li>Security - use {@link #configureSecurityForNativeSystem(PasswordSource)} or one of the other security configuration methods to configure secure settings.</li>
* </ol>
*
* <i>Note: failing to configure security will mean that any attempt to load or save {@literal @}{@link Secure} settings will result in an exception.</i>
*
* <h3>Typical Use</h3>
*
* The most typical use of Projo will use operating system defaults. 
* <p>
*
* For a console application:
* <pre>ProjoStore store = ProjoStoreBuilder.builder().defaults().configureSecurityForNativeSystem(new ConsoleUiPasswordSource()).build();</pre>
*
* For a Swing application:
* <pre>ProjoStore store = ProjoStoreBuilder.builder().defaults().configureSecurityForNativeSystem(new SwingUiPasswordSource()).build();</pre>
*
* @author prunge
*/
public class ProjoStoreBuilder
{
  private static final Logger log = LoggerFactory.getLogger(ProjoStoreBuilder.class);
 
  private static final String DEFAULT_DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
 
  private PreferenceNode baseNode;
 
  private final List<TransformerEntry> transformers = new ArrayList<>();
  private final ListMultimap<Class<? extends PreferenceTransformer>, PreferenceTransformer> transformerInstanceMap = ArrayListMultimap.create();
 
  private ByteEncryptor securityEncrypter;
 
  /**
   * Gets a new unconfigured builder instance.
   *
   * @return a builder instance.
   */
  public static ProjoStoreBuilder builder()
  {
    return(new ProjoStoreBuilder());
  }
 
  /**
   * Uses the builder's configuration to create a store.
   *
   * @return the created store.
   */
  public ProjoStore build()
  {
    if (baseNode == null)
      defaultNode();
   
    PreferenceNode node;
    if (transformers.isEmpty())
      node = baseNode;
    else
    {
      TransformerPreferenceNode xNode = new TransformerPreferenceNode(baseNode);
     
      //Security
      if (securityEncrypter == null)
        xNode.addTransformer(new AlwaysFailSecurityTransformer(), StandardTransformPhase.DATATYPE);
      else
        xNode.addTransformer(new EncryptedValueTransformer(securityEncrypter), StandardTransformPhase.DATATYPE);
     
      for (TransformerEntry entry : transformers)
      {
        xNode.addTransformer(entry.getTransformer(), entry.getPhase());
      }
     
      node = xNode;
    }
   
    return(new ProjoStore(node));
  }
 
  /**
   * Configures default transformers and node.  This is a shortcut for {@link #defaultTransformers()} and {@link #defaultNode()} but in one step.
   *
   * @return this builder.
   */
  public ProjoStoreBuilder defaults()
  {
    defaultTransformers();
    defaultNode();
    return(this);
  }
 
  /**
   * Configures the default data transformers.
   * <p>
   *
   * A set of standard data transformers are configured.  These will help converting datatypes that are not natively supported by preference store itself.
   * <p>
   *
   * It is possible to further customize transformers after calling this method by using {@link #addTransformer(PreferenceTransformer, TransformPhase)} and
   * {@link #replaceTransformer(Class, PreferenceTransformer, TransformPhase)}.
   *
   * @return this builder.
   */
  public ProjoStoreBuilder defaultTransformers()
  {
    addTransformer(new ListTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new RectangleTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new DimensionTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new NumericCastTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new FloatingPointCastTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new IntToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new LongToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new ShortToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new ByteToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new BigIntegerToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new DoubleToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new FloatToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new BigDecimalToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new DateToStringTransformer(new SimpleDateFormat(DEFAULT_DATE_FORMAT_PATTERN, Locale.ENGLISH)), StandardTransformPhase.DATATYPE);
    addTransformer(new CalendarToDateTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new BooleanToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new CharacterToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new UuidToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new UriToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new EnumToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new PathToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(FontTransformer.createStandardFontTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new LocaleToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new TimeZoneToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new FileToPathTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new ColorToStringTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new StringToByteTransformer(), StandardTransformPhase.DATATYPE);
    addTransformer(new Base64Transformer(), StandardTransformPhase.DATATYPE);
   
    return(this);
  }
 
  /**
   * Configures a custom transformer.
   * 
   * @param transformer the transformer to configure.
   * @param phase the phase to configure the transformer in.  Typically this will be one of the {@link StandardTransformPhase}s.
   *
   * @return this builder.
   *
   * @throws NullPointerException if <code>transformer</code> or <code>phase</code> is null.
   *
   * @see #replaceTransformer(Class, PreferenceTransformer, TransformPhase)
   */
  public ProjoStoreBuilder addTransformer(PreferenceTransformer transformer, TransformPhase phase)
  {
    if (transformer == null)
      throw new NullPointerException("transformer == null");
    if (phase == null)
      throw new NullPointerException("phase == null");
   
    transformers.add(new TransformerEntry(phase, transformer));
    transformerInstanceMap.put(transformer.getClass(), transformer);
   
    return(this);
  }
 
  /**
   * Adds a modular configuration to the builder.
   * <p>
   *
   * Modules typically configure one or more properties of a store and are used by libraries.
   *
   * @param module the module to add.
   *
   * @return this builder.
   *
   * @throws NullPointerException if <code>module</code> is null.
   */
  public ProjoStoreBuilder configureModule(ProjoBuilderModule module)
  {
    module.configure(this);
    return(this);
  }
 
  /**
   * Replaces a transformer of the specified type with a new transformer.  Useful for replacing existing registered transformers with customized versions.
   * <p>
   *
   * If no transformers of the specified type exist, then the <code>replaceWith</code> transformer is simply registered like with
   * {@link #addTransformer(PreferenceTransformer, TransformPhase)}.
   *
   * @param typeToReplace the transformer type to replace.  If multiple transformers of this type exist, the first one is replaced.
   * @param replaceWith the transformer to use as the replacement.
   * @param phase the phase.  Typically this will be one of the {@link StandardTransformPhase}s.
   *
   * @return this builder.
   *
   * @throws NullPointerException if any parameter is null.
   *
   * @see #addTransformer(PreferenceTransformer, TransformPhase)
   */
  public ProjoStoreBuilder replaceTransformer(Class<? extends PreferenceTransformer> typeToReplace, PreferenceTransformer replaceWith, TransformPhase phase)
  {
    if (typeToReplace == null)
      throw new NullPointerException("typeToReplace == null");
    if (replaceWith == null)
      throw new NullPointerException("replaceWith == null");
    if (phase == null)
      throw new NullPointerException("phase == null");
   
    for (ListIterator<TransformerEntry> i = transformers.listIterator(); i.hasNext();)
    {
      TransformerEntry entry = i.next();
      if (entry.getPhase() == phase && typeToReplace.isInstance(entry.getTransformer()))
      {
        //Replace in entry list
        TransformerEntry replacementEntry = new TransformerEntry(phase, replaceWith);
        i.set(replacementEntry);
       
        //Replace in instance list
        List<PreferenceTransformer> xInstances = transformerInstanceMap.get(entry.getTransformer().getClass());
        xInstances.remove(entry.getTransformer());       
       
        transformerInstanceMap.put(replaceWith.getClass(), replaceWith);
       
        return(this);
      }
    }
   
    //If it was not found just add it to the end
    return(addTransformer(replaceWith, phase));
  }
 
  /**
   * Configures the default root node for the system.
   * <p>
   *
   * On Windows, this will attempt to use a customized registry preference store, but requires the appropriate JAR file on the classpath.
   * Otherwise, this will use a preference store backed by the {@linkplain Preferences Java preferences API}.
   * <p>
   *
   * The preference node configured will be the root for all preferences of all applications, but the store will be configured to use
   * {@linkplain ProjoStore#setPreferenceRootUsed(boolean) preference roots configured from preference objects}, which typically means the node used
   * for settings will be derived from the package and class name of the preference object.
   *
   * @return this builder.
   *
   * @see #node(PreferenceNode)
   */
  public ProjoStoreBuilder defaultNode()
  {
    //Use the Windows registry on Windows if the appropriate libs are on classpath
    if (SystemUtils.IS_OS_WINDOWS)
    {
      //Do we have access to native Windows lib?
      try
      {
        //Use reflection in case the lib does not exist (in which case we fall back to prompt)
        Class<? extends PreferenceNode> registryNodeClass = Class.forName("au.net.causal.projo.prefs.windows.WindowsRegistryNode", true, ProjoStoreBuilder.class.getClassLoader()).asSubclass(PreferenceNode.class);
       
        return(node((PreferenceNode)(registryNodeClass.getMethod("javaPreferencesRootNode").invoke(null))));
      }
      catch (ClassNotFoundException e)
      {
        //Totally expected if we don't have the lib on the classpath, so only debug
        log.debug("Failed to load native Windows Registry preference node class.", e);
      }
      catch (ReflectiveOperationException e)
      {
        //Anything other than CNFE should be a warning as it should not happen normally
        log.warn("Error loading native Windows Registry preference node class: " + e, e);
      }
    }
   
    //Just use standard Java preferences otherwise
    Preferences pref = Preferences.userRoot();
    node(new JavaPreferenceNode(pref));
   
    return(this);
  }
 
  /**
   * Uses a specific preference node for storing settings.
   *
   * @param node the node to store settings in.
   *
   * @return this builder.
   *
   * @throws NullPointerException if <code>node</code> is null.
   */
  public ProjoStoreBuilder node(PreferenceNode node)
  {
    if (node == null)
      throw new NullPointerException("node == null");
   
    baseNode = node;
   
    return(this);
  }
 
  /**
   * Configure encryption of preferences witha an encrypter that does not need a password.
   *
   * @param simpleEncrypter the encrypter to use for encrypting and decrypting secure settings.
   *
   * @return this builder.
   *
   * @throws NullPointerException if <code>simpleEncrypter</code> is null.
   *
   * @see #configureSecurity(PBEByteEncryptor, PasswordSource)
   * @see #configureSecurityForNativeSystem(PasswordSource)
   */
  public ProjoStoreBuilder configureSecurity(ByteEncryptor simpleEncrypter)
  {
    if (simpleEncrypter == null)
      throw new NullPointerException("simpleEncrypter == null");
   
    this.securityEncrypter = simpleEncrypter;
   
    return(this);
  }
 
  /**
   * Configures encryption of preference with an encrypter that needs a password.
   *
   * @param encrypter the cnrypter to use for encrypting and decrypting secure settings.
   * @param passwordSource where to read passwords from.
   *
   * @return this builder.
   *
   * @throws NullPointerException if <code>encrypter</code> or <code>passwordSource</code> is null.
   *
   * @see #configureSecurity(ByteEncryptor)
   * @see #configureSecurityForNativeSystem(PasswordSource)
   */
  public ProjoStoreBuilder configureSecurity(PBEByteEncryptor encrypter, PasswordSource passwordSource)
  {
    if (encrypter == null)
      throw new NullPointerException("encrypter == null");
    if (passwordSource == null)
      throw new NullPointerException("passwordSource == null");
   
    this.securityEncrypter = new SourcedByteEncrypter(encrypter, passwordSource);
   
    return(this);
  }
 
  /**
   * Configures security appropriately for the native operating system.  Where possible, a promptless encrypter will be used.  For example, on Windows
   * DPAPI is used that uses the currently logged in user's credentials to perform encryption.
   * <p>
   *
   * Settings are not guaranteed be saved in a cross-platform compatible way.  If there is a need to have secure settings work across multiple operating
   * systems, then the {@link #configureSecurityForMasterPassword(PasswordSource)} or one of the other custom security configuration methods should be
   * used instead.
   * <p>
   *
   * Depending on the application, the {@link ConsoleUiPasswordSource} or {@link SwingUiPasswordSource} are possible choices to use as password sources,
   * or a custom implementation can be used.
   *
   * @param promptFallback if no promptless encryption facility exists on the operating system, the user will be prompted for a password using
   *       this password source.  Will not be used if the operating system supports promptless encryption.
   *       If this is <code>null</code>, and no promptless encrypters are available on the operating system, Projo will throw an exception whenever
   *       an attempt is made to load or store a secure setting. 
   *
   * @return this builder.
   *
   * @see #configureSecurity(PBEByteEncryptor, PasswordSource)
   */
  public ProjoStoreBuilder configureSecurityForNativeSystem(PasswordSource promptFallback)
  {
    //This is the tough part where we need knowledge of what native encrypters work with which OS
    if (SystemUtils.IS_OS_WINDOWS)
    {
      //Do we have access to native Windows lib?
      try
      {
        //Use reflection in case the lib does not exist (in which case we fall back to prompt)
        Class<? extends ByteEncryptor> windowsEncrypter = Class.forName("au.net.causal.projo.prefs.security.windows.WindowsDpApiEncrypter").asSubclass(ByteEncryptor.class);
        ByteEncryptor instance = windowsEncrypter.getConstructor().newInstance();
       
        return(configureSecurity(instance));
      }
      catch (ClassNotFoundException e)
      {
        //Totally expected if we don't have the lib on the classpath, so only debug
        log.debug("Failed to load native Windows encrypter.", e);
      }
      catch (ReflectiveOperationException e)
      {
        //Anything other than CNFE should be a warning as it should not happen normally
        log.warn("Error loading native Windows encrypter: " + e, e);
      }
    }
    //TODO Gnome keyring support
   
    return(configureSecurityForMasterPassword(promptFallback));
  }
 
  /**
   * Configures security so that secure settings are encrypted using a password and a standard encryption.
   * <p>
   *
   * This security configuration method will store secure credentials in a cross-platform compatible way, but will require a master password source.
   *
   * @param masterPasswordSource the source to read the master password from.
   *
   * @return this builder.
   *
   * @throws NullPointerException if <code>masterPasswordSource</code> is null.
   */
  public ProjoStoreBuilder configureSecurityForMasterPassword(PasswordSource masterPasswordSource)
  {
    return(configureSecurity(new StandardPBEByteEncryptor(), masterPasswordSource));
   
  }
 
  /**
   * Retrieves the first instance of a transformer of the specified type that is registered on the builder. 
   * <p>
   *
   * Useful for configuring indivudual transforms.
   *
   * @param transformerType the transformer type.
   *
   * @return the first registered instance of the transformer of the specified type, or <code>null</code> if no transformer of this type is registered.
   *
   * @throws NullPointerException if <code>transformerType</code> is null.
   */
  public <P extends PreferenceTransformer> P getTransformer(Class<P> transformerType)
  {
    if (transformerType == null)
      throw new NullPointerException("transformerType == null");
   
    List<? extends P> transformers = getTransformers(transformerType);
    if (transformers.isEmpty())
      return(null);
    else
      return(transformers.get(0));
  }
 
  /**
   * Retrieves all instances of a transformer of the specified type that is registered on the builder. 
   * <p>
   *
   * Useful for configuring indivudual transforms.
   *
   * @param transformerType the transformer type.
   *
   * @return all instance of the transformer of the specified type.  Returns an empty list if no transformers of the specified type are registered.
   *
   * @throws NullPointerException if <code>transformerType</code> is null.
   */
  public <P extends PreferenceTransformer> List<? extends P> getTransformers(Class<P> transformerType)
  {
    if (transformerType == null)
      throw new NullPointerException("transformerType == null");
   
    //Safe because it is a class->instance list map
    @SuppressWarnings({ "unchecked", "rawtypes" })
    List<? extends P> instances = (List)transformerInstanceMap.get(transformerType);
   
    return(instances);
  }
 
  /**
   * An entry to keep track of registered transformers and their phase.
   *
   * @author prunge
   */
  private static class TransformerEntry
  {
    private final TransformPhase phase;
    private final PreferenceTransformer transformer;
   
    /**
     * Creates a <code>TransformerEntry</code>.
     *
     * @param phase the phase the transformer is registered in.
     * @param transformer the transformer that was registered.
     */
    public TransformerEntry(TransformPhase phase, PreferenceTransformer transformer)
    {
      this.phase = phase;
      this.transformer = transformer;
    }

    /**
     * Returns the phase the transformer was registered in.
     *
     * @return the phase the transformer was registered in.
     */
    public TransformPhase getPhase()
    {
      return(phase);
    }

    /**
     * Returns the transformer that was registered.
     *
     * @return the transformer that was registered.
     */
    public PreferenceTransformer getTransformer()
    {
      return(transformer);
    }
  }

}
TOP

Related Classes of au.net.causal.projo.prefs.ProjoStoreBuilder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.