diff --git a/pom.xml b/pom.xml index b87df82..6694516 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,8 @@ 1.2 1.9 2.4.0 + 1.2.0 + [0.4, 0.5) @@ -92,7 +94,12 @@ net.coobird thumbnailator - [0.4, 0.5) + ${lib.thumbnailator.version} + + + com.typesafe + config + ${lib.typesafe-config.version} net.sourceforge.jtransforms diff --git a/src/includes/logback.xml b/src/includes/logback.xml index 4a298fd..713ce5d 100644 --- a/src/includes/logback.xml +++ b/src/includes/logback.xml @@ -4,7 +4,7 @@ false - [%-5level [%c{16}] - %message%n + [%date{HH:mm:ss}] %-5level [%c{16}] - %message%n INFO diff --git a/src/main/java/com/sothr/imagetools/AppCLI.java b/src/main/java/com/sothr/imagetools/AppCLI.java index 0a20ad9..6014b3e 100644 --- a/src/main/java/com/sothr/imagetools/AppCLI.java +++ b/src/main/java/com/sothr/imagetools/AppCLI.java @@ -26,6 +26,7 @@ class AppCLI { CommandLineParser parser = new BasicParser(); CommandLine cmd = parser.parse(options, args); process(cmd); + AppConfig.saveProperties(); System.exit(0); } catch (Exception ex) { logger.error("Unhandled exception in AppCLI",ex); diff --git a/src/main/java/com/sothr/imagetools/AppConfig.java b/src/main/java/com/sothr/imagetools/AppConfig.java index 366250d..fd5f6a7 100644 --- a/src/main/java/com/sothr/imagetools/AppConfig.java +++ b/src/main/java/com/sothr/imagetools/AppConfig.java @@ -26,8 +26,8 @@ public class AppConfig { private static Boolean configuredLogging = false; //Properties defaults - private static final String DEFAULTPROPERTIESFILE = "default.properties"; - private static final String USERPROPERTIESFILE = "./config.xml"; + private static final String DEFAULTPROPERTIESFILE = "application.conf"; + private static final String USERPROPERTIESFILE = "user.conf"; private static Boolean loadedProperties = false; //Cache defaults @@ -110,7 +110,7 @@ public class AppConfig { } public static void saveProperties() { - PropertiesService.saveXMLProperties(USERPROPERTIESFILE); + PropertiesService.saveConf(USERPROPERTIESFILE); logger.debug("Saved properties"); } diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index d2fcb02..9ae72b5 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1,6 +1,5 @@ //Default Properties File //Image Tools version: ${project.version} -version = "${build-version}" akka { loggers = ["akka.event.slf4j.Slf4jLogger"] @@ -9,12 +8,8 @@ akka { //Default App Settings app { + version.current = "${build-version}" timed = false - log { - debug = false - info = false - error = true - } engine { //Concurrency Settings concurrent { @@ -22,41 +17,39 @@ app { processing.limit = 15 } } -} - -#Default Image Settings -image { - //images must be 90% similar - differenceThreshold = 0.90 - //control generation of hashes for new images. - hash { - precision=64 - } - ahash { - use = true - weight = 0.70 - precision = 8 - tolerence = 8 - } - dhash { - use = true - weight = 0.85 - precision = 8 - tolerence = 8 + #Default Image Settings + image { + //images must be 90% similar + differenceThreshold = 0.90 + //control generation of hashes for new images. + hash { + precision=64 + } + ahash { + use = true + weight = 0.70 + precision = 8 + tolerence = 8 + } + dhash { + use = true + weight = 0.85 + precision = 8 + tolerence = 8 + } + phash { + //set to false if hashing images is taking too long + use = true + weight = 1.0 + precision = 32 + tolerence = 8 + } } - phash { - //set to false if hashing images is taking too long - use = true - weight = 1.0 - precision = 32 - tolerence = 8 + //Default Thumbnail Settings + thumbnail { + //Directory where to store thumbnails + directory = ".cache/thumbnails/" + //Size of the thumbnail to generate and store + size = 128 } -} - -//Default Thumbnail Settings -thumbnail { - //Directory where to store thumbnails - directory = ".cache/thumbnails/" - //Size of the thumbnail to generate and store - size = 128 } \ No newline at end of file diff --git a/src/main/scala/com/sothr/imagetools/util/PropertiesEnum.scala b/src/main/scala/com/sothr/imagetools/util/PropertiesEnum.scala index 0ccb433..b449ecf 100644 --- a/src/main/scala/com/sothr/imagetools/util/PropertiesEnum.scala +++ b/src/main/scala/com/sothr/imagetools/util/PropertiesEnum.scala @@ -2,31 +2,29 @@ package com.sothr.imagetools.util object PropertiesEnum extends Enumeration { type PropertiesEnum = Value - val Version = Value("version") + val Version = Value("app.version.current") + val PreviousVersion = Value("app.version.previous") //default app settings - val LogDebug = Value("app.log.debug") - val LogInfo = Value("app.log.info") - val LogError = Value("app.log.error") val Timed = Value("app.timed") //default engine concurrency settings val ConcurrentSimiliartyLimit = Value("app.engine.concurrent.similarity.limit") val ConcurrentProcessingLimit = Value("app.engine.concurrent.processing.limit") //default image settings - val ImageDifferenceThreshold = Value("image.differenceThreshold") - val HashPrecision = Value("image.hash.precision") - val UseAhash = Value("image.ahash.use") - val AhashWeight = Value("image.ahash.weight") - val AhashPrecision = Value("image.ahash.precision") - val AhashTolerance = Value("image.ahash.tolerence") - val UseDhash = Value("image.dhash.use") - val DhashWeight = Value("image.dhash.weight") - val DhashPrecision = Value("image.dhash.precision") - val DhashTolerance = Value("image.dhash.tolerence") - val UsePhash = Value("image.phash.use") - val PhashWeight = Value("image.phash.weight") - val PhashPrecision = Value("image.phash.precision") - val PhashTolerance = Value("image.phash.tolerence") + val ImageDifferenceThreshold = Value("app.image.differenceThreshold") + val HashPrecision = Value("app.image.hash.precision") + val UseAhash = Value("app.image.ahash.use") + val AhashWeight = Value("app.image.ahash.weight") + val AhashPrecision = Value("app.image.ahash.precision") + val AhashTolerance = Value("app.image.ahash.tolerence") + val UseDhash = Value("app.image.dhash.use") + val DhashWeight = Value("app.image.dhash.weight") + val DhashPrecision = Value("app.image.dhash.precision") + val DhashTolerance = Value("app.image.dhash.tolerence") + val UsePhash = Value("app.image.phash.use") + val PhashWeight = Value("app.image.phash.weight") + val PhashPrecision = Value("app.image.phash.precision") + val PhashTolerance = Value("app.image.phash.tolerence") //Default Thumbnail Settings - val ThumbnailDirectory = Value("thumbnail.directory") - val ThumbnailSize = Value("thumbnail.size") + val ThumbnailDirectory = Value("app.thumbnail.directory") + val ThumbnailSize = Value("app.thumbnail.size") } \ No newline at end of file diff --git a/src/main/scala/com/sothr/imagetools/util/PropertiesService.scala b/src/main/scala/com/sothr/imagetools/util/PropertiesService.scala index 103f553..658724b 100644 --- a/src/main/scala/com/sothr/imagetools/util/PropertiesService.scala +++ b/src/main/scala/com/sothr/imagetools/util/PropertiesService.scala @@ -1,8 +1,9 @@ package com.sothr.imagetools.util -import java.util.Properties +import com.typesafe.config.{Config, ConfigFactory} import grizzled.slf4j.Logging -import java.io.{FileInputStream, FileOutputStream, OutputStream} +import java.io.{File, PrintStream, FileOutputStream} +import java.util.Properties import scala.collection.JavaConversions._ /* @@ -10,107 +11,109 @@ import scala.collection.JavaConversions._ */ object PropertiesService extends Logging { - private val properties:Properties = new Properties() + private var defaultConf:Config = null + private var userConf:Config = null + private var newUserConf:Properties = new Properties() private var version:Version = null def getVersion:Version = this.version - private val propertiesToClean:Array[String] = Array("version") + + //specific highly used properties + var TimingEnabled:Boolean = false + + //ahash + var aHashPrecision = 0 + var aHashTolerance = 0 + var aHashWeight = 0.0f + var useAhash = false + //dhash + var dHashPrecision = 0 + var dHashTolerance = 0 + var dHashWeight = 0.0f + var useDhash = false + //phash + var pHashPrecision = 0 + var pHashTolerance = 0 + var pHashWeight = 0.0f + var usePhash = false /* * Load the properties file from the specified location */ def loadProperties(defaultLocation:String, userLocation:String = null) = { info(s"Attempting to load properties from: $defaultLocation") - val defaultInputStream = ResourceLoader.get.getResourceStream(defaultLocation) - properties.load(defaultInputStream) + defaultConf = ConfigFactory.load(defaultLocation); if (userLocation != null) { - val userInputStream = new FileInputStream(userLocation) - val userProperties = new Properties(); - userProperties.loadFromXML(userInputStream) - - for (propertyName:String <- userProperties.stringPropertyNames()) { - properties.setProperty(propertyName, userProperties.getProperty(propertyName)); - } + userConf = ConfigFactory.parseFile(new File(userLocation)); } else { + userConf = ConfigFactory.empty info("No user properties file exists to load from") } - version = new Version(properties.getProperty("version")); + version = new Version(get(PropertiesEnum.Version.toString)); info(s"Detected Version: $version") //load special properties - DebugLogEnabled = get(PropertiesEnum.LogDebug.toString).toBoolean - InfoLogEnabled = get(PropertiesEnum.LogInfo.toString).toBoolean - ErrorLogEnabled = get(PropertiesEnum.LogError.toString).toBoolean TimingEnabled = get(PropertiesEnum.Timed.toString).toBoolean //ahash - aHashPrecision = PropertiesService.get(PropertiesEnum.AhashPrecision.toString).toInt - aHashTolerance = PropertiesService.get(PropertiesEnum.AhashTolerance.toString).toInt - aHashWeight = PropertiesService.get(PropertiesEnum.AhashWeight.toString).toFloat - useAhash = PropertiesService.get(PropertiesEnum.UseAhash.toString).toBoolean + aHashPrecision = get(PropertiesEnum.AhashPrecision.toString).toInt + aHashTolerance = get(PropertiesEnum.AhashTolerance.toString).toInt + aHashWeight = get(PropertiesEnum.AhashWeight.toString).toFloat + useAhash = get(PropertiesEnum.UseAhash.toString).toBoolean //dhash - dHashPrecision = PropertiesService.get(PropertiesEnum.DhashPrecision.toString).toInt - dHashTolerance = PropertiesService.get(PropertiesEnum.DhashTolerance.toString).toInt - dHashWeight = PropertiesService.get(PropertiesEnum.DhashWeight.toString).toFloat - useDhash = PropertiesService.get(PropertiesEnum.UseDhash.toString).toBoolean + dHashPrecision = get(PropertiesEnum.DhashPrecision.toString).toInt + dHashTolerance = get(PropertiesEnum.DhashTolerance.toString).toInt + dHashWeight = get(PropertiesEnum.DhashWeight.toString).toFloat + useDhash = get(PropertiesEnum.UseDhash.toString).toBoolean //phash - pHashPrecision = PropertiesService.get(PropertiesEnum.PhashPrecision.toString).toInt - pHashTolerance = PropertiesService.get(PropertiesEnum.PhashTolerance.toString).toInt - pHashWeight = PropertiesService.get(PropertiesEnum.PhashWeight.toString).toFloat - usePhash = PropertiesService.get(PropertiesEnum.UsePhash.toString).toBoolean + pHashPrecision = get(PropertiesEnum.PhashPrecision.toString).toInt + pHashTolerance = get(PropertiesEnum.PhashTolerance.toString).toInt + pHashWeight = get(PropertiesEnum.PhashWeight.toString).toFloat + usePhash = get(PropertiesEnum.UsePhash.toString).toBoolean info("Loaded Special Properties") } - /** - * Gets a properties object that is cleaned of things that are expected to change. i.e. version - */ - private def getCleanProperties():Properties = { - val cleanProperties:Properties = properties.clone().asInstanceOf[Properties] - //Remove properties to be cleaned - for (key <- propertiesToClean) { - cleanProperties.remove(key) - } - return cleanProperties + private def cleanAndPrepareNewUserProperties():Properties = { + //insert special keys here + newUserConf.setProperty(PropertiesEnum.PreviousVersion.toString, version.parsableToString()) + //remove special keys here + newUserConf.remove(PropertiesEnum.Version.toString) + newUserConf } - def saveXMLProperties(location:String) = { + private def getCleanedMergedUserConf():Config = { + + ConfigFactory.parseProperties(cleanAndPrepareNewUserProperties()) withFallback(userConf) + } + + def saveConf(location:String) = { info(s"Saving user properties to $location") - val out:OutputStream = new FileOutputStream(location, false) - val cleanProperties = getCleanProperties - //insert special keys here - cleanProperties.setProperty("version.previous", version.parsableToString()) - cleanProperties.storeToXML(out, "User Properties") + val out:PrintStream = new PrintStream(new FileOutputStream(location, false)) + val userConfToSave = getCleanedMergedUserConf + //print to the output stream + out.print(userConfToSave.root.render) out.flush() out.close() } - def get(key:String):String = { - return properties.getProperty(key) + def get(key:String, defaultValue:String=null):String = { + var result:String = defaultValue + //check the latest properties + if (newUserConf.containsKey(key)) { + result = newUserConf.getProperty(key) + } + //check the loaded user properties + else if (userConf.hasPath(key)) { + result = userConf.getString(key) + } + //check the default properties + else if (defaultConf.hasPath(key)) { + result = defaultConf.getString(key) + } + return result } def set(key:String, value:String) = { - properties.setProperty(key, value) + newUserConf.setProperty(key, value) } - - //specific highly used properties - var DebugLogEnabled:Boolean = false - var InfoLogEnabled:Boolean = false - var ErrorLogEnabled:Boolean = false - var TimingEnabled:Boolean = false - - //ahash - var aHashPrecision = 0 - var aHashTolerance = 0 - var aHashWeight = 0.0f - var useAhash = false - //dhash - var dHashPrecision = 0 - var dHashTolerance = 0 - var dHashWeight = 0.0f - var useDhash = false - //phash - var pHashPrecision = 0 - var pHashTolerance = 0 - var pHashWeight = 0.0f - var usePhash = false } \ No newline at end of file diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf index 6fab465..5d0a916 100644 --- a/src/test/resources/application.conf +++ b/src/test/resources/application.conf @@ -1,63 +1,55 @@ -//Default Properties File -//Image Tools version: ${project.version} -version = "${build-version}" - - -akka { - loggers = ["akka.event.slf4j.Slf4jLogger"] - loglevel = "DEBUG" -} - -//Default App Settings -app { - timed = false - log { - debug = true - info = true - error = true - } - engine { - //Concurrency Settings - concurrent { - similarity.limit = 15 - processing.limit = 15 - } - } -} - -#Default Image Settings -image { - //images must be 90% similar - differenceThreshold = 0.90 - //control generation of hashes for new images. - hash { - precision=64 - } - ahash { - use = true - weight = 0.70 - precision = 8 - tolerence = 8 - } - dhash { - use = true - weight = 0.85 - precision = 8 - tolerence = 8 - } - phash { - //set to false if hashing images is taking too long - use = true - weight = 1.0 - precision = 32 - tolerence = 8 - } -} - -//Default Thumbnail Settings -thumbnail { - //Directory where to store thumbnails - directory = ".cache/thumbnails/" - //Size of the thumbnail to generate and store - size = 128 +//Default Properties File +//Image Tools version: ${project.version} + +akka { + loggers = ["akka.event.slf4j.Slf4jLogger"] + loglevel = "INFO" +} + +//Default App Settings +app { + version.current = "${build-version}" + timed = false + engine { + //Concurrency Settings + concurrent { + similarity.limit = 2 + processing.limit = 2 + } + } + #Default Image Settings + image { + //images must be 90% similar + differenceThreshold = 0.90 + //control generation of hashes for new images. + hash { + precision=64 + } + ahash { + use = true + weight = 0.70 + precision = 8 + tolerence = 8 + } + dhash { + use = true + weight = 0.85 + precision = 8 + tolerence = 8 + } + phash { + //set to false if hashing images is taking too long + use = true + weight = 1.0 + precision = 32 + tolerence = 8 + } + } + //Default Thumbnail Settings + thumbnail { + //Directory where to store thumbnails + directory = ".cache/thumbnails/" + //Size of the thumbnail to generate and store + size = 128 + } } \ No newline at end of file diff --git a/todo b/todo index c5f252e..a160966 100644 --- a/todo +++ b/todo @@ -1,18 +1,9 @@ -Move over to Logback for the backend - -Convert the configuration file - -Convert the AppConfig to configure basic logging -Profile and improve performance for the ConcurrentEngine when looking for similarities. - -SynchronousEngine is about 2.5x faster currently for finding similarities - -ConcurrentEngine actors for finding similarities are not CPU constrained and need to be -Convert app configuration format from a java properties to a typesafehub:config - -Rewrite properties in new format (JSON) - -Rewrite PropertiesLoader to use the new format Add functionality to both engines - -Recursive directory scanning - -Determine how to handle similarites there... All images or just per folder -Move files to new locations -Rename new files based on their MD5 Add functionality to ImageService -Cache thumbnails -Generate thumbnails - -Move caching of images into the ImageService \ No newline at end of file +Improve functionality of the PropertiesService + -Getters and Setters for sepcific values with proper cascading and type awareness + -Nicer debugging? \ No newline at end of file