diff --git a/src/main/java/net/locusworks/crypto/configuration/ConfigurationCallback.java b/src/main/java/net/locusworks/crypto/configuration/ConfigurationCallback.java new file mode 100644 index 0000000..4ae0c9c --- /dev/null +++ b/src/main/java/net/locusworks/crypto/configuration/ConfigurationCallback.java @@ -0,0 +1,6 @@ +package net.locusworks.crypto.configuration; + +@FunctionalInterface +public interface ConfigurationCallback { + void results(String msg); +} diff --git a/src/main/java/net/locusworks/crypto/configuration/ConfigurationManager.java b/src/main/java/net/locusworks/crypto/configuration/ConfigurationManager.java new file mode 100644 index 0000000..3aa3464 --- /dev/null +++ b/src/main/java/net/locusworks/crypto/configuration/ConfigurationManager.java @@ -0,0 +1,149 @@ +package net.locusworks.crypto.configuration; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.function.Consumer; + +import org.apache.commons.lang3.StringUtils; + +import net.locusworks.crypto.AES; + +public class ConfigurationManager { + + private Properties configuration; + private Properties defaults = null; + private Path conf = null; + + protected AES aes; + + private Consumer callback; + + protected void init(String baseDir, String propertiesFile, Consumer callback) throws IOException { + init(baseDir, propertiesFile, this.getClass().getName().getBytes(StandardCharsets.UTF_8), callback); + } + + protected void init(String baseDir, String propertiesFile, byte[] aesKey, Consumer callback) throws IOException { + aes = aesKey.length > 0 ? AES.createInstance(aesKey) : AES.createInstance(); + this.callback = callback; + try { + defaults = PropertiesManager.loadConfiguration(this.getClass(), propertiesFile); + } catch (IOException ex) { + throw ex; + } + // create patchrepoConf File object + conf = Paths.get(baseDir).resolve(propertiesFile); + + loadConfiguration(); + } + + private void loadConfiguration() { + // load the active config file + // ignore read error, we can continue with an empty configuration map + // and all default items will be added below and the file created + + callbackMessage("Loading config file: " + conf); + try { + configuration = PropertiesManager.loadConfiguration(conf); + } catch (Exception e) { + callbackMessage("Config file: " + conf + " will be created from template"); + configuration = new Properties(); + } + + Map results = PropertiesManager.addConfiguration(configuration, defaults); + boolean changed = !results.isEmpty(); + if (!results.isEmpty()) { + StringBuilder sb = new StringBuilder("Added new configuration items:\n"); + for (Entry entry : results.entrySet()) { + sb.append(String.format("%s=%s\n", entry.getKey(), entry.getValue())); + } + callbackMessage(sb.toString()); + } + + results = PropertiesManager.removeConfiguration(configuration, defaults); + changed |= !results.isEmpty(); + if (!results.isEmpty()) { + StringBuilder sb = new StringBuilder("Added new configuration items:\n"); + for (Entry entry : results.entrySet()) { + sb.append(String.format("%s=%s\n", entry.getKey(), entry.getValue())); + } + callbackMessage(sb.toString()); + } + + if (changed) { + PropertiesManager.saveConfiguration(configuration, conf, "Patch Repository properties file"); + } + } + + /** + * Save the configuration values to file + * @param confs Configuration property values to save + * @throws Exception general exception + */ + public void saveToConf(Properties confs) throws Exception { + PropertiesManager.saveConfiguration(confs, conf, conf.getFileName().toString()); + callbackMessage("Saved config file: " + conf + ", " + confs.size() + " entries"); + loadConfiguration(); + } + + public String getPropertyValue(String key) { + return getPropertyValue(key, null); + } + public String getPropertyValue(String key, String defaultValue) { + return configuration.containsKey(key) ? configuration.getProperty(key) : defaultValue; + } + + public Properties getConfiguration() { + return configuration; + } + + public void saveConfiguration(PersistableRequest request, Set fieldsToSave, Set excryptedFields) throws IOException { + if (fieldsToSave == null || fieldsToSave.isEmpty()) { + throw new IOException("No fields to save were defined"); + } + if (excryptedFields == null) { + excryptedFields = new HashSet<>(); + } + try { + Properties props = new Properties(); + + //copy what is current in the configuration settings into the new properties file + configuration.entrySet().forEach(item -> props.setProperty(String.valueOf(item.getKey()), String.valueOf(item.getValue()))); + boolean changed = false; + for (Field f : request.getClass().getDeclaredFields()) { + f.setAccessible(true); + String fieldName = f.getName(); + String fieldValue = String.valueOf(f.get(request)); + + //Ensures we are only saving values that are already configured + if (!fieldsToSave.contains(fieldName)) continue; + + //Check to see if the old value changed + String oldValue = props.getProperty(fieldName); + if (StringUtils.isAnyBlank(oldValue, fieldValue) || oldValue.equals(fieldValue)) continue; + + changed = true; + + fieldValue = excryptedFields.contains(fieldName) ? aes.encrypt(fieldValue) : fieldValue; + + props.setProperty(fieldName, fieldValue); + } + if (changed) { + saveToConf(props); + } + } catch (Exception ex) { + throw new IOException(ex); + } + } + + private void callbackMessage(String msg) { + if (callback != null) callback.accept(msg); + } +} diff --git a/src/main/java/net/locusworks/crypto/configuration/PersistableRequest.java b/src/main/java/net/locusworks/crypto/configuration/PersistableRequest.java new file mode 100644 index 0000000..79385fe --- /dev/null +++ b/src/main/java/net/locusworks/crypto/configuration/PersistableRequest.java @@ -0,0 +1,5 @@ +package net.locusworks.crypto.configuration; + +public interface PersistableRequest { + +} diff --git a/src/main/java/net/locusworks/crypto/configuration/PropertiesManager.java b/src/main/java/net/locusworks/crypto/configuration/PropertiesManager.java new file mode 100644 index 0000000..9368743 --- /dev/null +++ b/src/main/java/net/locusworks/crypto/configuration/PropertiesManager.java @@ -0,0 +1,153 @@ +package net.locusworks.crypto.configuration; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.tuple.ImmutablePair; + +/** + * Properties manager class to help load and read properties + * @author Isaac Parenteau + * @version 1.0.0 + * @date 02/15/2018 + */ +public class PropertiesManager { + /** + * Load a configuration from resource + * @param clazz class loader + * @param src source of the file + * @return properties + * @throws IOException Exception thrown the file can't be read + */ + public static Properties loadConfiguration(Class clazz, String src) throws IOException { + InputStream is = clazz.getResourceAsStream(src); + if (is == null) { + is = clazz.getClassLoader().getResourceAsStream(src); + } + if (is == null) { + return null; + } + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + return loadConfiguration(br); + } + + /** + * Load configuration from a file. This method has been deprecated and may be removed in + * future released. Please use java.nio.Path + * @param file File to load + * @return properties + * @throws IOException Exception thrown the file can't be read + */ + @Deprecated + public static Properties loadConfiguration(File file) throws IOException { + return loadConfiguration(file.toPath()); + } + + /** + * Load configuration from a file. + * @param path the path to the file + * @return properties + * @throws IOException Exception thrown the file can't be read + */ + public static Properties loadConfiguration(Path path) throws IOException { + if (Files.notExists(path)) { + return new Properties(); + } + + try(BufferedReader br = Files.newBufferedReader(path)) { + return loadConfiguration(br); + } + } + + /** + * Load configuration from a buffered reader + * @param reader Buffered reader to read the properties values from + * @return properties + * @throws IOException Exception thrown the file can't be read + */ + public static Properties loadConfiguration(BufferedReader reader) throws IOException { + Properties config = new Properties(); + config.load(reader); + return config; + } + + /** + * Add configurations from one properties file to another + * @param to Properties file to copy values to + * @param from Properties file to copy values from + * @return a map containing the results of the values added + */ + public static Map addConfiguration(Properties to, Properties from) { + Map results = from.entrySet() + .stream() + .filter(entry -> !to.containsKey(entry.getKey())) + .map(entry -> { + String key = entry.getKey().toString(); + String value = entry.getValue().toString(); + to.put(key, value); + return new ImmutablePair(key, value); + }) + .collect(Collectors.toMap(key -> key.getLeft(), value -> value.getRight())); + + return results; + } + + /** + * Removes configuration values that are not present in the comparedTo + * @param from Properties file to remove values from + * @param comparedTo Properties file to compare to + * @return a map containing the results of the values removed + */ + public static Map removeConfiguration(Properties from, Properties comparedTo) { + Map results = from.keySet() + .stream() + .filter(key -> !comparedTo.containsKey(key)) //only get the items that are not in the comparedTo properties + .map(key -> new ImmutablePair(String.valueOf(key), String.valueOf(from.get(key)))) + .collect(Collectors.toList()) //Create a list of paired items (key value) of the items that were filtered + .stream() + .map(pair -> { //remove those pairs from the from properties + from.remove(pair.getLeft()); + return pair; + }) + .collect(Collectors.toMap(key -> key.getLeft(), value -> value.getRight())); //create a map of what was removed + + return results; + } + + /** + *

Save the properties file to disk.

+ *

This method has been depecreated and could be removed in future release.
Please use java.nio.Path

+ * @param props Properties file to save + * @param fileToSave File to save to + * @param comment Any comments to add + */ + @Deprecated + public static void saveConfiguration(Properties props, File fileToSave, String comment) { + saveConfiguration(props, fileToSave.toPath(), comment); + + } + + /** + *

Save the properties file to disk.

+ * @param props Properties file to save + * @param fileToSave File to save to + * @param comment Any comments to add + */ + public static void saveConfiguration(Properties props, Path fileToSave, String comment) { + try(OutputStream fos = Files.newOutputStream(fileToSave)) { + props.store(fos, comment == null ? "" : comment); + } catch (IOException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + } +} diff --git a/src/test/java/net/locusworks/crypto/tests/PropertiesManagerTest.java b/src/test/java/net/locusworks/crypto/tests/PropertiesManagerTest.java new file mode 100644 index 0000000..b91808a --- /dev/null +++ b/src/test/java/net/locusworks/crypto/tests/PropertiesManagerTest.java @@ -0,0 +1,124 @@ +package net.locusworks.crypto.tests; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; + +import org.junit.AfterClass; +import org.junit.Test; + +import net.locusworks.crypto.configuration.PropertiesManager; + +/** + * Test cases for the properties manager class + * @author Isaac Parenteau + * @since 1.0.0-RELEASE + * + */ +public class PropertiesManagerTest { + + private static final String PROPERTIES_FILE = "test.properties"; + private static final String TMP_PROPS = "temp.properties"; + private static final int ENTRY_SIZE = 4; + + public static enum Configuration { + DB_HOST("dbHost"), + DB_PORT("dbPort"), + USER_EXPIRATION_DAYS("userExpirationDays"), + LOG_LEVEL("logLevel"); + + private String value; + + private Configuration(String value) { + this.value = value; + } + + /** + * Get the current value of the enumeration + * @return value + */ + public String getValue() { + return this.value; + } + + @Override + public String toString() { + return this.value; + } + } + + @AfterClass + public static void removeSavedProps() { + File tmp = new File(TMP_PROPS); + if (tmp.exists()) { + tmp.delete(); + } + } + + @Test + public void testPropertiesLoad() { + try { + Properties props = PropertiesManager.loadConfiguration(this.getClass(), PROPERTIES_FILE); + assertTrue(props != null); + assertTrue(props.containsKey(Configuration.USER_EXPIRATION_DAYS.toString())); + assertTrue(props.containsKey(Configuration.DB_HOST.toString())); + assertTrue(props.containsKey(Configuration.DB_PORT.toString())); + assertTrue(props.containsKey(Configuration.LOG_LEVEL.toString())); + } catch (IOException e) { + fail(e.getMessage()); + } + } + + @Test + public void testAddConfiguration() { + try { + Properties props = PropertiesManager.loadConfiguration(this.getClass(), PROPERTIES_FILE); + Properties tmp = new Properties(); + assertTrue(tmp.keySet().size() == 0); + PropertiesManager.addConfiguration(tmp, props); + assertTrue(tmp.keySet().size() == ENTRY_SIZE); + assertTrue(tmp.containsKey(Configuration.USER_EXPIRATION_DAYS.toString())); + assertTrue(tmp.containsKey(Configuration.DB_HOST.toString())); + assertTrue(tmp.containsKey(Configuration.DB_PORT.toString())); + assertTrue(tmp.containsKey(Configuration.LOG_LEVEL.toString())); + } catch (IOException e) { + fail(e.getMessage()); + } + } + + @Test + public void testRemoveConfiguration() { + try { + Properties props = PropertiesManager.loadConfiguration(this.getClass(), PROPERTIES_FILE); + Properties tmp = new Properties(); + assertTrue(props.keySet().size() == ENTRY_SIZE); + assertTrue(tmp.keySet().size() == 0); + PropertiesManager.removeConfiguration(props, tmp); + assertTrue(props.keySet().size() == 0); + assertTrue(tmp.keySet().size() == 0); + } catch (IOException e) { + fail(e.getMessage()); + } + } + + @Test + public void testSaveConfiguration() { + try { + Properties props = PropertiesManager.loadConfiguration(this.getClass(), PROPERTIES_FILE); + Path tmpFile = Paths.get(TMP_PROPS); + PropertiesManager.saveConfiguration(props, tmpFile, "test propertis"); + Properties tmp = PropertiesManager.loadConfiguration(tmpFile); + assertTrue(tmp.keySet().size() == ENTRY_SIZE); + assertTrue(tmp.containsKey(Configuration.USER_EXPIRATION_DAYS.toString())); + assertTrue(tmp.containsKey(Configuration.DB_HOST.toString())); + assertTrue(tmp.containsKey(Configuration.DB_PORT.toString())); + assertTrue(tmp.containsKey(Configuration.LOG_LEVEL.toString())); + } catch (IOException e) { + fail(e.getMessage()); + } + } +} diff --git a/src/test/resources/log4j2-test.xml b/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..a2c3fe0 --- /dev/null +++ b/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties new file mode 100644 index 0000000..292c3da --- /dev/null +++ b/src/test/resources/test.properties @@ -0,0 +1,4 @@ +userExpirationDays=3650 +dbHost=localhost +dbPort=3306 +logLevel=INFO