
ends.giter8.giter8-lib_2.12.0.17.0.source-code.g8.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of giter8-lib_2.12 Show documentation
Show all versions of giter8-lib_2.12 Show documentation
shared library for app and plugin
The newest version!
/*
* Original implementation (C) 2010-2015 Nathan Hamblen and contributors
* Adapted and extended in 2016 by foundweekends project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package giter8
import java.io.{File, InputStream}
import java.util.regex.Matcher
import java.util.{Locale, Properties}
import org.apache.commons.io.FileUtils
import org.codehaus.plexus.archiver.util.ArchiveEntryUtils
import org.codehaus.plexus.components.io.attributes.PlexusIoResourceAttributeUtils
import org.stringtemplate.v4.compiler.STException
import org.stringtemplate.v4.misc.STMessage
import scala.collection.mutable
import scala.util.Try
import scala.util.control.Exception.{allCatch, catching}
import scala.util.control.NonFatal
import java.nio.charset.StandardCharsets
object G8 {
import org.stringtemplate.v4.{AttributeRenderer, ST, STGroup, STErrorListener}
/** Properties in the order they were created/defined */
type OrderedProperties = List[(String, String)]
/** Properties which have not been resolved. I.e., ValueF() has not been evaluated */
type UnresolvedProperties = List[(String, ValueF)]
object UnresolvedProperties {
val empty = List.empty[(String, ValueF)]
}
/** G8 template properties which have been fully resolved, i.e. defaults replaced by user input, ready for insertion
* into template
*/
type ResolvedProperties = Map[String, String]
object ResolvedProperties {
val empty = Map.empty[String, String]
}
/** A function which will return the resolved value of a property given the properties resolved thus far. This is a
* bit more general than was needed for resolving "dynamic defaults". I did it this way so it's possible to have
* other ValueF definitions which perform arbitrary logic given previously defined properties.
*/
type ValueF = ResolvedProperties => String
// Called from JgitHelper
def fromDirectory(
templateDirectory: File,
workingDirectory: File,
arguments: Seq[String],
forceOverwrite: Boolean,
outputDirectory: Option[File]
): Either[String, String] =
applyT((file: File) =>
fetchInfo(
file: File,
defaultTemplatePaths,
List(path("src") / "main" / "scaffolds", path("project") / "src" / "main" / "scaffolds")
)
)(templateDirectory, workingDirectory, arguments, forceOverwrite, outputDirectory)
// Called from ScaffoldPlugin
def fromDirectoryRaw(
templateDirectory: File,
outputDirectory: File,
arguments: Seq[String],
forceOverwrite: Boolean
): Either[String, String] =
applyT((file: File) => fetchInfo(file, List(Path(Nil)), Nil))(
templateDirectory,
outputDirectory,
arguments,
forceOverwrite,
None
)
val defaultTemplatePaths: List[Path] = List(path("src") / "main" / "g8", Path(Nil))
def apply(fromMapping: Seq[(File, String)], toPath: File, parameters: Map[String, String]): Seq[File] =
fromMapping filter { !_._1.isDirectory } flatMap { case (in, relative) =>
expandPath(relative, toPath, parameters) match {
case None => Seq()
case Some(out) => apply(in, out, toPath, parameters)
}
}
def apply(in: File, out: File, base: File, parameters: Map[String, String]) = {
try {
if (verbatim(out, parameters, base)) FileUtils.copyFile(in, out)
else {
write(in, out, parameters /*, false*/ )
}
} catch {
case e: Exception =>
println(s"Falling back to file copy for ${in}: ${e.getMessage}")
FileUtils.copyFile(in, out)
}
allCatch opt {
if (in.canExecute) out.setExecutable(true)
}
Seq(out)
}
def applyTemplate(default: String, resolved: ResolvedProperties): String = {
val group = new STGroup('$', '$')
group.setListener(new STErrorHandler)
group.registerRenderer(
classOf[AugmentedString],
new AttributeRenderer[AugmentedString] {
override def toString(o: AugmentedString, format: String, locale: Locale) =
renderer.toString(o, format, locale)
}
)
val attributes = resolved mapValues AugmentedString.apply
try {
val st = new ST(group, default)
attributes.foreach { case (k, v) =>
st.add(k, v)
}
st.render()
} catch {
case NonFatal(e: Exception) =>
// This is a workaround for https://github.com/antlr/stringtemplate4/issues/253.
throw new STException(
"An unexpected error occurred while processing the template. " +
"Check that all literal '$' are properly escaped with '\\$'",
e
)
}
}
class STErrorHandler extends STErrorListener {
def compileTimeError(msg: STMessage): Unit = {
throw new STException(msg.toString, null)
}
def runTimeError(msg: STMessage): Unit = {
throw new STException(msg.toString, null)
}
def IOError(msg: STMessage): Unit = {
throw new STException(msg.toString, null)
}
def internalError(msg: STMessage): Unit = {
throw new STException(msg.toString, null)
}
}
/** The ValueF implementation for handling default properties. It performs formatted substitution on any properties
* found.
*/
case class DefaultValueF(default: String) extends ValueF {
override def apply(resolved: ResolvedProperties): String = applyTemplate(default, resolved)
}
private val renderer = new AugmentedStringRenderer
def write(in: File, out: File, parameters: Map[String, String] /*, append: Boolean*/ ): Unit =
try {
Option(PlexusIoResourceAttributeUtils.getFileAttributes(in)) match {
case Some(attr) =>
val mode = attr.getOctalMode
write(out, FileUtils.readFileToString(in, "UTF-8"), parameters /*, append*/ )
Try(ArchiveEntryUtils.chmod(out, mode))
case None =>
// PlexusIoResourceAttributes is not available for some OS'es such as windows
write(out, FileUtils.readFileToString(in, "UTF-8"), parameters /*, append*/ )
}
} catch {
case e: STException =>
// add the current file to the exception for debugging purposes
throw new STException(s"File: $in, ${e.getMessage}", null)
case t: Throwable =>
throw t
}
def write(out: File, template: String, parameters: Map[String, String] /*, append: Boolean = false*/ ): Unit =
FileUtils.writeStringToFile(out, applyTemplate(template, parameters), StandardCharsets.UTF_8 /*, append*/ )
def verbatim(file: File, parameters: Map[String, String], base: File = new File(".")): Boolean = {
val rules = JGitIgnore(
parameters
.get("verbatim")
.toSeq
.flatMap(_.split(' ').toSeq)*
)
rules.isIgnored(file, base)
}
def expandPath(relative: String, toPath: File, parameters: Map[String, String]): Option[File] =
try {
val fileSeparator = File.separator
val fileParams = Map(parameters.toSeq map {
case (k, v) if k == "package" =>
(k, v.replaceAll("""\.""", Matcher.quoteReplacement(fileSeparator)))
case x => x
}: _*)
val ignored = relative
.split("/")
.map(part => applyTemplate(formatize(part), fileParams))
.exists(_.isEmpty)
if (ignored) None
else Some(new File(toPath, applyTemplate(formatize(relative), fileParams)))
} catch {
case e: STException =>
// add the current relative path to the exception for debugging purposes
throw new STException(s"relative: $relative, toPath: $toPath, ${e.getMessage}", null)
case t: Throwable =>
throw t
}
private def formatize(s: String) = s.replaceAll("""\$(\w+)__([\w,]+)\$""", """\$$1;format="$2"\$""")
def decapitalize(s: String) = if (s.isEmpty) s else s(0).toLower + s.substring(1)
def startCase(s: String) = s.toLowerCase.split(" ").map(_.capitalize).mkString(" ")
def wordOnly(s: String) = s.replaceAll("""\W""", "")
def space(s: String) = s.replaceAll("""[\W_]+""", " ")
def upperCamel(s: String) = wordOnly(startCase(s))
def lowerCamel(s: String) = decapitalize(upperCamel(s))
def hyphenate(s: String) = s.replaceAll("""\s+""", "-")
def normalize(s: String) = hyphenate(s.toLowerCase)
def snakeCase(s: String) = s.replaceAll("""[\s\.\-]+""", "_")
def packageNaming(s: String) = s.replaceAll("""\s+""", ".")
def packageDir(s: String) = s.replace(".", System.getProperty("file.separator"))
def dotReverse(s: String) = s.split('.').reverse.mkString(".")
def addRandomId(s: String) = s + "-" + new java.math.BigInteger(256, new java.security.SecureRandom).toString(32)
def scalaIdentifier(s: String) = if (s.contains("-")) "`" + s + "`" else s
val Param = """(?s)^--(\S+)=(.+)$""".r
implicit class RichFile(file: File) {
def /(child: String): File = new File(file, child)
def /(path: Path): File = path.paths.foldLeft(file) { _ / _ }
}
def path(path: String): Path = Path(List(path))
private[giter8] def getFiles(filter: File => Boolean)(f: File): Stream[File] =
if (f.isDirectory) f.listFiles().toStream.withFilter(filter).flatMap(getFiles(filter))
else if (filter(f)) Stream(f)
else Stream()
private[giter8] def getFiles(f: File): Stream[File] =
getFiles(_ => true)(f)
/** Select the root template directory from the given relative paths. */
def templateRoot(baseDirectory: File, templatePaths: List[Path]): File =
// Go through the list of dirs and pick the first one.
(templatePaths map {
case Path(Nil) => baseDirectory
case p => baseDirectory / p
}).find(_.exists).getOrElse(baseDirectory)
/** Extract template files under the first matching relative templatePaths under the baseDirectory. */
def templateFiles(root: File, baseDirectory: File): Stream[File] = {
val gitignoreFile: File = root / ".gitignore"
val g8IgnoreFile: File = baseDirectory / ".g8ignore"
val g8Ignores: JGitIgnore = {
lazy val defaults: JGitIgnore = JGitIgnore(
".git",
"giter8.sbt",
"project/giter8.sbt",
"g8.sbt",
"project/g8.sbt",
"activator.properties",
"project/activator.properties",
"template.properties",
"project/template.properties",
"test",
"!**/sbt-test/**/test",
"!test/",
"project/test",
"!project/test/",
"test.script",
"project/test.script",
"giter8.test",
"project/giter8.test",
"g8.test",
"project/g8.test",
"target/",
"**/target/"
)
if (g8IgnoreFile.exists) {
JGitIgnore(g8IgnoreFile)
} else {
defaults
}
}
val gitIgnores: Option[JGitIgnore] =
Some(gitignoreFile)
.withFilter(_.exists)
.map(JGitIgnore.apply)
// The list of ignore rules. Rules in `.g8ignore` will
// override those in `.gitignore`
val ignores = gitIgnores.map(_ ++ g8Ignores).getOrElse(g8Ignores)
def fileFilter(file: File): Boolean = {
val isIgnored = ignores.isIgnored(file, root)
!(isIgnored || file == g8IgnoreFile)
}
getFiles(fileFilter _)(root)
}
private[giter8] def applyT(
fetch: File => Either[String, (UnresolvedProperties, Stream[File], File, Option[File])]
// isScaffolding: Boolean = false
)(
tmpl: File,
workingDirectory: File,
arguments: Seq[String] = Nil,
forceOverwrite: Boolean = false,
outputDirectory: Option[File]
): Either[String, String] =
try {
fetch(tmpl).flatMap { case (defaults, templates, templatesRoot, scaffoldsRoot) =>
val parameters = consoleParams(defaults, arguments).getOrElse {
interact(defaults)
}
val base: File = outputDirectory
.orElse {
parameters.get("name").map(workingDirectory / G8.normalize(_))
}
.getOrElse(workingDirectory)
val r = writeTemplates(
templatesRoot,
templates,
parameters,
base, // isScaffolding,
forceOverwrite
)
for {
_ <- r
root <- scaffoldsRoot
} copyScaffolds(root, base)
r
}
} catch {
case e: STException =>
Left(s"Exiting due to error in the template: ${tmpl}\n${e.getMessage}")
case NullInputException =>
Left(s"Interrupting...")
case t: Throwable =>
t.printStackTrace()
Left("Unknown exception: " + t.getMessage)
}
private def getVisibleFiles = getFiles(!_.isHidden) _
/** transforms any maven() property operations to the latest version number reported by that service.
*/
def transformProps(props: G8.OrderedProperties): Either[String, G8.OrderedProperties] =
Maven.lookup(props)
/** Extract params, template files, and scaffolding folder based on the conventionnal project structure
*/
private[giter8] def fetchInfo(
baseDirectory: File,
templatePaths: List[Path],
scaffoldPaths: List[Path]
): Either[String, (UnresolvedProperties, Stream[File], File, Option[File])] = {
import java.io.FileInputStream
val templatesRoot = templateRoot(baseDirectory, templatePaths)
val fs = templateFiles(templatesRoot, baseDirectory)
val metaDir = baseDirectory / "project"
val propertiesLoc0 = templatesRoot / "default.properties"
val propertiesLoc1 = metaDir / "default.properties"
val propertiesLocs = Set(propertiesLoc0, propertiesLoc1)
val scaffoldsRoot =
(scaffoldPaths map {
case Path(Nil) => baseDirectory
case p => baseDirectory / p
}).find(_.exists)
val (propertiesFiles, tmpls) = fs.partition {
propertiesLocs(_)
}
val parametersEither = propertiesFiles.headOption
.map { f =>
val props = readProps(new FileInputStream(f))
val transformed = transformProps(props)
transformed.map { _.map { case (k, v) => (k, DefaultValueF(v)) } }
}
.getOrElse(Right(UnresolvedProperties.empty))
val g8templates = tmpls.filter(!_.isDirectory)
for (parameters <- parametersEither) yield (parameters, g8templates, templatesRoot, scaffoldsRoot)
}
def consoleParams(defaults: UnresolvedProperties, arguments: Seq[String]) = {
arguments.headOption.map { _ =>
val specified = arguments.foldLeft(ResolvedProperties.empty) {
case (map, Param(key, value)) if defaults.map(_._1).contains(key) =>
map + (key -> value)
case (map, Param(key, _)) =>
println("Ignoring unrecognized parameter: " + key)
map
}
// Add anything from defaults that wasn't picked up as an argument from the console.
defaults.foldLeft(specified) { case (resolved, (k, f)) =>
if (!resolved.contains(k)) resolved + (k -> f(resolved))
else resolved
}
}
}
def interact(params: UnresolvedProperties): ResolvedProperties = {
val description = params
.collectFirst {
case (param, getDescription) if param == "description" =>
getDescription(ResolvedProperties.empty)
}
description.foreach { descr =>
descr.split("\n").foreach { line =>
@scala.annotation.tailrec
def liner(cursor: Int, rem: Iterable[String]): Unit = {
if (rem.nonEmpty) {
val next = cursor + 1 + rem.head.length
if (next > 70 && cursor > 0) {
println()
liner(0, rem)
} else {
print(rem.head + " ")
liner(next, rem.tail)
}
}
}
liner(0, line.split(" "))
println()
}
println()
}
val fixed = Set("verbatim", "description")
params
.foldLeft(ResolvedProperties.empty) { case (resolved, (k, f)) =>
resolved + (
if (fixed.contains(k)) k -> f(resolved)
else {
val default = f(resolved)
val message = Truthy.getMessage(default)
print(s"$k [$message]: ")
Console.flush() // Gotta flush for Windows console!
val in = Option(scala.io.StdIn.readLine())
.map(_.trim)
.getOrElse(throw NullInputException)
(k, if (in.isEmpty) default else in)
}
)
}
.toMap
}
case object NullInputException extends Throwable
private def relativize(in: File, from: File): String = from.toURI().relativize(in.toURI).getPath
def writeTemplates(
tmpl: File,
templates: Iterable[File],
parameters: Map[String, String],
base: File,
// isScaffolding: Boolean,
forceOverwrite: Boolean
): Either[String, String] = {
import java.nio.charset.MalformedInputException
templates
.map { in =>
val name = relativize(in, tmpl)
val optionOut = G8.expandPath(name, base, parameters)
(in, optionOut)
}
.foreach { case (in, optionOut) =>
optionOut match {
case None => println(s"Skipping ignored file: ${in}")
case Some(out) => {
if (out.exists && !forceOverwrite) {
println(s"Skipping existing file: ${out}")
} else {
out.getParentFile.mkdirs()
if (G8.verbatim(in, parameters, tmpl))
FileUtils.copyFile(in, out)
else {
catching(classOf[MalformedInputException])
.opt {
G8.write(in, out, parameters /*, append = existingScaffoldingAction.getOrElse(false)*/ )
}
.getOrElse {
// if (existingScaffoldingAction.getOrElse(false)) {
// val existing = FileUtils.readFileToString(in, UTF_8)
// FileUtils.write(out, existing, UTF_8, true)
// } else {
FileUtils.copyFile(in, out)
// }
}
}
if (in.canExecute) {
out.setExecutable(true)
}
}
}
}
}
Right(s"Template applied in ${base}")
}
def copyScaffolds(sf: File, output: File): Unit = {
val scaffolds = if (sf.exists) Some(getFiles(_ => true)(sf)) else None
for (
fs <- scaffolds;
f <- fs if !f.isDirectory
) {
// Copy scaffolding recipes
val realProjectRoot = getVisibleFiles(output)
.filter(_.isDirectory)
.filter(_.getName == "project")
.map(_.getParentFile)
.headOption
.getOrElse(output)
val hidden = new File(realProjectRoot, ".g8")
val name = relativize(f, sf)
val out = new File(hidden, name)
FileUtils.copyFile(f, out)
}
}
def readProps(stm: InputStream): G8.OrderedProperties = {
// Overrides java.util.Properties to return properties back in the order they were specified
val parameters = mutable.ListBuffer.empty[(String, String)]
val properties = new Properties {
override def put(key: Object, value: Object) = {
val k = key.toString
val v = value.toString
parameters += k -> v
super.put(k, v)
}
}
try {
properties.load(stm)
parameters.toList
} finally {
stm.close()
}
}
}
case class Path(paths: List[String]) {
def /(child: String): Path = copy(paths = paths ::: List(child))
}
class AugmentedStringRenderer {
import G8._
def toString(value: String): String = value
def toString(value: AugmentedString, formatName: String, locale: java.util.Locale): String = {
val str = value.toString
if (formatName == null) str
else {
val formats = formatName.split(",").map(_.trim)
formats.foldLeft(str)(format)
}
}
def format(value: String, formatName: String): String = formatName match {
case "upper" | "uppercase" => value.toUpperCase
case "lower" | "lowercase" => value.toLowerCase
case "cap" | "capitalize" => value.capitalize
case "decap" | "decapitalize" => decapitalize(value)
case "start" | "start-case" => startCase(value)
case "word" | "word-only" => wordOnly(value)
case "space" | "word-space" => space(value)
case "Camel" | "upper-camel" => upperCamel(value)
case "camel" | "lower-camel" => lowerCamel(value)
case "hyphen" | "hyphenate" => hyphenate(value)
case "norm" | "normalize" => normalize(value)
case "snake" | "snake-case" => snakeCase(value)
case "package" | "package-naming" => packageNaming(value)
case "packaged" | "package-dir" => packageDir(value)
case "dotReverse" | "dot-reverse" => dotReverse(value)
case "random" | "generate-random" => addRandomId(value)
case _ => value
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy