diff --git a/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BashStartScriptPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BashStartScriptPlugin.scala index 0f9410896..89a18c3b2 100644 --- a/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BashStartScriptPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BashStartScriptPlugin.scala @@ -1,7 +1,6 @@ package com.typesafe.sbt.packager.archetypes.scripts import java.io.File -import java.net.URL import com.typesafe.sbt.SbtNativePackager.Universal import com.typesafe.sbt.packager.Keys._ @@ -16,7 +15,7 @@ import sbt._ * [[com.typesafe.sbt.packager.archetypes.JavaAppPackaging]]. * */ -object BashStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator { +object BashStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator with CommonStartScriptGenerator { /** * Name of the bash template if user wants to provide custom one @@ -26,27 +25,32 @@ object BashStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator { /** * Name of the bash forwarder template if user wants to provide custom one */ - val bashForwarderTemplate = "bash-forwarder-template" + override protected[this] val forwarderTemplateName = "bash-forwarder-template" /** * Location for the application.ini file used by the bash script to load initialization parameters for jvm and app */ val appIniLocation = "${app_home}/../conf/application.ini" - /** - * Script destination in final package - */ - val scriptTargetFolder = "bin" + override protected[this] val scriptSuffix: String = "" + override protected[this] val eol: String = "\n" + override protected[this] val keySurround: String => String = TemplateWriter.bashFriendlyKeySurround + override protected[this] val executableBitValue: Boolean = true override val requires = JavaAppPackaging override val trigger = AllRequirements object autoImport extends BashStartScriptKeys - private[this] case class BashScriptConfig(executableScriptName: String, - scriptClasspath: Seq[String], - bashScriptReplacements: Seq[(String, String)], - bashScriptTemplateLocation: File) + protected[this] case class BashScriptConfig(override val executableScriptName: String, + override val scriptClasspath: Seq[String], + override val replacements: Seq[(String, String)], + override val templateLocation: File) + extends ScriptConfig { + override def withScriptName(scriptName: String): BashScriptConfig = copy(executableScriptName = scriptName) + } + + override protected[this] type SpecializedScriptConfig = BashScriptConfig override def projectSettings: Seq[Setting[_]] = Seq( bashScriptTemplateLocation := (sourceDirectory.value / "templates" / bashTemplate), @@ -69,8 +73,8 @@ object BashStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator { BashScriptConfig( executableScriptName = executableScriptName.value, scriptClasspath = (scriptClasspath in bashScriptDefines).value, - bashScriptReplacements = bashScriptReplacements.value, - bashScriptTemplateLocation = bashScriptTemplateLocation.value + replacements = bashScriptReplacements.value, + templateLocation = bashScriptTemplateLocation.value ), (mainClass in (Compile, bashScriptDefines)).value, (discoveredMainClasses in Compile).value, @@ -85,44 +89,6 @@ object BashStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator { Seq("template_declares" -> defineString) } - private[this] def generateStartScripts(config: BashScriptConfig, - mainClass: Option[String], - discoveredMainClasses: Seq[String], - targetDir: File, - log: Logger): Seq[(File, String)] = - StartScriptMainClassConfig.from(mainClass, discoveredMainClasses) match { - case NoMain => - log.warn("You have no main class in your project. No start script will be generated.") - Seq.empty - case SingleMain(main) => - Seq(MainScript(main, config, targetDir, Seq(main)) -> s"$scriptTargetFolder/${config.executableScriptName}") - case MultipleMains(mains) => - generateMainScripts(mains, config, targetDir) - case ExplicitMainWithAdditional(main, additional) => - (MainScript(main, config, targetDir, discoveredMainClasses) -> s"$scriptTargetFolder/${config.executableScriptName}") +: - ForwarderScripts(config.executableScriptName, additional, targetDir) - } - - private[this] def generateMainScripts(discoveredMainClasses: Seq[String], - config: BashScriptConfig, - targetDir: File): Seq[(File, String)] = - discoveredMainClasses.map { qualifiedClassName => - val bashConfig = - config.copy(executableScriptName = makeScriptName(qualifiedClassName)) - MainScript(qualifiedClassName, bashConfig, targetDir, discoveredMainClasses) -> s"$scriptTargetFolder/${bashConfig.executableScriptName}" - } - - private[this] def makeScriptName(qualifiedClassName: String): String = { - val clazz = qualifiedClassName.split("\\.").last - - val lowerCased = clazz.drop(1).flatMap { - case c if c.isUpper => Seq('-', c.toLower) - case c => Seq(c) - } - - clazz(0).toLower +: lowerCased - } - /** * @param path that could be relative to app_home * @return path relative to app_home @@ -157,56 +123,16 @@ object BashStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator { "declare -r script_conf_file=\"%s\"" format configFile } - object MainScript { - - /** - * - * @param mainClass - Main class added to the java command - * @param config - Config data for this script - * @param targetDir - Target directory for this script - * @return File pointing to the created main script - */ - def apply(mainClass: String, config: BashScriptConfig, targetDir: File, mainClasses: Seq[String]): File = { - val template = resolveTemplate(config.bashScriptTemplateLocation) - val replacements = Seq( - "app_mainclass" -> mainClass, - "available_main_classes" -> usageMainClassReplacement(mainClasses) - ) ++ config.bashScriptReplacements - - val scriptContent = TemplateWriter.generateScript(template, replacements) - val script = targetDir / "scripts" / config.executableScriptName - IO.write(script, scriptContent) - // TODO - Better control over this! - script.setExecutable(true) - script - } - - private[this] def usageMainClassReplacement(mainClasses: Seq[String]): String = - if (mainClasses.nonEmpty) - mainClasses.mkString("Available main classes:\n\t", "\n\t", "") - else - "" - - private[this] def resolveTemplate(defaultTemplateLocation: File): URL = - if (defaultTemplateLocation.exists) defaultTemplateLocation.toURI.toURL - else getClass.getResource(defaultTemplateLocation.getName) - } - - object ForwarderScripts { - def apply(executableScriptName: String, discoveredMainClasses: Seq[String], targetDir: File): Seq[(File, String)] = { - val tmp = targetDir / "scripts" - val forwarderTemplate = getClass.getResource(bashForwarderTemplate) - discoveredMainClasses.map { qualifiedClassName => - val clazz = makeScriptName(qualifiedClassName) - val file = tmp / clazz - - val replacements = Seq("startScript" -> executableScriptName, "qualifiedClassName" -> qualifiedClassName) - val scriptContent = TemplateWriter.generateScript(forwarderTemplate, replacements) - - IO.write(file, scriptContent) - file.setExecutable(true) - file -> s"bin/$clazz" - } - } - } + private[this] def usageMainClassReplacement(mainClasses: Seq[String]): String = + if (mainClasses.nonEmpty) + mainClasses.mkString("Available main classes:\n\t", "\n\t", "") + else + "" + + override protected[this] def createReplacementsForMainScript( + mainClass: String, + mainClasses: Seq[String], + config: SpecializedScriptConfig + ): Seq[(String, String)] = + Seq("app_mainclass" -> mainClass, "available_main_classes" -> usageMainClassReplacement(mainClasses)) ++ config.replacements } diff --git a/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BatStartScriptPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BatStartScriptPlugin.scala index 91eca92c9..804f32e3c 100644 --- a/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BatStartScriptPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BatStartScriptPlugin.scala @@ -1,7 +1,6 @@ package com.typesafe.sbt.packager.archetypes.scripts import java.io.File -import java.net.URL import com.typesafe.sbt.SbtNativePackager.Universal import com.typesafe.sbt.packager.Keys._ @@ -17,7 +16,7 @@ import sbt._ * [[com.typesafe.sbt.packager.archetypes.JavaAppPackaging]]. * */ -object BatStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator { +object BatStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator with CommonStartScriptGenerator { /** * Name of the bat template if user wants to provide custom one @@ -27,30 +26,35 @@ object BatStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator { /** * Name of the bat forwarder template if user wants to provide custom one */ - val batForwarderTemplate = "bat-forwarder-template" - - /** - * Script destination in final package - */ - val scriptTargetFolder = "bin" + override protected[this] val forwarderTemplateName = "bat-forwarder-template" /** * Location for the application.ini file used by the bat script to load initialization parameters for jvm and app */ val appIniLocation = "%APP_HOME%\\conf\\application.ini" + override protected[this] val scriptSuffix = ".bat" + override protected[this] val eol: String = "\r\n" + override protected[this] val keySurround: String => String = TemplateWriter.batFriendlyKeySurround + override protected[this] val executableBitValue: Boolean = false + override val requires = JavaAppPackaging override val trigger = AllRequirements object autoImport extends BatStartScriptKeys import autoImport._ - private[this] case class BatScriptConfig(executableScriptName: String, - scriptClasspath: Seq[String], - configLocation: Option[String], - extraDefines: Seq[String], - replacements: Seq[(String, String)], - batScriptTemplateLocation: File) + protected[this] case class BatScriptConfig(override val executableScriptName: String, + override val scriptClasspath: Seq[String], + configLocation: Option[String], + extraDefines: Seq[String], + override val replacements: Seq[(String, String)], + override val templateLocation: File) + extends ScriptConfig { + override def withScriptName(scriptName: String): BatScriptConfig = copy(executableScriptName = scriptName) + } + + override protected[this] type SpecializedScriptConfig = BatScriptConfig override def projectSettings: Seq[Setting[_]] = Seq( batScriptTemplateLocation := (sourceDirectory.value / "templates" / batTemplate), @@ -67,12 +71,12 @@ object BatStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator { ), makeBatScripts := generateStartScripts( BatScriptConfig( - executableScriptName = s"${executableScriptName.value}.bat", + executableScriptName = executableScriptName.value, scriptClasspath = (scriptClasspath in batScriptReplacements).value, configLocation = batScriptConfigLocation.value, extraDefines = batScriptExtraDefines.value, replacements = batScriptReplacements.value, - batScriptTemplateLocation = batScriptTemplateLocation.value + templateLocation = batScriptTemplateLocation.value ), (mainClass in (Compile, batScriptReplacements)).value, (discoveredMainClasses in Compile).value, @@ -82,43 +86,6 @@ object BatStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator { mappings in Universal ++= makeBatScripts.value ) - private[this] def generateStartScripts(config: BatScriptConfig, - mainClass: Option[String], - discoveredMainClasses: Seq[String], - targetDir: File, - log: Logger): Seq[(File, String)] = - StartScriptMainClassConfig.from(mainClass, discoveredMainClasses) match { - case NoMain => - log.warn("You have no main class in your project. No start script will be generated.") - Seq.empty - case SingleMain(main) => - Seq(MainScript(main, config, targetDir) -> s"$scriptTargetFolder/${config.executableScriptName}") - case MultipleMains(mains) => - generateMainScripts(discoveredMainClasses, config, targetDir) - case ExplicitMainWithAdditional(main, additional) => - (MainScript(main, config, targetDir) -> s"$scriptTargetFolder/${config.executableScriptName}") +: - ForwarderScripts(config.executableScriptName, additional, targetDir) - } - - private[this] def generateMainScripts(discoveredMainClasses: Seq[String], - config: BatScriptConfig, - targetDir: File): Seq[(File, String)] = - discoveredMainClasses.map { qualifiedClassName => - val batConfig = config.copy(executableScriptName = makeScriptName(qualifiedClassName)) - MainScript(qualifiedClassName, batConfig, targetDir) -> s"$scriptTargetFolder/${batConfig.executableScriptName}" - } - - private[this] def makeScriptName(qualifiedClassName: String): String = { - val clazz = qualifiedClassName.split("\\.").last - - val lowerCased = clazz.drop(1).flatMap { - case c if c.isUpper => Seq('-', c.toLower) - case c => Seq(c) - } - - clazz(0).toLower + lowerCased + ".bat" - } - /** * @param path that could be relative to APP_HOME * @return path relative to APP_HOME @@ -173,47 +140,11 @@ object BatStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator { } } - object MainScript { - - /** - * - * @param mainClass - Main class added to the java command - * @param config - Config data for this script - * @param targetDir - Target directory for this script - * @return File pointing to the created main script - */ - def apply(mainClass: String, config: BatScriptConfig, targetDir: File): File = { - val template = resolveTemplate(config.batScriptTemplateLocation) - val replacements = config.replacements :+ Replacements.appDefines(mainClass, config, config.replacements) - val scriptContent = - TemplateWriter.generateScript(template, replacements, "\r\n", TemplateWriter.batFriendlyKeySurround) - val script = targetDir / "scripts" / config.executableScriptName - IO.write(script, scriptContent) - script - } - - private[this] def resolveTemplate(defaultTemplateLocation: File): URL = - if (defaultTemplateLocation.exists) defaultTemplateLocation.toURI.toURL - else getClass.getResource(defaultTemplateLocation.getName) - - } - - object ForwarderScripts { - def apply(executableScriptName: String, discoveredMainClasses: Seq[String], targetDir: File): Seq[(File, String)] = { - val tmp = targetDir / "scripts" - val forwarderTemplate = getClass.getResource(batForwarderTemplate) - discoveredMainClasses.map { qualifiedClassName => - val scriptName = makeScriptName(qualifiedClassName) - val file = tmp / scriptName - - val replacements = Seq("startScript" -> executableScriptName, "qualifiedClassName" -> qualifiedClassName) - val scriptContent = - TemplateWriter.generateScript(forwarderTemplate, replacements, "\r\n", TemplateWriter.batFriendlyKeySurround) - - IO.write(file, scriptContent) - file -> s"bin/$scriptName" - } - } + override protected[this] def createReplacementsForMainScript( + mainClass: String, + mainClasses: Seq[String], + config: SpecializedScriptConfig + ): Seq[(String, String)] = + config.replacements :+ Replacements.appDefines(mainClass, config, config.replacements) - } } diff --git a/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/CommonStartScriptGenerator.scala b/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/CommonStartScriptGenerator.scala new file mode 100644 index 000000000..c55a58cc8 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/CommonStartScriptGenerator.scala @@ -0,0 +1,145 @@ +package com.typesafe.sbt.packager.archetypes.scripts + +import java.io.File +import java.net.URL + +import com.typesafe.sbt.packager.archetypes.TemplateWriter +import sbt._ + +trait CommonStartScriptGenerator { + + /** + * Script destination in final package + */ + protected[this] val scriptTargetFolder: String = "bin" + + /** + * Suffix to append to the generated script name (such as ".bat") + */ + protected[this] val scriptSuffix: String + + /** + * Name of the forwarder template if user wants to provide custom one + */ + protected[this] val forwarderTemplateName: String + + /** + * Line separator for generated scripts + */ + protected[this] val eol: String + + /** + * keySurround for TemplateWriter.generateScript() + */ + protected[this] val keySurround: String => String + + /** + * Set executable bit of the generated scripts to this value + * @todo Does it work when building archives on hosts that do not support such permission? + */ + protected[this] val executableBitValue: Boolean + + protected[this] def createReplacementsForMainScript(mainClass: String, + mainClasses: Seq[String], + config: SpecializedScriptConfig): Seq[(String, String)] + + protected[this] trait ScriptConfig { + val executableScriptName: String + val scriptClasspath: Seq[String] + val replacements: Seq[(String, String)] + val templateLocation: File + + def withScriptName(scriptName: String): SpecializedScriptConfig + } + + /** + * The type of specialized ScriptConfig. + * This enables callback methods of the concrete plugin implementations + * to use fields of config that only exist in their ScriptConfig specialization. + */ + protected[this] type SpecializedScriptConfig <: ScriptConfig + + protected[this] def generateStartScripts(config: SpecializedScriptConfig, + mainClass: Option[String], + discoveredMainClasses: Seq[String], + targetDir: File, + log: sbt.Logger): Seq[(File, String)] = + StartScriptMainClassConfig.from(mainClass, discoveredMainClasses) match { + case NoMain => + log.warn("You have no main class in your project. No start script will be generated.") + Seq.empty + case SingleMain(main) => + Seq(createMainScript(main, config, targetDir, Seq(main))) + case MultipleMains(mains) => + generateMainScripts(mains, config, targetDir, log) + case ExplicitMainWithAdditional(main, additional) => + createMainScript(main, config, targetDir, discoveredMainClasses) +: + createForwarderScripts(config.executableScriptName, additional, targetDir, config, log) + } + + private[this] def generateMainScripts(discoveredMainClasses: Seq[String], + config: SpecializedScriptConfig, + targetDir: File, + log: sbt.Logger): Seq[(File, String)] = { + val classAndScriptNames = ScriptUtils.createScriptNames(discoveredMainClasses) + ScriptUtils.warnOnScriptNameCollision(classAndScriptNames, log) + classAndScriptNames.map { + case (qualifiedClassName, scriptName) => + val newConfig = config.withScriptName(scriptName) + createMainScript(qualifiedClassName, newConfig, targetDir, discoveredMainClasses) + } + } + + private[this] def mainScriptName(config: ScriptConfig): String = + config.executableScriptName + scriptSuffix + + /** + * + * @param mainClass - Main class added to the java command + * @param config - Config data for this script + * @param targetDir - Target directory for this script + * @return File pointing to the created main script + */ + private[this] def createMainScript(mainClass: String, + config: SpecializedScriptConfig, + targetDir: File, + mainClasses: Seq[String]): (File, String) = { + val template = resolveTemplate(config.templateLocation) + val replacements = createReplacementsForMainScript(mainClass, mainClasses, config) + val scriptContent = TemplateWriter.generateScript(template, replacements, eol, keySurround) + val scriptNameWithSuffix = mainScriptName(config) + val script = targetDir / scriptTargetFolder / scriptNameWithSuffix + + IO.write(script, scriptContent) + // TODO - Better control over this! + script.setExecutable(executableBitValue) + script -> s"$scriptTargetFolder/$scriptNameWithSuffix" + } + + private[this] def resolveTemplate(defaultTemplateLocation: File): URL = + if (defaultTemplateLocation.exists) defaultTemplateLocation.toURI.toURL + else getClass.getResource(defaultTemplateLocation.getName) + + private[this] def createForwarderScripts(executableScriptName: String, + discoveredMainClasses: Seq[String], + targetDir: File, + config: ScriptConfig, + log: sbt.Logger): Seq[(File, String)] = { + val tmp = targetDir / scriptTargetFolder + val forwarderTemplate = getClass.getResource(forwarderTemplateName) + val classAndScriptNames = ScriptUtils.createScriptNames(discoveredMainClasses) + ScriptUtils.warnOnScriptNameCollision(classAndScriptNames :+ ("
" -> mainScriptName(config)), log) + classAndScriptNames.map { + case (qualifiedClassName, scriptNameWithoutSuffix) => + val scriptName = scriptNameWithoutSuffix + scriptSuffix + val file = tmp / scriptName + + val replacements = Seq("startScript" -> executableScriptName, "qualifiedClassName" -> qualifiedClassName) + val scriptContent = TemplateWriter.generateScript(forwarderTemplate, replacements, eol, keySurround) + + IO.write(file, scriptContent) + file.setExecutable(executableBitValue) + file -> s"$scriptTargetFolder/$scriptName" + } + } +} diff --git a/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/ScriptUtils.scala b/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/ScriptUtils.scala new file mode 100644 index 000000000..14ea90f99 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/ScriptUtils.scala @@ -0,0 +1,105 @@ +package com.typesafe.sbt.packager.archetypes.scripts + +object ScriptUtils { + + private[this] case class MainClass(fullyQualifiedClassName: String) { + private val lowerCased = toLowerCase(fullyQualifiedClassName) + val simpleName: String = lowerCased.split("\\.").last + + def asSimpleTuple: (String, String) = (fullyQualifiedClassName, simpleName) + def asQualifiedTuple: (String, String) = (fullyQualifiedClassName, lowerCased.replace('.', '_')) + } + + /** + * Generates launcher script names for the specified main class names. + * @param discoveredMainClasses discovered qualified main class names + * @return sequence of tuples: (passed in class name) -> (generated script name) + * @note may introduce name collisions in some corner cases + */ + def createScriptNames(discoveredMainClasses: Seq[String]): Seq[(String, String)] = { + val mainClasses = discoveredMainClasses.map { fullyQualifiedClassName => + MainClass(fullyQualifiedClassName) + } + val (duplicates, uniques) = mainClasses + .groupBy(_.simpleName) + .partition { + case (_, classes) => classes.length > 1 + } + + val resultsForUniques = uniques.toSeq.map { + case (_, seqOfOneClass) => seqOfOneClass.head.asSimpleTuple + } + val resultsForDuplicates = duplicates.toSeq.flatMap { + case (_, classes) => classes.map(_.asQualifiedTuple) + } + resultsForUniques ++ resultsForDuplicates + } + + def describeDuplicates(classesAndScripts: Seq[(String, String)]): Seq[String] = + classesAndScripts + .groupBy { + case (_, scriptName) => scriptName + } + .toSeq + .filter { + case (_, classesWithTheSameScriptName) => classesWithTheSameScriptName.length > 1 + } + .map { + case (scriptName, duplicates) => + val temp = duplicates + .map { + case (qualifiedClassName, _) => qualifiedClassName + } + .sorted + .mkString(", ") + s"$scriptName ($temp)" + } + + def warnOnScriptNameCollision(classesAndScripts: Seq[(String, String)], log: sbt.Logger): Unit = { + val duplicates = describeDuplicates(classesAndScripts) + if (duplicates.nonEmpty) { + log.warn( + s"The resulting zip seems to contain duplicated script names for these classes: ${duplicates.mkString(", ")}" + ) + } + } + + /** + * Converts class name to lower case, applying some heuristics + * to guess the word splitting. + * @param qualifiedClassName a class name + * @return lower cased name with '-' between words. Dots ('.') are left as is. + * @note This function can still introduce name collisions sometimes: for example, + * both Test1Class and Test1class (note the capitalization) will end up test-1-class. + */ + def toLowerCase(qualifiedClassName: String): String = { + // suppose list is not very huge, so no need in tail recursion + def split(chars: List[Char]): List[Char] = chars match { + case c1 :: c2 :: cs if c1.isLower && c2.isUpper => + // aClass -> a-Class + // anUITest -> an-UITest + // ^ + c1 :: '-' :: split(c2 :: cs) + case c1 :: c2 :: c3 :: cs if c1.isUpper && c2.isUpper && c3.isLower => + // UITest -> UI-Test + // ^ + c1 :: '-' :: split(c2 :: c3 :: cs) + case c1 :: c2 :: cs if c1.isLetter && c2.isDigit => + // Test1 -> Test-1 + // ^ + c1 :: '-' :: split(c2 :: cs) + case c1 :: c2 :: cs if c1.isDigit && c2.isLetter => + // Test1Class -> Test-1-Class + // ^ ^ + // _not_ pkg1.Test + // ^ + c1 :: '-' :: split(c2 :: cs) + case c :: cs => + c :: split(cs) + case Nil => Nil + } + val sb = new StringBuilder + sb ++= split(qualifiedClassName.toList).map(_.toLower) + sb.result() + } +} diff --git a/src/sphinx/archetypes/java_app/index.rst b/src/sphinx/archetypes/java_app/index.rst index 5fe110166..a0c9a5e4e 100644 --- a/src/sphinx/archetypes/java_app/index.rst +++ b/src/sphinx/archetypes/java_app/index.rst @@ -223,6 +223,48 @@ For two main classes ``com.example.FooMain`` and ``com.example.BarMain`` ``sbt s Now you can package your application as usual, but with multiple start scripts. +A note on script names +---------------------- + +When this plugin generates script names from main class names, it tries to generate readable and unique names: + +1. An heuristic is used to split the fully qualified class names into words: + + .. code-block:: none + + pkg1.TestClass + pkg2.AnUIMainClass + pkg2.SomeXMLLoader + pkg3.TestClass + + becomes + + .. code-block:: none + + pkg-1.test-class + pkg-2.an-ui-main-class + pkg-2.some-xml-loader + pkg-3.test-class + +2. Resulted lower-cased names are grouped by the simple class name. + + - Names from single-element groups are reduced to their lower-cased simple names. + + - Names that would otherwise collide by their simple names are used as is (that is, full names) + with dots replaced by underscores + + So the final names will be: + + .. code-block:: none + + pkg-1_test-class + an-ui-main-class + some-xml-loader + pkg-3_test-class + +Please note that in some corner cases this may result in multiple scripts with the same name +in the resulting archive, but it is not expected to happen in normal circumstances. + Customize ========= diff --git a/src/test/scala/com/typesafe/sbt/packager/archetypes/scripts/ScriptUtilsTest.scala b/src/test/scala/com/typesafe/sbt/packager/archetypes/scripts/ScriptUtilsTest.scala new file mode 100644 index 000000000..275d08165 --- /dev/null +++ b/src/test/scala/com/typesafe/sbt/packager/archetypes/scripts/ScriptUtilsTest.scala @@ -0,0 +1,73 @@ +package com.typesafe.sbt.packager.archetypes.scripts + +import org.scalatest.{FlatSpec, Matchers} + +class ScriptUtilsTest extends FlatSpec with Matchers { + "toLowerCase()" should "convert regular names" in { + ScriptUtils.toLowerCase("package.TestClass") should be("package.test-class") + } + + it should "convert regular names with single-lettered words" in { + ScriptUtils.toLowerCase("package.ATestClass") should be("package.a-test-class") + ScriptUtils.toLowerCase("package.FindAClass") should be("package.find-a-class") + } + + it should "convert names with abbreviations" in { + ScriptUtils.toLowerCase("package.XMLParser") should be("package.xml-parser") + ScriptUtils.toLowerCase("package.AnXMLParser") should be("package.an-xml-parser") + } + + it should "convert names with numbers" in { + ScriptUtils.toLowerCase("package.Test1") should be("package.test-1") + ScriptUtils.toLowerCase("package.Test11") should be("package.test-11") + ScriptUtils.toLowerCase("package.Test1Class") should be("package.test-1-class") + ScriptUtils.toLowerCase("package.Test11Class") should be("package.test-11-class") + } + + private[this] def testMapping(testCase: (String, String)*): Unit = + ScriptUtils.createScriptNames(testCase.map(_._1)) should contain theSameElementsAs testCase + + "createScriptNames()" should "generate short names when no conflicts" in { + testMapping( + "pkg1.TestClass" -> "test-class", + "pkg1.AnotherTestClass" -> "another-test-class", + "pkg2.ThirdTestClass" -> "third-test-class" + ) + } + + it should "generate long names only when necessary" in { + testMapping( + "pkg1.TestClass" -> "pkg-1_test-class", + "pkg1.ui.TestClass" -> "pkg-1_ui_test-class", + "pkg1.AnotherTestClass" -> "another-test-class", + "pkg2.TestClass" -> "pkg-2_test-class" + ) + } + + it should "handle single main class" in { + testMapping("pkg1.Test" -> "test") + } + + it should "be consistent with the docs" in { + // see src/sphinx/archetypes/java_app/index.rst + testMapping( + "pkg1.TestClass" -> "pkg-1_test-class", + "pkg2.AnUIMainClass" -> "an-ui-main-class", + "pkg2.SomeXMLLoader" -> "some-xml-loader", + "pkg3.TestClass" -> "pkg-3_test-class" + ) + } + + "duplicated script name detector" should "work" in { + ScriptUtils.describeDuplicates( + Seq( + "Test11" -> "dup1", + "Test12" -> "dup1", + "Test21" -> "dup2", + "Test22" -> "dup2", + "Test23" -> "dup2", + "Test3" -> "nodup" + ) + ) should contain theSameElementsAs Seq("dup1 (Test11, Test12)", "dup2 (Test21, Test22, Test23)") + } +}