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);
}
}
}