diff --git a/.gitignore b/.gitignore index f737b066..0b4991a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,20 @@ *.class -target/ logs/ transformed-apps/ +results/ +metrics/ test-output/ gen-metrics/ # Package Files # *.jar +# Gradle specific # +.gradle/ +build/ +out/ +!gradle/wrapper/gradle-wrapper.jar + # IDE Files # .classpath .project diff --git a/.travis.yml b/.travis.yml index 3fcc34f9..06af3381 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,26 +5,21 @@ cache: directories: - .autoconf - $HOME/.m2 + - $HOME/.gradle jdk: - - oraclejdk8 + - openjdk8 branches: only: - - develop + - butterfly_3 install: - - mvn install + - ./gradlew build jacocoRootReport jacocoTestCoverageVerification --scan after_script: - # Installing Codacy code coverage reporter upload tool - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then wget https://github.com/codacy/codacy-coverage-reporter/releases/download/1.0.13/codacy-coverage-reporter-1.0.13-assembly.jar -O ccr.jar; fi' - # Uploading Cobertura report to Codacy - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then java -cp ccr.jar com.codacy.CodacyCoverageReporter -l Java -r ./butterfly-cli/target/site/cobertura/coverage.xml --projectToken $CODACY_PROJECT_TOKEN; fi' - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then java -cp ccr.jar com.codacy.CodacyCoverageReporter -l Java -r ./butterfly-cli-package/target/site/cobertura/coverage.xml --projectToken $CODACY_PROJECT_TOKEN; fi' - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then java -cp ccr.jar com.codacy.CodacyCoverageReporter -l Java -r ./butterfly-core/target/site/cobertura/coverage.xml --projectToken $CODACY_PROJECT_TOKEN; fi' - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then java -cp ccr.jar com.codacy.CodacyCoverageReporter -l Java -r ./butterfly-extensions-api/target/site/cobertura/coverage.xml --projectToken $CODACY_PROJECT_TOKEN; fi' - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then java -cp ccr.jar com.codacy.CodacyCoverageReporter -l Java -r ./butterfly-facade/target/site/cobertura/coverage.xml --projectToken $CODACY_PROJECT_TOKEN; fi' - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then java -cp ccr.jar com.codacy.CodacyCoverageReporter -l Java -r ./butterfly-metrics-couchdb/target/site/cobertura/coverage.xml --projectToken $CODACY_PROJECT_TOKEN; fi' - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then java -cp ccr.jar com.codacy.CodacyCoverageReporter -l Java -r ./butterfly-metrics-file/target/site/cobertura/coverage.xml --projectToken $CODACY_PROJECT_TOKEN; fi' - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then java -cp ccr.jar com.codacy.CodacyCoverageReporter -l Java -r ./butterfly-utilities/target/site/cobertura/coverage.xml --projectToken $CODACY_PROJECT_TOKEN; fi' + # Installing Codacy code coverage reporter upload tool and uploading Cobertura report to Codacy + - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then + wget https://github.com/codacy/codacy-coverage-reporter/releases/download/1.0.13/codacy-coverage-reporter-1.0.13-assembly.jar -O ccr.jar; + java -cp ccr.jar com.codacy.CodacyCoverageReporter -l Java -r ./build/reports/jacoco/coverage.xml --projectToken $CODACY_PROJECT_TOKEN; + fi \ No newline at end of file diff --git a/.travis_release.yml b/.travis_release.yml index 39f16d21..ccf1b940 100644 --- a/.travis_release.yml +++ b/.travis_release.yml @@ -5,6 +5,7 @@ branches: before_install: - openssl aes-256-cbc -K $encrypted_dd05710e44e2_key -iv $encrypted_dd05710e44e2_iv -in secring.gpg.enc -out secring.gpg -d - gpg --import secring.gpg + - export SECRING_FILE="$PWD/secring.gpg" install: - - mvn -s settings.xml deploy -Possrh \ No newline at end of file + - ./gradlew uploadArchives --scan \ No newline at end of file diff --git a/.travis_snapshot.yml b/.travis_snapshot.yml index 182ad75a..77fad7be 100644 --- a/.travis_snapshot.yml +++ b/.travis_snapshot.yml @@ -1,2 +1,2 @@ install: - - mvn -s settings.xml deploy -Possrh \ No newline at end of file + - ./gradlew uploadArchives --scan \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..fecaefa2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,277 @@ + +plugins { + id 'com.gradle.build-scan' version '2.1' +} + +description = "Butterfly - Application transformation tool" + +allprojects { + apply plugin: 'maven' + apply plugin: 'signing' + apply plugin: "jacoco" + + group = 'com.paypal.butterfly' + version = '3.0.0' + + repositories { + mavenLocal() + mavenCentral() + } + + buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' + } +} + +Set testModules = [ + "integration-tests", + "tests" +] + +subprojects { + + apply plugin: 'java' + + def javaVersion = JavaVersion.VERSION_1_8 + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + + test { + testLogging { + events "failed" +// events "started", "passed", "skipped", "failed", "standardError" + +// outputs.upToDateWhen {false} +// showStandardStreams = true + } + } + +// compileJava { +// options.compilerArgs += ["-Xlint:deprecation", "-Xlint:unchecked"] +// } + + ext { + ossrhUsername = System.getenv('SONATYPE_USER') + ossrhPassword = System.getenv('SONATYPE_PASSWORD') + } + + jar { + manifest { + attributes 'Implementation-Version': version, + 'Implementation-Name': name, + 'Implementation-Vendor': 'PayPal' + } + } + + task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allSource + } + + if (version.endsWith("SNAPSHOT")) { + if (!name.equals("butterfly-cli-package")) { + artifacts { + archives sourcesJar + } + } + } else if (System.getenv("SECRING_FILE") != null) { + if (!name.equals("butterfly-cli-package")) { + task javadocJar(type: Jar) { + classifier = 'javadoc' + from javadoc + } + + artifacts { + archives javadocJar, sourcesJar + } + } + + project.ext['signing.keyId'] = "$System.env.GPG_KEYNAME" + project.ext['signing.password'] = "$System.env.GPG_PASSPHRASE" + project.ext['signing.secretKeyRingFile'] = "$System.env.SECRING_FILE" + + signing { + sign configurations.archives + } + } + + // Publishing non test modules + if (!testModules.contains(name)) { + uploadArchives { + repositories { + mavenDeployer { + if (!version.endsWith("SNAPSHOT") && System.getenv("SECRING_FILE") != null) { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + } + + repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + pom.project { + name project.name + description project.description + packaging 'jar' + + url 'https://github.com/paypal/butterfly' + + scm { + connection 'scm:git:git@github.com:paypal/butterfly.git' + developerConnection 'scm:git:git@github.com:paypal/butterfly.git' + url 'git@github.com:paypal/butterfly.git' + } + + licenses { + license { + name 'MIT' + url 'https://opensource.org/licenses/MIT' + } + } + + developers { + developer { + id 'fabiocarvalho777' + name 'Fabio Carvalho' + email 'fabiocarvalho777@gmail.com' + organization 'PayPal' + organizationUrl 'http://www.paypal.com' + } + } + } + } + } + } + } + + jacocoTestCoverageVerification { + violationRules { + rule { element = 'PACKAGE'; limit { minimum = 0.28 }; includes = ['com.paypal.butterfly.cli'] } + rule { element = 'PACKAGE'; limit { minimum = 0.71 }; includes = ['com.paypal.butterfly.cli.logging'] } + rule { element = 'PACKAGE'; limit { minimum = 0.41 }; includes = ['com.paypal.butterfly.core*'] } + rule { element = 'PACKAGE'; limit { minimum = 0.54 }; includes = ['com.paypal.butterfly.extensions.api'] } + rule { element = 'PACKAGE'; limit { minimum = 0.00 }; includes = ['com.paypal.butterfly.extensions.api*'] } + rule { element = 'PACKAGE'; limit { minimum = 1.00 }; includes = ['com.paypal.butterfly.extensions.springboot*'] } + rule { element = 'PACKAGE'; limit { minimum = 0.00 }; includes = ['com.paypal.butterfly.persist*'] } + rule { element = 'PACKAGE'; limit { minimum = 0.63 }; includes = ['com.paypal.butterfly.test*'] } + rule { element = 'PACKAGE'; limit { minimum = 0.82 }; includes = ['com.paypal.butterfly.utilities*'] } + } + } + +} + +task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { + def coverageProjects = subprojects.stream().filter({sb -> !(sb.name in [ + // Projects without Java code + 'butterfly-bom', + 'butterfly-cli-package', + 'extensions-catalog', + + // API projects without concrete classes, or relevant classes, to be tested + 'butterfly-api', + 'butterfly-rest-api', + + // Test projects + 'tests', + 'integration-tests' + ])}).collect() + + additionalSourceDirs = files(coverageProjects.sourceSets.main.allSource.srcDirs) + sourceDirectories = files(coverageProjects.sourceSets.main.allSource.srcDirs) + classDirectories = files(coverageProjects.sourceSets.main.output) + executionData = files(coverageProjects.jacocoTestReport.executionData) + reports { + html.enabled = true + xml.enabled = true + csv.enabled = false + + html.destination file("${buildDir}/reports/jacoco/html") + xml.destination file("${buildDir}/reports/jacoco/coverage.xml") + } +} + +// Returns the hash number of the latest commit +ext.getGitHash = { -> + def stdout = new ByteArrayOutputStream() + exec { + commandLine 'git', 'log', '-1', '--pretty=%H' + standardOutput = stdout + } + return stdout.toString().trim() +} + +// Returns a String made of project version and commit hash number as a suffix, +// used to tag SNAPSHOT Docker images +ext.getCommitProjectVersion = { version -> + if (version.endsWith('SNAPSHOT')) { + return version + "-" + getGitHash() + } else { + return version + } +} + +// Docker repository +// Leaving it empty intentionally, so it defaults to Docker public DTR +ext.docker_repo = '' + +// Dependencies version management +ext.lib = [ + + // Java specs + annotation_api: "javax.annotation:javax.annotation-api:1.3.2", + jaxrs: "javax.ws.rs:javax.ws.rs-api:2.1", + bean_validation: "javax.validation:validation-api:2.0.1.Final", + + // Spring + spring_context: "org.springframework:spring-context:4.3.2.RELEASE", + spring_boot_starter: "org.springframework.boot:spring-boot-starter:1.4.0.RELEASE", + spring_boot_autoconfigure: "org.springframework.boot:spring-boot-autoconfigure:1.4.0.RELEASE", + spring_test: "org.springframework:spring-test:4.3.2.RELEASE", + + // Maven + maven_model: "org.apache.maven:maven-model:3.5.4", + maven_invoker: "org.apache.maven.shared:maven-invoker:3.0.1", + + // Parsers + gson: "com.google.code.gson:gson:2.7", + woodstox_core: "com.fasterxml.woodstox:woodstox-core:5.0.3", + xmlunit: "xmlunit:xmlunit:1.5", + yamlbeans: "com.esotericsoftware.yamlbeans:yamlbeans:1.08", + javaparser_core: "com.github.javaparser:javaparser-core:3.15.14", + jackson: "com.fasterxml.jackson.core:jackson-annotations:2.9.8", + jackson_databind: "com.fasterxml.jackson.core:jackson-databind:2.9.8", + + // Drivers + lightcouch: "org.lightcouch:lightcouch:0.1.8", + + // Utilities + annotations: "com.google.code.findbugs:annotations:3.0.0", + commons_io: "commons-io:commons-io:2.5", + commons_lang3: "org.apache.commons:commons-lang3:3.7", + commons_collections4: "org.apache.commons:commons-collections4:4.0", + guava: "com.google.guava:guava:15.0", + jopt_simple: "net.sf.jopt-simple:jopt-simple:5.0.2", + plexus_utils: "org.codehaus.plexus:plexus-utils:3.1.0", + reflections: "org.reflections:reflections:0.9.10", + slf4j_api: "org.slf4j:slf4j-api:1.7.21", + logback: "ch.qos.logback:logback-classic:1.1.7", + zip4j: "net.lingala.zip4j:zip4j:1.3.2", + + // Swagger + swagger_annotations: "io.swagger.core.v3:swagger-annotations:2.0.8", + + // Tests + testng: "org.testng:testng:6.14.2", + mockito_all: "org.mockito:mockito-all:1.10.19", + powermock_module_testng: "org.powermock:powermock-module-testng:1.6.5", + powermock_api_mockito: "org.powermock:powermock-api-mockito:1.6.5", + + // Jersey + jersey_core: "org.glassfish.jersey.core:jersey-client:2.26", + jersey_inject: "org.glassfish.jersey.inject:jersey-hk2:2.26", + +] diff --git a/butterfly-api/build.gradle b/butterfly-api/build.gradle new file mode 100644 index 00000000..6dd87cf8 --- /dev/null +++ b/butterfly-api/build.gradle @@ -0,0 +1,5 @@ +apply plugin: 'java-library' + +dependencies { + api project(':butterfly-extensions-api') +} diff --git a/butterfly-api/src/main/java/com/paypal/butterfly/api/AbortDetails.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/AbortDetails.java new file mode 100644 index 00000000..7d093dbb --- /dev/null +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/AbortDetails.java @@ -0,0 +1,68 @@ +package com.paypal.butterfly.api; + +/** + * POJO describing a transformation abort in details. + * + * @author facarvalho + */ +public interface AbortDetails { + + /** + * Returns the name of the transformation template that caused the transformation abort. + * + * @return the name of the transformation template that caused the transformation abort. + */ + String getTemplateName(); + + /** + * Returns the name of the transformation template class that caused the transformation abort. + * + * @return the name of the transformation template class that caused the transformation abort. + */ + String getTemplateClassName(); + + /** + * Returns the name of the Transformation Utility that caused the abort + * + * @return the name of the Transformation Utility that caused the abort + */ + String getUtilityName(); + + /** + * Returns the name of the transformation utility class that caused the abort + * + * @return the name of the transformation utility class that caused the abort + */ + String getUtilityClassName(); + + /** + * Returns the abort message + * + * @return the abort message + */ + String getAbortMessage(); + + /** + * Returns the class of the exception that caused the transformation abort + * + * @return the class of the exception that caused the transformation abort + */ + String getExceptionClassName(); + + /** + * Returns the message of the exception that caused the transformation abort + * + * @return the message of the exception that caused the transformation abort + */ + String getExceptionMessage(); + + /** + * Returns a String representation of the stack trace related to the exception + * that caused the transformation abort + * + * @return a String representation of the stack trace related to the exception + * that caused the transformation abort + */ + String getExceptionStackTrace(); + +} diff --git a/butterfly-api/src/main/java/com/paypal/butterfly/api/Application.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/Application.java new file mode 100644 index 00000000..949798bd --- /dev/null +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/Application.java @@ -0,0 +1,19 @@ +package com.paypal.butterfly.api; + +import java.io.File; + +/** + * POJO holding information about the application to be transformed + * + * @author facarvalho + */ +public interface Application { + + /** + * Returns the application folder + * + * @return the application folder + */ + File getFolder(); + +} diff --git a/butterfly-api/src/main/java/com/paypal/butterfly/api/ButterflyFacade.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/ButterflyFacade.java new file mode 100644 index 00000000..a1aa29d2 --- /dev/null +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/ButterflyFacade.java @@ -0,0 +1,162 @@ +package com.paypal.butterfly.api; + +import com.paypal.butterfly.extensions.api.Extension; +import com.paypal.butterfly.extensions.api.TransformationTemplate; +import com.paypal.butterfly.extensions.api.exception.TemplateResolutionException; +import com.paypal.butterfly.extensions.api.upgrade.UpgradeStep; + +import java.io.File; +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; + +/** + * Butterfly façade + * + * @author facarvalho + */ +public interface ButterflyFacade { + + /** + * Returns Butterfly version + * + * @return Butterfly version + */ + String getButterflyVersion(); + + /** + * Returns an unmodifiable list of all registered extensions + * + * @return an unmodifiable list of all registered extensions + */ + List getExtensions(); + + /** + * Butterfly might be able to automatically identify the type of application + * and which transformation template to be applied to it. This automatic + * transformation template resolution is performed by each registered + * Extension class. Based on the application folder, and its content, each + * registered extension might decide which transformation template should be used + * to transform it. Only one can be chosen. These are the possible resolution results: + *
    + *
  1. Empty optional is returned: if no transformation template is resolved (unless if extension(s) evaluated the application as invalid, + * then an {@link TemplateResolutionException} is thrown, as explained below in details)
  2. + *
  3. An optional with {@link TransformationTemplate} class is returned: if only one transformation template is resolved
  4. + *
  5. A {@link TemplateResolutionException} exception is thrown: if one of the following cases is true (call {@link TemplateResolutionException#getMessage()} for details): + *
      + *
    1. If there are no extensions registered
    2. + *
    3. Multiple transformation templates are resolved
    4. + *
    5. If no transformation template is resolved, but one or more extensions recognized the application type, but determined it to be invalid for transformation
    6. + *
    + *
+ * + * @param applicationFolder the folder where the code of the application to be transformed is + * @return see above + * @throws TemplateResolutionException see above + */ + Optional> automaticResolution(File applicationFolder) throws TemplateResolutionException; + + /** + * Creates and returns a new {@link Configuration} object + * set to apply the transformation against the original application folder + * and the result will not be compressed to a zip file. + *
+ * Notice that calling this method will result in {@link Configuration#isModifyOriginalFolder()} + * to return {@code true}. + * + * @param properties a properties object specifying details about the transformation itself. + * These properties help to specialize the + * transformation, for example, determining if certain operations should + * be skipped or not, or how certain aspects of the transformation should + * be executed. The set of possible properties is defined by the used transformation + * extension and template, read the documentation offered by the extension + * for further details. The properties values are defined by the user requesting the transformation. + * Properties are optional, so, if not desired, this parameter can be set to null. + * @return a brand new {@link Configuration} object + * @throws IllegalArgumentException if properties object is invalid. Properties name must + * be non blank and only contain alphabetical characters, dots, underscore or hyphen. Properties + * object must be Strings and cannot be null. + */ + Configuration newConfiguration(Properties properties); + + /** + * Creates and returns a new {@link Configuration} object + * set to place the transformed application at a new folder at the original application + * parent folder, besides compressing it to a zip file, depending on {@code zipOutput}. + *
+ * The transformed application folder's name is the same as original folder, + * plus a "-transformed-yyyyMMddHHmmssSSS" suffix. + *
+ * Notice that calling this method will result in {@link Configuration#isModifyOriginalFolder()} + * to return {@code false}. + * + * @param properties a properties object specifying details about the transformation itself. + * These properties help to specialize the + * transformation, for example, determining if certain operations should + * be skipped or not, or how certain aspects of the transformation should + * be executed. The set of possible properties is defined by the used transformation + * extension and template, read the documentation offered by the extension + * for further details. The properties values are defined by the user requesting the transformation. + * Properties are optional, so, if not desired, this parameter can be set to null. + * @param zipOutput if true, the transformed application folder will be compressed into a zip file + * @return a brand new {@link Configuration} object + * @throws IllegalArgumentException if properties object is invalid. Properties name must + * be non blank and only contain alphabetical characters, dots, underscore or hyphen. Properties + * object must be Strings and cannot be null. + */ + Configuration newConfiguration(Properties properties, boolean zipOutput); + + /** + * Creates and returns a new {@link Configuration} object + * set to place the transformed application at {@code outputFolder}, + * and compress it to a zip file or not, depending on {@code zipOutput}. + *
+ * Notice that calling this method will result in {@link Configuration#isModifyOriginalFolder()} + * to return {@code false}. + * + * @param properties a properties object specifying details about the transformation itself. + * These properties help to specialize the + * transformation, for example, determining if certain operations should + * be skipped or not, or how certain aspects of the transformation should + * be executed. The set of possible properties is defined by the used transformation + * extension and template, read the documentation offered by the extension + * for further details. The properties values are defined by the user requesting the transformation. + * Properties are optional, so, if not desired, this parameter can be set to null. + * @param outputFolder the output folder where the transformed application is + * supposed to be placed + * @param zipOutput if true, the transformed application folder will be compressed into a zip file + * @return a brand new {@link Configuration} object + * @throws IllegalArgumentException if {@code outputFolder} is null, does not exist, or is not a directory + * @throws IllegalArgumentException if properties object is invalid. Properties name must + * be non blank and only contain alphabetical characters, dots, underscore or hyphen. Properties + * object must be Strings and cannot be null. + */ + Configuration newConfiguration(Properties properties, File outputFolder, boolean zipOutput); + + /** + * Transforms an application in an asynchronous and non-blocking manner. + * If templateClass is a {@link UpgradeStep}, application will be upgraded to the latest version. + * + * @param applicationFolder application folder + * @param templateClass transformation template class + * @return the transformation result object + */ + CompletableFuture transform(File applicationFolder, Class templateClass); + + /** + * Transforms an application in an asynchronous and non-blocking manner. + * If templateClass is a {@link UpgradeStep}, application will be upgraded according to version. + * It also accepts an additional parameter providing configuration. See {@link Configuration} for further information. + * + * @param applicationFolder application folder + * @param templateClass transformation template class + * @param version the target upgrade version. If this parameter is null or blank, application will be upgraded to the latest version. + * If templateClass is not a {@link UpgradeStep}, this parameter is ignored. + * @param configuration Butterfly configuration object + * @throws IllegalArgumentException if templateClass is a {@link UpgradeStep} and version is not empty and an unknown version + * @return the transformation result object + */ + CompletableFuture transform(File applicationFolder, Class templateClass, String version, Configuration configuration); + +} \ No newline at end of file diff --git a/butterfly-api/src/main/java/com/paypal/butterfly/api/Configuration.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/Configuration.java new file mode 100644 index 00000000..37531b41 --- /dev/null +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/Configuration.java @@ -0,0 +1,55 @@ +package com.paypal.butterfly.api; + +import java.io.File; +import java.util.Properties; + +/** + * Butterfly transformation configuration object. This object specify configuration + * details about the requested transformation. + * Use one of the factory methods under {@link ButterflyFacade} to create a new configuration object. + * + * @author facarvalho + */ +public interface Configuration { + + /** + * Returns the folder where the transformed application is supposed to be placed, + * or null, if no custom folder has been specified + * + * @return the folder where the transformed application is supposed to be placed + */ + File getOutputFolder(); + + /** + * Returns whether the transformed application folder will be compressed into a zip file or not + * + * @return whether the transformed application folder will be compressed into a zip file or not + */ + boolean isZipOutput(); + + /** + * Returns whether the transformation will occur in the original application folder + * + * @return whether the transformation will occur in the original application folder + */ + boolean isModifyOriginalFolder(); + + /** + * Returns a properties object specifying details about the transformation itself. + * These properties help to specialize the transformation, for example, + * determining if certain operations should be skipped or not, + * or how certain aspects of the transformation should be executed. + *
+ * The set of possible properties is defined by the used transformation + * extension and template, read the documentation offered by the extension + * for further details. + *
+ * The properties values are defined by the user requesting the transformation. + *
+ * Properties are optional and, if not set, this method will return null. + * + * @return transformation request specific properties + */ + Properties getProperties(); + +} diff --git a/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationListener.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationListener.java new file mode 100644 index 00000000..959abd80 --- /dev/null +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationListener.java @@ -0,0 +1,41 @@ +package com.paypal.butterfly.api; + +/** + * Transformation listener objects are notified of transformation events + * + * @author facarvalho + */ +public interface TransformationListener { + + /** + * This event notification happens right before a transformation begins. + * If the transformation is an upgrade path, then this notification will be sent only + * once, in the very beginning, before the first upgrade step. + * + * @param transformationRequest an object describing the transformation request + */ +// void preTransformation(TransformationRequest transformationRequest); + + /** + * This event notification happens right after a transformation is successfully completed. + * If the transformation is an upgrade path, then this notification will be sent only + * once, in the end, after all upgrade steps are completed. + * + * @param transformationRequest an object describing the transformation request + * @param transformationResult an object describing the transformation result + */ + void postTransformation(TransformationRequest transformationRequest, TransformationResult transformationResult); + + /** + * This event notification happens only if a transformation is aborted, and it is sent right after it. + * + * @param transformationRequest an object describing the transformation request + * @param transformationResult an object describing the transformation result + */ + void postTransformationAbort(TransformationRequest transformationRequest, TransformationResult transformationResult); + +// void preUpgradeStep(TransformationRequest transformationRequest, List transformationContexts); + +// void postUpgradeStep(TransformationRequest transformationRequest, List transformationContexts); + +} diff --git a/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationMetrics.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationMetrics.java new file mode 100644 index 00000000..0de9904b --- /dev/null +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationMetrics.java @@ -0,0 +1,110 @@ +package com.paypal.butterfly.api; + +import com.paypal.butterfly.api.TransformationRequest; +import com.paypal.butterfly.api.TransformationResult; + +/** + * POJO containing metrics and statistics about the result of a transformation template execution. + * One, or more (in case of an upgrade), transformation metric can object is retrieved + * by calling {@link TransformationResult#getMetrics()} + * + * @author facarvalho + */ +public interface TransformationMetrics { + + /** + * Returns the name of the transformation template whose execution generated these metrics. + * Notice that this value might differ from {@link TransformationRequest#getTemplateName()}, + * in case the transformation was an upgrade, since one transformation metric object is + * created per upgrade step. + * + * @return the name of the transformation template whose execution generated these metrics + */ + String getTemplateName(); + + /** + * Returns the name of the transformation template class whose execution generated these metrics. + * Notice that this value might differ from {@link TransformationRequest#getTemplateClassName()}, + * in case the transformation was an upgrade, since one transformation metric object is + * created per upgrade step. + * + * @return the name of the transformation template class whose execution generated these metrics + */ + String getTemplateClassName(); + + /** + * Returns the transformation conclusion date in "yyyyy-mm-dd hh:mm:ss" + * + * @return the transformation conclusion date in "yyyyy-mm-dd hh:mm:ss" + */ + String getDateTime(); + + /** + * Returns the transformation conclusion date in milliseconds + * + * @return the transformation conclusion date in milliseconds + */ + long getTimestamp(); + + /** + * Returns the version the application was upgraded from. + * It returns null if the transformation template is not + * an upgrade template + * + * @return the version the application was upgraded from. + * It returns null if the transformation template is not + * an upgrade template + */ + String getFromVersion(); + + /** + * Returns the version the application was upgraded to. + * It returns null if the transformation template is not + * an upgrade template + * + * @return the version the application was upgraded to. + * It returns null if the transformation template is not + * an upgrade template + */ + String getToVersion(); + + /** + * Returns true only if this particular transformation template has completed successfully. + * Notice that even a successful transformation might have post-transformation + * manual instructions, warnings or errors. + *
+ * Notice that this method differs from {@link TransformationResult#isSuccessful()}, + * which refers to the result of the whole transformation, while this, in the case of an upgrade, + * refers to the result of a particular upgrade step, since one transformation metric object is + * created per upgrade step. + *
+ * If transformation is not successful, details about why it aborted + * can be retrieved by {@link TransformationResult#getAbortDetails()} + * + * @return true if the transformation was successful, or false, if it aborted. + */ + boolean isSuccessful(); + + /** + * Returns true if this transformation requires + * manual instructions to be completed. It returns + * false otherwise, or if the transformation aborted. + *
+ * Notice that this method differs from {@link TransformationResult#hasManualInstructions()}, + * which refers to the result of the whole transformation, while this, in the case of an upgrade, + * refers to the result of a particular upgrade step, since one transformation metric object is + * created per upgrade step. + * + * @return true if this transformation requires + * manual instructions to be completed + */ + boolean hasManualInstructions(); + + /** + * Returns transformation statistics + * + * @return transformation statistics + */ + TransformationStatistics getStatistics(); + +} diff --git a/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationRequest.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationRequest.java new file mode 100644 index 00000000..407a0d15 --- /dev/null +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationRequest.java @@ -0,0 +1,99 @@ +package com.paypal.butterfly.api; + +/** + * This interface represents a transformation request, providing meta-data about + * the application to be transformed. + * + * @author facarvalho + */ +public interface TransformationRequest { + + /** + * Returns the unique identifier for the transformation + * + * @return Unique ID + */ + String getId(); + + /** + * Returns Butterfly version + * + * @return Butterfly version + */ + String getButterflyVersion(); + + /** + * Returns the transformation request date in "yyyyy-mm-dd hh:mm:ss" + * + * @return the transformation request date in "yyyyy-mm-dd hh:mm:ss" + */ + String getDateTime(); + + /** + * Returns the transformation request date in milliseconds + * + * @return the transformation request date in milliseconds + */ + long getTimestamp(); + + /** + * Returns information about the application to be transformed + * + * @return information about the application to be transformed + */ + Application getApplication(); + + /** + * Returns the configuration object associated with this transformation request + * + * @return the configuration object associated with this transformation request + */ + Configuration getConfiguration(); + + /** + * Returns the name of the Butterfly extension used in this transformation request + * + * @return the name of the Butterfly extension used in this transformation request + */ + String getExtensionName(); + + /** + * Returns the version of the Butterfly extension used in this transformation request + * + * @return the version of the Butterfly extension used in this transformation request + */ + String getExtensionVersion(); + + /** + * Returns the name of the transformation template set in this transformation request. + * + * @return the name of the transformation template set in this transformation request + */ + String getTemplateName(); + + /** + * Returns the name of the transformation template class set in this transformation request + * + * @return the name of the transformation template class set in this transformation request + */ + String getTemplateClassName(); + + /** + * Returns true if the transformation template used in this transformation request is an upgrade step, + * or false, if it is a regular transformation template. + * See {@link com.paypal.butterfly.extensions.api.upgrade.UpgradeStep}. + * See {@link com.paypal.butterfly.extensions.api.TransformationTemplate}. + * + * @return true if the transformation template used in this transformation request is an upgrade step + */ + boolean isUpgradeStep(); + + /** + * Returns true if the transformation template used in this transformation request is blank. + * See {@link com.paypal.butterfly.extensions.api.TransformationTemplate#setBlank(boolean)}. + * + * @return true if the transformation template used in this transformation request is blank + */ + boolean isBlank(); + +} diff --git a/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationResult.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationResult.java new file mode 100644 index 00000000..ac314575 --- /dev/null +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationResult.java @@ -0,0 +1,171 @@ +package com.paypal.butterfly.api; + +import java.io.File; +import java.util.List; +import java.util.Map; + +/** + * Transformation result + * + * @author facarvalho + */ +public interface TransformationResult { + + /** + * Returns an identifier for this transformation result + * + * @return an identifier for this transformation result + */ + String getId(); + + /** + * Returns the {@link TransformationRequest} that originated this transformation + * + * @return the {@link TransformationRequest} that originated this transformation + */ + TransformationRequest getTransformationRequest(); + + /** + * Returns the id of the user who requested the transformation + * + * @return the id of the user who requested the transformation + */ + String getUserId(); + + /** + * Returns the transformation conclusion date in "yyyyy-mm-dd hh:mm:ss" + * + * @return the transformation conclusion date in "yyyyy-mm-dd hh:mm:ss" + */ + String getDateTime(); + + /** + * Returns the transformation conclusion date in milliseconds + * + * @return the transformation conclusion date in milliseconds + */ + long getTimestamp(); + + /** + * Returns the type of the transformed application + * + * @return the type of the transformed application + */ + String getApplicationType(); + + /** + * Returns the name of the transformed application + * + * @return the name of the transformed application + */ + String getApplicationName(); + + /** + * Returns true only if this transformation has completed successfully. + * Notice that even a successful transformation might have post-transformation + * manual instructions, warnings or errors. + * Check {@link #getMetrics()} to find out more about the transformation result. + * + * @return true if the transformation was successful, or false, if it aborted. + */ + boolean isSuccessful(); + + /** + * Returns the folder where the transformed application is. + * Even if the transformation didn't complete successfully + * this method will return the transformed application directory. + * + * @return the folder where the transformed application is + */ + File getTransformedApplicationDir(); + + /** + * Return how many upgrade steps were executed during this transformation. + * It returns 0 if the transformation was actually not an upgrade. + * + * @return how many upgrade steps were executed during this transformation, + * or 0, if the transformation was actually not an upgrade + */ + int getUpgradeStepsCount(); + + /** + * Returns true if this transformation requires + * manual instructions to be completed. It returns + * false otherwise, or if the transformation aborted. + * + * @return true if this transformation requires + * manual instructions to be completed + */ + boolean hasManualInstructions(); + + /** + * Return how many manual instructions are + * necessary to complete the transformation. + * Notice that, in case of upgrades, this number + * will be the total of all upgrade steps + * manual instructions. + * + * @return how many manual instructions are + * necessary to complete the transformation + */ + int getManualInstructionsTotal(); + + /** + * Returns the directory where all post-transformation + * manual instruction documents are placed. It returns + * null if the transformation didn't result in any + * post-transformation manual instruction, + * or if the transformation aborted. + * + * @return the directory where all post-transformation + * manual instruction documents are placed + */ + File getManualInstructionsDir(); + + /** + * Returns the document file that contains + * the manual instructions, or null, if there is none, + * or if the transformation aborted. + * + * @return the document file that contains + * the manual instructions, or null, if there is none + */ + File getManualInstructionsFile(); + + /** + * Returns an unmodifiable list with the metrics generated by the transformation. + * If the transformation is an upgrade, then the list will have one metrics object per + * upgrade step. If it is not, then it will have just one item. + * + * @return a list of metrics generated by this transformation + */ + List getMetrics(); + + /** + * Returns an unmodifiable map with the metrics generated by the transformation. + * The key is the transformation template class name, while the value is the metrics object. + * If the transformation is an upgrade, then the map will have one metrics object per + * upgrade step. If it is not, then it will have just one item. + * + * @return a map of metrics generated by this transformation + */ + Map getMetricsMap(); + + /** + * Returns details about the reason why the transformation + * associated with this metric record aborted. If it actually + * succeeded, then it returns null. + * + * @return details about the reason why the transformation + * associated with this metric record aborted + */ + AbortDetails getAbortDetails(); + + /** + * Returns a String representing this transformation result object in JSON format + * + * @return a String representing this transformation result object in JSON format + */ + String toJson(); + +} diff --git a/butterfly-extensions-api/src/main/java/com/paypal/butterfly/extensions/api/metrics/TransformationStatistics.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationStatistics.java similarity index 95% rename from butterfly-extensions-api/src/main/java/com/paypal/butterfly/extensions/api/metrics/TransformationStatistics.java rename to butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationStatistics.java index 752dd905..75fca42e 100644 --- a/butterfly-extensions-api/src/main/java/com/paypal/butterfly/extensions/api/metrics/TransformationStatistics.java +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/TransformationStatistics.java @@ -1,7 +1,8 @@ -package com.paypal.butterfly.extensions.api.metrics; +package com.paypal.butterfly.api; /** - * Transformation statistics. + * POJO containing statistics about + * the result of a transformation template execution * * @author facarvalho */ diff --git a/butterfly-facade/src/main/java/com/paypal/butterfly/facade/exception/TransformationException.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/exception/TransformationException.java similarity index 69% rename from butterfly-facade/src/main/java/com/paypal/butterfly/facade/exception/TransformationException.java rename to butterfly-api/src/main/java/com/paypal/butterfly/api/exception/TransformationException.java index cead81ec..30e461ad 100644 --- a/butterfly-facade/src/main/java/com/paypal/butterfly/facade/exception/TransformationException.java +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/exception/TransformationException.java @@ -1,4 +1,4 @@ -package com.paypal.butterfly.facade.exception; +package com.paypal.butterfly.api.exception; import com.paypal.butterfly.extensions.api.exception.ButterflyException; @@ -14,8 +14,8 @@ public TransformationException(String exceptionMessage) { super(exceptionMessage); } - public TransformationException(String exceptionMessage, Exception exception) { - super(exceptionMessage, exception); + public TransformationException(String exceptionMessage, Throwable throwable) { + super(exceptionMessage, throwable); } } diff --git a/butterfly-api/src/main/java/com/paypal/butterfly/api/package-info.java b/butterfly-api/src/main/java/com/paypal/butterfly/api/package-info.java new file mode 100644 index 00000000..351460ce --- /dev/null +++ b/butterfly-api/src/main/java/com/paypal/butterfly/api/package-info.java @@ -0,0 +1,6 @@ +/** + * Butterfly API offering a facade for programmatically transformation integration. + * + * @since 3.0.0 + */ +package com.paypal.butterfly.api; \ No newline at end of file diff --git a/butterfly-bom/build.gradle b/butterfly-bom/build.gradle new file mode 100644 index 00000000..68ff51ab --- /dev/null +++ b/butterfly-bom/build.gradle @@ -0,0 +1,79 @@ + +description = "Butterfly BOM (Bill of Materials)" + +// List of all Butterfly projects to be exposed as an API and shared in Butterfly BOM +def api = [ + 'com.paypal.butterfly:butterfly-api', + 'com.paypal.butterfly:butterfly-extensions-api', + 'com.paypal.butterfly:butterfly-utilities', + 'com.paypal.butterfly:butterfly-persist-couchdb', + 'com.paypal.butterfly:butterfly-persist-file', + 'com.paypal.butterfly:butterfly-slack', + 'com.paypal.butterfly:butterfly-test', +] + +install.repositories.mavenInstaller { + pom.whenConfigured { + packaging = "pom" + withXml { + asNode().children().last() + { + delegate.dependencyManagement { + delegate.dependencies { + def d = [] + + // Adding Butterfly API dependencies defined above + api.each { a -> d.add(a + ':' + version) } + + // And also all dependencies managed in the root project under ext.lib + d.addAll(parent.lib.values()) + + // Inserting each managed dependency in the pom.xml file + d.each { p -> + if (p != project) { + def pa = p.tokenize(':') + delegate.dependency { + delegate.groupId(pa[0]) + delegate.artifactId(pa[1]) + delegate.version(pa[2]) + } + } + } + } + } + } + } + } +} + +uploadArchives.repositories.mavenDeployer { + pom.whenConfigured { + packaging = "pom" + withXml { + asNode().children().last() + { + delegate.dependencyManagement { + delegate.dependencies { + def d = [] + + // Adding Butterfly API dependencies defined above + api.each { a -> d.add(a + ':' + version)} + + // And also all dependencies managed in the root project under ext.lib + d.addAll(parent.lib.values()) + + // Inserting each managed dependency in the pom.xml file + d.each { p -> + if (p != project) { + def pa = p.tokenize(':') + delegate.dependency { + delegate.groupId(pa[0]) + delegate.artifactId(pa[1]) + delegate.version(pa[2]) + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/butterfly-cli-package/build.gradle b/butterfly-cli-package/build.gradle new file mode 100644 index 00000000..214c025c --- /dev/null +++ b/butterfly-cli-package/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'java' +apply plugin: "maven-publish" + +dependencies { + compile project(':butterfly-cli') +} + +task buildZip(type: Zip) { + from processResources { + exclude ('butterfly/butterfly') + } + from("src/main/resources") { + include ('butterfly/butterfly') + fileMode = 0755 + } + into('butterfly/lib') { + from configurations.runtime + } +} + +artifacts { + archives buildZip +} + +build.dependsOn buildZip \ No newline at end of file diff --git a/butterfly-cli-package/pom.xml b/butterfly-cli-package/pom.xml deleted file mode 100644 index 61eb2398..00000000 --- a/butterfly-cli-package/pom.xml +++ /dev/null @@ -1,116 +0,0 @@ - - 4.0.0 - - - com.paypal.butterfly - butterfly-parent - 2.5.0 - .. - - - butterfly-cli-package - pom - - - - MIT - https://opensource.org/licenses/MIT - - - - - - com.paypal.butterfly - butterfly-cli - ${project.version} - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - prepare-package - - copy-dependencies - - - src/main/resources/lib - - - - - - - maven-clean-plugin - 3.0.0 - - - - src/main/resources/lib - - - src/main/resources/extensions - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - process-resources - - - - - - - run - - - - - - - - org.apache.maven.plugins - maven-resources-plugin - 3.0.1 - - true - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - src/main/assembly/zip.xml - - - - - make-zip - package - - single - - - - - - - - - diff --git a/butterfly-cli-package/src/main/assembly/zip.xml b/butterfly-cli-package/src/main/assembly/zip.xml deleted file mode 100644 index 149b4a07..00000000 --- a/butterfly-cli-package/src/main/assembly/zip.xml +++ /dev/null @@ -1,33 +0,0 @@ - - / - - zip - - false - - - src/main/resources/extensions - butterfly/extensions - - - src/main/resources/lib - butterfly/lib - - - src/main/resources - - butterfly.cmd - - butterfly - - - src/main/resources - - butterfly - - butterfly - 755 - - - \ No newline at end of file diff --git a/butterfly-cli-package/src/main/resources/butterfly b/butterfly-cli-package/src/main/resources/butterfly/butterfly similarity index 100% rename from butterfly-cli-package/src/main/resources/butterfly rename to butterfly-cli-package/src/main/resources/butterfly/butterfly diff --git a/butterfly-cli-package/src/main/resources/butterfly.cmd b/butterfly-cli-package/src/main/resources/butterfly/butterfly.cmd similarity index 96% rename from butterfly-cli-package/src/main/resources/butterfly.cmd rename to butterfly-cli-package/src/main/resources/butterfly/butterfly.cmd index f5292f72..275d15f2 100644 --- a/butterfly-cli-package/src/main/resources/butterfly.cmd +++ b/butterfly-cli-package/src/main/resources/butterfly/butterfly.cmd @@ -1,9 +1,9 @@ -@echo off - -set __BUTTERFLY_HOME__=%~dp0 - -if defined BUTTERFLY_HOME ( - set __BUTTERFLY_HOME__=%BUTTERFLY_HOME% -) - -java -cp "%__BUTTERFLY_HOME__%\lib\*";"%__BUTTERFLY_HOME__%\extensions\*" com.paypal.butterfly.cli.ButterflyCliApp %* +@echo off + +set __BUTTERFLY_HOME__=%~dp0 + +if defined BUTTERFLY_HOME ( + set __BUTTERFLY_HOME__=%BUTTERFLY_HOME% +) + +java -cp "%__BUTTERFLY_HOME__%\lib\*";"%__BUTTERFLY_HOME__%\extensions\*" com.paypal.butterfly.cli.ButterflyCliApp %* diff --git a/butterfly-cli-package/src/main/resources/butterfly/extensions/README.txt b/butterfly-cli-package/src/main/resources/butterfly/extensions/README.txt new file mode 100644 index 00000000..691a97d3 --- /dev/null +++ b/butterfly-cli-package/src/main/resources/butterfly/extensions/README.txt @@ -0,0 +1 @@ +Place your Butterfly extensions jar files here. \ No newline at end of file diff --git a/butterfly-cli/build.gradle b/butterfly-cli/build.gradle new file mode 100644 index 00000000..da0e44b7 --- /dev/null +++ b/butterfly-cli/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'application' + +mainClassName = 'com.paypal.butterfly.cli.ButterflyCliApp' + +dependencies { + + compile project(':butterfly-api') + compile project(':butterfly-extensions-api') + runtime project(':butterfly-core') + compile lib.jopt_simple, + lib.gson, + lib.commons_io + compile (lib.spring_boot_starter) { + exclude(module: 'commons-logging') + } + testCompile project(':butterfly-utilities') + testCompile(lib.testng) { + exclude(module: 'aopalliance') + exclude(module: 'guava') + } + testCompile lib.mockito_all, + lib.powermock_module_testng, + lib.powermock_api_mockito +} + +jar { + manifest { + attributes 'Main-Class': 'com.paypal.butterfly.cli.ButterflyCliApp', + 'Implementation-Version': version, + 'Implementation-Name': name, + 'Implementation-Vendor': 'PayPal' + } +} + +test { + forkEvery = 1 +} + +test.useTestNG() diff --git a/butterfly-cli/pom.xml b/butterfly-cli/pom.xml deleted file mode 100644 index 5df6c6d5..00000000 --- a/butterfly-cli/pom.xml +++ /dev/null @@ -1,73 +0,0 @@ - - 4.0.0 - - - com.paypal.butterfly - butterfly-parent - 2.5.0 - .. - - - butterfly-cli - - - - MIT - https://opensource.org/licenses/MIT - - - - - - org.springframework.boot - spring-boot-starter - - - net.sf.jopt-simple - jopt-simple - 5.0.2 - - - com.google.code.gson - gson - - - com.paypal.butterfly - butterfly-facade - ${project.version} - - - com.paypal.butterfly - butterfly-core - ${project.version} - runtime - - - com.paypal.butterfly - butterfly-extensions-api - ${project.version} - - - org.testng - testng - test - - - org.mockito - mockito-all - test - - - org.powermock - powermock-module-testng - test - - - org.powermock - powermock-api-mockito - test - - - - diff --git a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliApp.java b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliApp.java index 1b3a3e5c..217fcfc5 100644 --- a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliApp.java +++ b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliApp.java @@ -2,8 +2,11 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.paypal.butterfly.api.ButterflyFacade; import com.paypal.butterfly.cli.logging.LogFileDefiner; -import com.paypal.butterfly.facade.ButterflyProperties; import joptsimple.OptionException; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; @@ -24,30 +27,36 @@ * @author facarvalho */ @SpringBootApplication -public class ButterflyCliApp extends ButterflyCliOption { +class ButterflyCliApp extends ButterflyCliOption { - private static File butterflyHome; - private static String banner; + private File butterflyHome; + private String banner; - private static Logger logger; + private Logger logger; @SuppressWarnings("PMD.DoNotCallSystemExit") - public static void main(String... arguments) throws IOException { - int exitStatus = run(arguments).getExitStatus(); + public static void main(String... arguments) { + ButterflyCliApp butterflyCliApp = new ButterflyCliApp(); + int exitStatus = butterflyCliApp.run(arguments).getExitStatus(); System.exit(exitStatus); } - public static ButterflyCliRun run(String... arguments) throws IOException { + ButterflyCliRun run(String... arguments) { setButterflyHome(); + + LogFileDefiner.setButterflyHome(butterflyHome); + setEnvironment(arguments); logger = LoggerFactory.getLogger(ButterflyCliApp.class); - setBanner(); - ConfigurableApplicationContext applicationContext = SpringApplication.run(ButterflyCliApp.class, arguments); + ButterflyFacade butterflyFacade = applicationContext.getBean(ButterflyFacade.class); + + setBanner(butterflyFacade); + ButterflyCliRunner butterflyCliRunner = applicationContext.getBean(ButterflyCliRunner.class); - ButterflyCliRun run = butterflyCliRunner.run(); + ButterflyCliRun run = butterflyCliRunner.run(butterflyHome, banner); run.setInputArguments(arguments); if (optionSet != null && optionSet.has(CLI_OPTION_RESULT_FILE)) { @@ -57,7 +66,7 @@ public static ButterflyCliRun run(String... arguments) throws IOException { return run; } - private static void setButterflyHome() { + private void setButterflyHome() { String butterflyHomePath = System.getenv("BUTTERFLY_HOME"); if (butterflyHomePath == null) { butterflyHomePath = System.getProperty("user.dir"); @@ -66,7 +75,7 @@ private static void setButterflyHome() { } @SuppressWarnings("PMD.DoNotCallSystemExit") - private static void setEnvironment(String[] arguments) { + private void setEnvironment(String[] arguments) { if(arguments.length != 0){ try { setOptionSet(arguments); @@ -75,8 +84,7 @@ private static void setEnvironment(String[] arguments) { LogFileDefiner.setLogFileName(applicationFolder, debug); } catch (OptionException e) { Logger logger = LoggerFactory.getLogger(ButterflyCliApp.class); - setBanner(); - logger.info(getBanner()); + logger.info("Butterfly application transformation tool"); logger.error(e.getMessage()); System.exit(1); } @@ -88,7 +96,7 @@ private static void setEnvironment(String[] arguments) { * as an input argument that is really an existent folder. * Otherwise, returns null. */ - private static File getApplicationFolder() { + private File getApplicationFolder() { List nonOptionArguments = optionSet.nonOptionArguments(); if (nonOptionArguments == null || nonOptionArguments.size() == 0 || StringUtils.isEmpty(nonOptionArguments.get(0))) { return null; @@ -101,28 +109,28 @@ private static File getApplicationFolder() { return applicationFolder; } - private static void setBanner() { + private void setBanner(ButterflyFacade butterflyFacade) { // Ideally the version should be gotten from the façade Spring bean. // However, it is not available this early, so we are getting it directly // from the CLI artifact, assuming that the CLI jar will always bring together // the exact same version of butterfly-core, which is the component to officially // define Butterfly version - banner = String.format("Butterfly application transformation tool (version %s)", ButterflyProperties.getString("butterfly.version")); + banner = String.format("Butterfly application transformation tool (version %s)", butterflyFacade.getButterflyVersion()); } - public static File getButterflyHome() { - return butterflyHome; - } - - // This method's visibility is intentionally being set to package - @SuppressWarnings("PMD.DefaultPackage") - static String getBanner() { - return banner; - } - - private static void writeResultFile(ButterflyCliRun run) { - GsonBuilder gsonBuilder = new GsonBuilder().setPrettyPrinting(); + private void writeResultFile(ButterflyCliRun run) { + GsonBuilder gsonBuilder = new GsonBuilder().serializeNulls().setPrettyPrinting().registerTypeAdapter(File.class, new TypeAdapter() { + @Override + public void write(JsonWriter jsonWriter, File file) throws IOException { + String fileAbsolutePath = (file == null ? null : file.getAbsolutePath()); + jsonWriter.value(fileAbsolutePath); + } + @Override + public File read(JsonReader jsonReader) { + throw new UnsupportedOperationException("There is no support for deserializing transformation result objects at the moment"); + } + }); Gson gson = gsonBuilder.create(); String runJsonString = gson.toJson(run); File resultFile = (File) optionSet.valueOf(CLI_OPTION_RESULT_FILE); diff --git a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliOption.java b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliOption.java index 0ccaf271..07d10259 100644 --- a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliOption.java +++ b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliOption.java @@ -28,6 +28,8 @@ abstract class ButterflyCliOption { protected static final String CLI_OPTION_UPGRADE_VERSION = "u"; protected static final String CLI_OPTION_RESULT_FILE = "r"; protected static final String CLI_OPTION_MODIFY_ORIGINAL_FOLDER = "f"; + protected static final String CLI_OPTION_INLINE_PROPERTIES = "p"; + protected static final String CLI_OPTION_PROPERTIES_FILE = "q"; protected static final OptionParser optionParser = new OptionParser(); protected static OptionSet optionSet; @@ -81,9 +83,21 @@ abstract class ButterflyCliOption { // Modify original folder option optionParser.accepts(CLI_OPTION_MODIFY_ORIGINAL_FOLDER, "Transforms the application in the same folder as the original content. Options (-o) or (-z) are ignored if (-f) is specified"); + + // Transformation template inline properties option + optionParser.accepts(CLI_OPTION_INLINE_PROPERTIES, "Transformation specific properties, used to determine if certain operations should be skipped or not, or how certain aspects of the transformation should be executed. Use it followed by a list of key value pairs separated by semi-colons (example `-p prop1=value1;prop2=value2`). If both options (-p) and (-q) are set, (-q) is ignored.") + .withRequiredArg() + .ofType(String.class) + .describedAs("inline properties"); + + // Transformation template inline properties option + optionParser.accepts(CLI_OPTION_PROPERTIES_FILE, "Transformation specific properties, used to determine if certain operations should be skipped or not, or how certain aspects of the transformation should be executed. Use it pointing to a '.properties' file. If both options (-p) and (-q) are set, (-q) is ignored.") + .withRequiredArg() + .ofType(File.class) + .describedAs("properties file"); } - public static void setOptionSet(String... args) { + static void setOptionSet(String... args) { optionSet = optionParser.parse(args); } diff --git a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliRun.java b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliRun.java index 819efcf8..5834d73a 100644 --- a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliRun.java +++ b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliRun.java @@ -1,7 +1,12 @@ package com.paypal.butterfly.cli; import java.io.File; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.paypal.butterfly.api.TransformationResult; /** * This is just a POJO that represents an execution of @@ -11,118 +16,79 @@ * * @author facarvalho */ -public class ButterflyCliRun { +class ButterflyCliRun { private String butterflyVersion; - private String[] inputArguments; - - private String application; - - private String transformationTemplate; - private int exitStatus; - - private String transformedApplication; - - private String logFile; - - private String manualInstructionsFile; - - // TODO - // Metrics are not first class citizen yet - // They work as an opt-in feature - private String metricsFile; - + private File logFile; private String errorMessage; - private String exceptionMessage; + private TransformationResult transformationResult; + private List extensions = new ArrayList<>(); - public void setButterflyVersion(String butterflyVersion) { + void setButterflyVersion(String butterflyVersion) { this.butterflyVersion = butterflyVersion; } - public void setInputArguments(String[] inputArguments) { + void setInputArguments(String[] inputArguments) { this.inputArguments = Arrays.copyOf(inputArguments, inputArguments.length); } - public void setApplication(File application) { - this.application = application.getAbsolutePath(); - } - - public void setTransformationTemplate(String transformationTemplate) { - this.transformationTemplate = transformationTemplate; - } - - public void setExitStatus(int exitStatus) { + void setExitStatus(int exitStatus) { this.exitStatus = exitStatus; } - public void setTransformedApplication(File transformedApplication) { - this.transformedApplication = transformedApplication.getAbsolutePath(); - } - - public void setLogFile(File logFile) { - this.logFile = logFile.getAbsolutePath(); + void setLogFile(File logFile) { + this.logFile = logFile; } - public void setManualInstructionsFile(File manualInstructionsFile) { - this.manualInstructionsFile = manualInstructionsFile.getAbsolutePath(); + void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; } - public void setMetricsFile(File metricsFile) { - this.metricsFile = metricsFile.getAbsolutePath(); + void setExceptionMessage(String exceptionMessage) { + this.exceptionMessage = exceptionMessage; } - public void setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; + void setTransformationResult(TransformationResult transformationResult) { + this.transformationResult = transformationResult; } - public void setExceptionMessage(String exceptionMessage) { - this.exceptionMessage = exceptionMessage; + void addExtensionMetaData(ExtensionMetaData extensionMetaData) { + extensions.add(extensionMetaData); } - public int getExitStatus() { + int getExitStatus() { return exitStatus; } - public String getButterflyVersion() { + String getButterflyVersion() { return butterflyVersion; } - public String[] getInputArguments() { + String[] getInputArguments() { return Arrays.copyOf(inputArguments, inputArguments.length); } - public String getApplication() { - return application; - } - - public String getTransformationTemplate() { - return transformationTemplate; - } - - public String getTransformedApplication() { - return transformedApplication; - } - - public String getLogFile() { + File getLogFile() { return logFile; } - public String getManualInstructionsFile() { - return manualInstructionsFile; + String getErrorMessage() { + return errorMessage; } - public String getMetricsFile() { - return metricsFile; + String getExceptionMessage() { + return exceptionMessage; } - public String getErrorMessage() { - return errorMessage; + TransformationResult getTransformationResult() { + return transformationResult; } - public String getExceptionMessage() { - return exceptionMessage; + List getExtensions() { + return Collections.unmodifiableList(extensions); } + } diff --git a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliRunner.java b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliRunner.java index 8eb82e72..97eac8a6 100644 --- a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliRunner.java +++ b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ButterflyCliRunner.java @@ -1,18 +1,15 @@ package com.paypal.butterfly.cli; +import com.paypal.butterfly.api.ButterflyFacade; +import com.paypal.butterfly.api.Configuration; +import com.paypal.butterfly.api.TransformationResult; import com.paypal.butterfly.cli.logging.LogConfigurator; import com.paypal.butterfly.cli.logging.LogFileDefiner; import com.paypal.butterfly.extensions.api.Extension; import com.paypal.butterfly.extensions.api.TransformationTemplate; -import com.paypal.butterfly.extensions.api.exception.ButterflyException; import com.paypal.butterfly.extensions.api.exception.ButterflyRuntimeException; import com.paypal.butterfly.extensions.api.exception.TemplateResolutionException; -import com.paypal.butterfly.extensions.api.upgrade.UpgradePath; import com.paypal.butterfly.extensions.api.upgrade.UpgradeStep; -import com.paypal.butterfly.facade.ButterflyFacade; -import com.paypal.butterfly.facade.ButterflyProperties; -import com.paypal.butterfly.facade.Configuration; -import com.paypal.butterfly.facade.TransformationResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; @@ -21,8 +18,12 @@ import org.springframework.util.StringUtils; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.StringReader; import java.util.List; +import java.util.Optional; +import java.util.Properties; /** * Butterfly CLI runner @@ -30,7 +31,7 @@ * @author facarvalho */ @Component -public class ButterflyCliRunner extends ButterflyCliOption { +class ButterflyCliRunner extends ButterflyCliOption { @Autowired private LogConfigurator logConfigurator; @@ -40,21 +41,26 @@ public class ButterflyCliRunner extends ButterflyCliOption { private static final Logger logger = LoggerFactory.getLogger(ButterflyCliRunner.class); - public ButterflyCliRun run() throws IOException { + ButterflyCliRun run(File butterflyHome, String butterflyBanner) { ButterflyCliRun run = new ButterflyCliRun(); - Configuration configuration = null; - run.setButterflyVersion(ButterflyProperties.getString("butterfly.version")); + Configuration configuration; + run.setButterflyVersion(butterflyFacade.getButterflyVersion()); - logger.info(ButterflyCliApp.getBanner()); + logger.info(butterflyBanner); if (optionSet == null || optionSet.has(CLI_OPTION_HELP) || (!optionSet.hasOptions() && optionSet.nonOptionArguments() == null)){ logger.info(""); logger.info("Usage:\t butterfly [options] [application folder]"); logger.info(""); logger.info("The following options are available:\n"); - optionParser.printHelpOn(System.out); - run.setExitStatus(0); - return run; + try { + optionParser.printHelpOn(System.out); + run.setExitStatus(0); + return run; + } catch (IOException e) { + registerError(run, "An error occurred when printing help", e); + return run; + } } if (optionSet.has(CLI_OPTION_VERBOSE)) { @@ -65,7 +71,7 @@ public ButterflyCliRun run() throws IOException { if (optionSet.has(CLI_OPTION_DEBUG)) { logConfigurator.setDebugMode(true); logger.info("Debug mode is ON"); - logger.info("Butterfly home: {}", ButterflyCliApp.getButterflyHome()); + logger.info("Butterfly home: {}", butterflyHome); logger.info("JAVA_HOME: {}", System.getenv("JAVA_HOME")); logger.info("java.version: {}", System.getProperty("java.version")); logger.info("java.runtime.version: {}", System.getProperty("java.runtime.version")); @@ -74,11 +80,21 @@ public ButterflyCliRun run() throws IOException { if (optionSet.has(CLI_OPTION_LIST_EXTENSIONS)) { try { - printExtensionsList(butterflyFacade); + if (butterflyFacade.getExtensions().isEmpty()) { + logger.info("There are no registered extensions"); + } else { + logger.info("See registered extensions below (shortcut in parenthesis)"); + for (Extension e : butterflyFacade.getExtensions()) { + ExtensionMetaData extensionMetaData = ExtensionMetaData.newExtensionMetaData(e); + run.addExtensionMetaData(extensionMetaData); + printExtensionMetaData(extensionMetaData); + } + } + run.setExitStatus(0); return run; - } catch (Exception e) { - registerError(run, "An error occurred when listing extensions has occurred", e); + } catch (Throwable e) { + registerError(run, "An error occurred when listing extensions", e); return run; } } @@ -97,7 +113,6 @@ public ButterflyCliRun run() throws IOException { registerError(run, errorMessage); return run; } - run.setApplication(applicationFolder); File transformedApplicationFolder = (File) optionSet.valueOf(CLI_OPTION_TRANSFORMED_APP_FOLDER); @@ -118,11 +133,12 @@ public ButterflyCliRun run() throws IOException { logger.info("Transformation template associated with shortcut {}: {}", shortcut, templateClass.getName()); } else { try { - templateClass = butterflyFacade.automaticResolution(applicationFolder); - if (templateClass == null) { + Optional> resolution = butterflyFacade.automaticResolution(applicationFolder); + if (!resolution.isPresent()) { registerError(run, "No transformation template could be resolved for this application. Specify it explicitly using option -t or -s."); return run; } + templateClass = resolution.get(); logger.info("Transformation template automatically resolved"); } catch (TemplateResolutionException e) { registerError(run, e.getMessage()); @@ -134,159 +150,155 @@ public ButterflyCliRun run() throws IOException { logger.info("-z option has been set, transformed application will be placed into a zip file"); } + // Setting transformation specific properties + Properties properties = new Properties(); + try { + if (optionSet.has(CLI_OPTION_INLINE_PROPERTIES)) { + String inlineProperties = (String) optionSet.valueOf(CLI_OPTION_INLINE_PROPERTIES); + try (StringReader stringReader = new StringReader(inlineProperties.replace(';', '\n'))) { + properties.load(stringReader); + } + } else if (optionSet.has(CLI_OPTION_PROPERTIES_FILE)) { + File propertiesFile = (File) optionSet.valueOf(CLI_OPTION_PROPERTIES_FILE); + try (FileInputStream fileInputStream = new FileInputStream(propertiesFile)) { + properties.load(fileInputStream); + } + } + } catch (Exception e) { + registerError(run, "Error when reading or parsing the specified properties", e); + return run; + } + if (optionSet.has(CLI_OPTION_MODIFY_ORIGINAL_FOLDER)) { - configuration = new Configuration(); + configuration = butterflyFacade.newConfiguration(properties); + } else if (transformedApplicationFolder == null) { + configuration = butterflyFacade.newConfiguration(properties, createZip); } else { - configuration = new Configuration(transformedApplicationFolder, createZip); + configuration = butterflyFacade.newConfiguration(properties, transformedApplicationFolder, createZip); } // Setting extensions log level to DEBUG if(optionSet.has(CLI_OPTION_DEBUG)) { - Extension extension = butterflyFacade.getRegisteredExtension(); - if (extension != null) { + List registeredExtensions = butterflyFacade.getExtensions(); + for(Extension extension : registeredExtensions) { logger.info("Setting DEBUG log level for extension {}", extension.getClass().getName()); logConfigurator.setLoggerLevel(extension.getClass().getPackage().getName(), Level.DEBUG); } } + TransformationResult transformationResult = null; + try { if (templateClass == null) { templateClass = (Class) Class.forName(templateClassName); } - run.setTransformationTemplate(templateClass.getName()); - - TransformationResult transformationResult = null; if (UpgradeStep.class.isAssignableFrom(templateClass)) { Class firstStepClass = (Class) templateClass; String upgradeVersion = (String) optionSet.valueOf(CLI_OPTION_UPGRADE_VERSION); - UpgradePath upgradePath = new UpgradePath(firstStepClass, upgradeVersion); + String originalVersion = firstStepClass.newInstance().getCurrentVersion(); - logger.info("Performing upgrade from version {} to version {} (it might take a few seconds)", upgradePath.getOriginalVersion(), upgradePath.getUpgradeVersion()); - transformationResult = butterflyFacade.transform(applicationFolder, upgradePath, configuration); + logger.info("Performing upgrade from version {} to version {} (it might take a few seconds)", originalVersion, upgradeVersion); + transformationResult = butterflyFacade.transform(applicationFolder, firstStepClass, upgradeVersion, configuration).get(); } else { logger.info("Performing transformation (it might take a few seconds)"); - transformationResult = butterflyFacade.transform(applicationFolder, templateClass, configuration); + transformationResult = butterflyFacade.transform(applicationFolder, templateClass, null, configuration).get(); } - logger.info(""); - logger.info("----------------------------------------------"); - logger.info("Application has been transformed successfully!"); - logger.info("----------------------------------------------"); - logger.info("Transformed application folder: {}", transformationResult.getTransformedApplicationLocation()); - logger.info("Check log file for details: {}", LogFileDefiner.getLogFile()); - run.setTransformedApplication(transformationResult.getTransformedApplicationLocation()); run.setLogFile(LogFileDefiner.getLogFile()); - if (transformationResult.hasManualInstructions()) { + if (transformationResult.isSuccessful()) { logger.info(""); - logger.info(" **************************************************************************************"); - logger.info(" *** THIS APPLICATION REQUIRES POST-TRANSFORMATION MANUAL INSTRUCTIONS"); - logger.info(" *** Read manual instructions document for further details:"); - logger.info(" *** {}", transformationResult.getManualInstructionsFile()); - logger.info(" **************************************************************************************"); - - run.setManualInstructionsFile(transformationResult.getManualInstructionsFile()); + logger.info("----------------------------------------------"); + logger.info("Application has been transformed successfully!"); + logger.info("----------------------------------------------"); + logger.info("Transformed application folder: {}", transformationResult.getTransformedApplicationDir()); + logger.info("Check log file for details: {}", LogFileDefiner.getLogFile().getAbsolutePath()); + + if (transformationResult.hasManualInstructions()) { + logger.info(""); + logger.info(" **************************************************************************************"); + logger.info(" *** THIS APPLICATION REQUIRES POST-TRANSFORMATION MANUAL INSTRUCTIONS"); + logger.info(" *** Read manual instructions document for further details:"); + logger.info(" *** {}", transformationResult.getManualInstructionsFile()); + logger.info(" **************************************************************************************"); + } + logger.info(""); + } else { + transformationAbort(run, transformationResult.getAbortDetails().getAbortMessage()); } - logger.info(""); - } catch (ButterflyException | ButterflyRuntimeException e) { - logger.info(""); - logger.info("--------------------------------------------------------------------------------------------"); - logger.error("*** Transformation has been aborted due to:"); - logger.error("*** {}", e.getMessage()); - logger.info("--------------------------------------------------------------------------------------------"); - logger.info("Check log file for details: {}", LogFileDefiner.getLogFile().getAbsolutePath()); - - run.setErrorMessage("Transformation has been aborted due to: " + e.getMessage()); - run.setExceptionMessage(e.getMessage()); - run.setExitStatus(1); - return run; + } catch (ButterflyRuntimeException e) { + transformationAbort(run, e.getMessage()); } catch (ClassNotFoundException e) { registerError(run, "The specified transformation template class has not been found", e); - return run; } catch (IllegalArgumentException e) { registerError(run, "This transformation request input arguments are invalid", e); - return run; - } catch (Exception e) { - registerError(run, "An unexpected exception happened when processing this transformation request", e); - return run; + } catch (Throwable e) { + registerError(run, "An error happened when processing this transformation request", e); + } finally { + run.setTransformationResult(transformationResult); } return run; } + private void transformationAbort(ButterflyCliRun run, String abortMessage) { + logger.info(""); + logger.info("--------------------------------------------------------------------------------------------"); + logger.error("*** Transformation has been aborted due to:"); + logger.error("*** {}", abortMessage); + logger.info("--------------------------------------------------------------------------------------------"); + logger.info("Check log file for details: {}", LogFileDefiner.getLogFile().getAbsolutePath()); + + run.setErrorMessage("Transformation has been aborted due to: " + abortMessage); + run.setExceptionMessage(abortMessage); + run.setExitStatus(1); + } + private Class getTemplateClass(int shortcut) { - Extension extension = butterflyFacade.getRegisteredExtension(); + List registeredExtensions = butterflyFacade.getExtensions(); - if(extension == null) { + if(registeredExtensions.size() == 0) { logger.info("There are no registered extensions"); return null; } int shortcutCount = 1; - for(Object templateObj : extension.getTemplateClasses().toArray()) { - if (shortcutCount == shortcut) { - return (Class) templateObj; + for(Extension extension : registeredExtensions) { + for (Object templateObj : extension.getTemplateClasses()) { + if (shortcutCount == shortcut) { + return (Class) templateObj; + } + shortcutCount++; } - shortcutCount++; } return null; } - private static void printExtensionsList(ButterflyFacade butterflyFacade) throws IllegalAccessException, InstantiationException { - Extension extension = butterflyFacade.getRegisteredExtension(); - if(extension == null) { - logger.info("There are no registered extensions"); - return; - } - - logger.info("See registered extensions below (shortcut in parenthesis)"); - - Class template; - int shortcut = 1; - - String version = (StringUtils.isEmpty(extension.getVersion()) ? "" : String.format("(version %s)", extension.getVersion())); + private void printExtensionMetaData(ExtensionMetaData extensionMetaData) { + String version = (StringUtils.isEmpty(extensionMetaData.getVersion()) ? "" : String.format("(version %s)", extensionMetaData.getVersion())); logger.info(""); - logger.info("- {}: {} {}", extension, extension.getDescription(), version); - for(Object templateObj : extension.getTemplateClasses().toArray()) { - template = (Class) templateObj; - logger.info("\t ({}) - [{}] \t {} \t {}", shortcut++, ExtensionTypeInitial.getFromClass(template), template.getName(), template.newInstance().getDescription()); - - } + logger.info("- {}: {} {}", extensionMetaData.getName(), extensionMetaData.getDescription(), version); + extensionMetaData.getTemplates().forEach(t -> logger.info("\t ({}) - [{}] \t {} \t {}", t.getShortcut(), t.getTemplateType().getInitials(), t.getName(), t.getDescription())); } private void registerError(ButterflyCliRun run, String errorMessage) { registerError(run, errorMessage, null); } - private void registerError(ButterflyCliRun run, String errorMessage, Exception exception) { - if (exception == null || !logConfigurator.isVerboseMode()) { + private void registerError(ButterflyCliRun run, String errorMessage, Throwable throwable) { + if (throwable == null || !logConfigurator.isVerboseMode()) { logger.error(errorMessage); } else { - logger.error(errorMessage, exception); + logger.error(errorMessage, throwable); } - if (exception != null) { - run.setExceptionMessage(exception.getMessage()); + if (throwable != null) { + run.setExceptionMessage(throwable.getMessage()); } run.setErrorMessage(errorMessage); run.setExitStatus(1); } - private enum ExtensionTypeInitial { - TT, US, VC; - - public static ExtensionTypeInitial getFromClass(Class template) { - if(UpgradeStep.class.isAssignableFrom(template)) return US; - - if(TransformationTemplate.class.isAssignableFrom(template)) return TT; - - // TODO -// if(Validation.class.isAssignableFrom(template)) return VC; - - throw new IllegalArgumentException("Class " + template.getName() + " is not recognized as an extension type"); - } - } - } \ No newline at end of file diff --git a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ExtensionMetaData.java b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ExtensionMetaData.java new file mode 100644 index 00000000..78bb7e37 --- /dev/null +++ b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/ExtensionMetaData.java @@ -0,0 +1,86 @@ +package com.paypal.butterfly.cli; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import com.paypal.butterfly.extensions.api.Extension; +import com.paypal.butterfly.extensions.api.TransformationTemplate; + +/** + * This is just a POJO that represents a Butterfly Extension. + * Its purpose is to facilitate serializing a the extension for UI purposes. + * + * @author facarvalho, mmcrockett + */ +class ExtensionMetaData { + + private String name; + private String description; + private String version; + private List templates = new ArrayList<>(); + + private static transient AtomicInteger shortcut = new AtomicInteger(1); + + private ExtensionMetaData() { + } + + static ExtensionMetaData newExtensionMetaData(Extension extension) throws IllegalAccessException, InstantiationException { + ExtensionMetaData extensionMetaData = new ExtensionMetaData(); + extensionMetaData.name = extension.toString(); + extensionMetaData.description = extension.getDescription(); + extensionMetaData.version = extension.getVersion(); + + for(Object templateObj : extension.getTemplateClasses().toArray()) { + Class template = (Class) templateObj; + extensionMetaData.addTemplate(template, shortcut.getAndIncrement()); + } + + return extensionMetaData; + } + + private TemplateMetaData addTemplate(Class transformationTemplateClass, int shortcut) throws InstantiationException, IllegalAccessException { + TemplateMetaData templateMetaData = TemplateMetaData.newTemplateMetaData(this, transformationTemplateClass, shortcut); + templates.add(templateMetaData); + + return templateMetaData; + } + + String getName() { + return name; + } + + String getDescription() { + return description; + } + + String getVersion() { + return version; + } + + /** + * Returns an unmodifiable list of all transformation template metadata + * registered to this extension metadata + * + * @return an unmodifiable list of all transformation template metadata + */ + List getTemplates() { + return Collections.unmodifiableList(templates); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ExtensionMetaData)) return false; + + ExtensionMetaData extensionMetaData = (ExtensionMetaData) obj; + if (!Objects.equals(extensionMetaData.name, this.name)) return false; + if (!Objects.equals(extensionMetaData.description, this.description)) return false; + if (!Objects.equals(extensionMetaData.version, this.version)) return false; + + return Objects.equals(extensionMetaData.templates, this.templates); + } + +} \ No newline at end of file diff --git a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/TemplateMetaData.java b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/TemplateMetaData.java new file mode 100644 index 00000000..b9baa7c7 --- /dev/null +++ b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/TemplateMetaData.java @@ -0,0 +1,98 @@ +package com.paypal.butterfly.cli; + +import com.paypal.butterfly.extensions.api.TransformationTemplate; +import com.paypal.butterfly.extensions.api.upgrade.UpgradeStep; + +import java.util.Objects; + +/** + * This is just a POJO that represents a Template of a Butterfly extension. + * Its purpose is to facilitate serializing + * a transformation template for UI purposes. + * + * @author facarvalho, mmcrockett + */ +class TemplateMetaData { + + private transient ExtensionMetaData extensionMetaData; + private String name; + private String className; + private TemplateType templateType; + private String description; + private String upgradeFromVersion; + private String upgradeToVersion; + private int shortcut; + + private TemplateMetaData() { + } + + static TemplateMetaData newTemplateMetaData(ExtensionMetaData extensionMetaData, Class transformationTemplateClass, int shortcut) throws IllegalAccessException, InstantiationException { + TemplateMetaData templateMetaData = new TemplateMetaData(); + + templateMetaData.extensionMetaData = extensionMetaData; + templateMetaData.className = transformationTemplateClass.getName(); + templateMetaData.shortcut = shortcut; + templateMetaData.templateType = TemplateType.getFromClass(transformationTemplateClass); + + TransformationTemplate transformationTemplate = transformationTemplateClass.newInstance(); + templateMetaData.name = transformationTemplate.getName(); + templateMetaData.description = transformationTemplate.getDescription(); + + if (transformationTemplate instanceof UpgradeStep) { + UpgradeStep upgradeStep = (UpgradeStep) transformationTemplate; + templateMetaData.upgradeFromVersion = upgradeStep.getCurrentVersion(); + templateMetaData.upgradeToVersion = upgradeStep.getNextVersion(); + } + + return templateMetaData; + } + + ExtensionMetaData getExtensionMetaData() { + return extensionMetaData; + } + + String getName() { + return name; + } + + String getClassName() { + return className; + } + + TemplateType getTemplateType() { + return templateType; + } + + String getDescription() { + return description; + } + + String getUpgradeFromVersion() { + return upgradeFromVersion; + } + + String getUpgradeToVersion() { + return upgradeToVersion; + } + + int getShortcut() { + return shortcut; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof TemplateMetaData)) return false; + + TemplateMetaData templateMetaData = (TemplateMetaData) obj; + if (!Objects.equals(templateMetaData.name, this.name)) return false; + if (!Objects.equals(templateMetaData.className, this.className)) return false; + if (!Objects.equals(templateMetaData.templateType, this.templateType)) return false; + if (!Objects.equals(templateMetaData.description, this.description)) return false; + if (!Objects.equals(templateMetaData.upgradeFromVersion, this.upgradeFromVersion)) return false; + if (!Objects.equals(templateMetaData.upgradeToVersion, this.upgradeToVersion)) return false; + + return templateMetaData.shortcut == this.shortcut; + } + +} \ No newline at end of file diff --git a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/TemplateType.java b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/TemplateType.java new file mode 100644 index 00000000..89dfb1c9 --- /dev/null +++ b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/TemplateType.java @@ -0,0 +1,37 @@ +package com.paypal.butterfly.cli; + + +import com.paypal.butterfly.extensions.api.TransformationTemplate; +import com.paypal.butterfly.extensions.api.upgrade.UpgradeStep; + +/** + * Type enumeration for {@link TransformationTemplate} + * @author facarvalho + */ +enum TemplateType { + + TransformationTemplate("TT"), UpgradeStep("US"); + + private final String initials; + + TemplateType(String initials) { + this.initials = initials; + } + + /** + * Return a String representing a short version identifier for this template type, + * useful for displaying a list of transformation templates + * + * @return a String representing a short version identifier for this template type + */ + String getInitials() { + return initials; + } + + static TemplateType getFromClass(Class template) { + if(UpgradeStep.class.isAssignableFrom(template)) return UpgradeStep; + if(TransformationTemplate.class.isAssignableFrom(template)) return TransformationTemplate; + throw new IllegalArgumentException("Class " + template.getName() + " is not recognized as an extension type"); + } + +} diff --git a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/logging/LogFileDefiner.java b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/logging/LogFileDefiner.java index 11cab54b..ddea82ca 100644 --- a/butterfly-cli/src/main/java/com/paypal/butterfly/cli/logging/LogFileDefiner.java +++ b/butterfly-cli/src/main/java/com/paypal/butterfly/cli/logging/LogFileDefiner.java @@ -3,7 +3,6 @@ import ch.qos.logback.core.Context; import ch.qos.logback.core.spi.PropertyDefiner; import ch.qos.logback.core.status.Status; -import com.paypal.butterfly.cli.ButterflyCliApp; import java.io.File; import java.text.SimpleDateFormat; @@ -22,8 +21,13 @@ public class LogFileDefiner implements PropertyDefiner { private static String logFileName = DEFAULT_LOG_FILE_NAME; private static boolean customLogFileNameSet = false; + private static File butterflyHome; private static File logFile; + public static void setButterflyHome(File butterflyHome) { + LogFileDefiner.butterflyHome = butterflyHome; + } + public static void setLogFileName(File applicationFolder, boolean debug) { if (!customLogFileNameSet && applicationFolder != null) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS"); @@ -33,7 +37,7 @@ public static void setLogFileName(File applicationFolder, boolean debug) { } private static void setLogFile() { - logFile = new File(ButterflyCliApp.getButterflyHome(), "logs" + File.separator + logFileName); + logFile = new File(butterflyHome, "logs" + File.separator + logFileName); } public static File getLogFile() { diff --git a/butterfly-cli/src/test/java/com/paypal/butterfly/cli/ButterflyCliTest.java b/butterfly-cli/src/test/java/com/paypal/butterfly/cli/ButterflyCliTest.java deleted file mode 100644 index b4a7697d..00000000 --- a/butterfly-cli/src/test/java/com/paypal/butterfly/cli/ButterflyCliTest.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.paypal.butterfly.cli; - -import com.paypal.butterfly.cli.logging.LogConfigurator; -import com.paypal.butterfly.extensions.api.exception.ButterflyException; -import com.paypal.butterfly.extensions.api.upgrade.UpgradePath; -import com.paypal.butterfly.facade.ButterflyFacade; -import com.paypal.butterfly.facade.Configuration; -import com.paypal.butterfly.facade.TransformationResult; -import com.test.SampleExtension; -import com.test.SampleTransformationTemplate; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.powermock.modules.testng.PowerMockTestCase; -import org.testng.Assert; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import java.io.File; -import java.io.IOException; - -import static org.mockito.Mockito.*; - -/** - * Butterfly Command Line Interface test - * - * @author facarvalho - */ -public class ButterflyCliTest extends PowerMockTestCase { - - @InjectMocks - private ButterflyCliRunner butterflyCli; - - @Mock - private ButterflyFacade facade; - - // Even though this variable is not used explicitly in this class, - // it is necessary to its proper execution, since the mock initialization - // is happening regardless of it - @SuppressWarnings("PMD.UnusedPrivateField") - @Mock - private LogConfigurator logConfigurator; - - private File sampleAppFolder; - - @BeforeMethod - public void beforeTest() throws ButterflyException { - TransformationResult mockResult = mock(TransformationResult.class); - - when(facade.transform(any(File.class), any(String.class))).thenReturn(mockResult); - when(facade.transform(any(File.class), any(String.class), any(Configuration.class))).thenReturn(mockResult); - when(facade.transform(any(File.class), any(Class.class))).thenReturn(mockResult); - when(facade.transform(any(File.class), any(Class.class), any(Configuration.class))).thenReturn(mockResult); - when(facade.transform(any(File.class), any(UpgradePath.class))).thenReturn(mockResult); - when(facade.transform(any(File.class), any(UpgradePath.class), any(Configuration.class))).thenReturn(mockResult); - - File file = new File(""); - when(mockResult.getTransformedApplicationLocation()).thenReturn(file); - when(mockResult.getManualInstructionsFile()).thenReturn(file); - - sampleAppFolder = new File(this.getClass().getResource("/sample_app").getFile()); - } - - /** - * To Test listing of extensions - * - * @throws IOException - */ - @Test - public void testListingExtensions() throws IOException { - Assert.assertNotNull(butterflyCli); - Assert.assertNotNull(facade); - - String[] arguments = {"-l", "-v"}; - butterflyCli.setOptionSet(arguments); - - int status = butterflyCli.run().getExitStatus(); - - Assert.assertEquals(status, 0); - verify(facade, times(1)).getRegisteredExtension(); - } - - /** - * To Test Transformation with -t, -v and -z options - * - * @throws IOException - * @throws ButterflyException - */ - @Test - public void testTransformation() throws IOException, ButterflyException { - String arguments[] = {sampleAppFolder.getAbsolutePath(), "-t", "com.test.SampleTransformationTemplate", "-v", "-z"}; - butterflyCli.setOptionSet(arguments); - int status = butterflyCli.run().getExitStatus(); - - Assert.assertEquals(status, 0); - verify(facade, times(1)).transform(eq(sampleAppFolder), eq(SampleTransformationTemplate.class), eq(new Configuration(null, true))); - } - - /** - * To Test Transformation with -s options - * - * @throws IOException - * @throws ButterflyException - */ - @Test - public void testTransformationWithShortcut() throws IOException, ButterflyException { - when(facade.getRegisteredExtension()).thenReturn(new SampleExtension()); - - String arguments[] = {sampleAppFolder.getAbsolutePath(), "-s", "2"}; - butterflyCli.setOptionSet(arguments); - int status = butterflyCli.run().getExitStatus(); - - Assert.assertEquals(status, 0); - verify(facade, times(1)).transform(eq(sampleAppFolder), eq(SampleTransformationTemplate.class), eq(new Configuration(null, false))); - } - - /** - * To Test Transformation with -t and -s options. - * Option -s should be ignored, since -t was also provided - * - * @throws IOException - * @throws ButterflyException - */ - @Test - public void testTransformationWithShortcutButIgnoringIt() throws IOException, ButterflyException { - when(facade.getRegisteredExtension()).thenReturn(new SampleExtension()); - - String arguments[] = {sampleAppFolder.getAbsolutePath(), "-t", "com.test.SampleTransformationTemplate", "-z", "-s", "2"}; - butterflyCli.setOptionSet(arguments); - int status = butterflyCli.run().getExitStatus(); - - Assert.assertEquals(status, 0); - verify(facade, times(1)).transform(eq(sampleAppFolder), eq(SampleTransformationTemplate.class), eq(new Configuration(null, true))); - verify(facade, times(0)).getRegisteredExtension(); - } - - /** - * To Test without using any option, so that help option would be used - * - * @throws IOException - */ - @Test - public void testNoOptions() throws IOException { - int status = butterflyCli.run().getExitStatus(); - Assert.assertEquals(status, 0); - } - - /** - * To Test the case where exception is expected when non existing directory is being used as output directory - * - * @throws IOException - * @throws ButterflyException - */ - @Test(expectedExceptions = IllegalArgumentException.class) - public void testTransformationWithNonExistDir() throws IOException, ButterflyException { - String arguments[] = {sampleAppFolder.getAbsolutePath(), "-t", "com.test.SampleTransformationTemplate", "-v", "-o", "PATH_TO_OUTPUT_FOLDER"}; - butterflyCli.setOptionSet(arguments); - butterflyCli.run(); - } - - /** - * To test Transformation with the output directory that actually should exist. Hence current directory is being used as an output directory - * - * @throws IOException - * @throws ButterflyException - */ - @Test - public void testTransformationWithValidOutPutDir() throws IOException, ButterflyException { - String currentDir = System.getProperty("user.dir"); - String arguments[] = {sampleAppFolder.getAbsolutePath(), "-t", "com.test.SampleTransformationTemplate", "-v", "-o", currentDir}; - butterflyCli.setOptionSet(arguments); - int status = butterflyCli.run().getExitStatus(); - - Assert.assertEquals(status, 0); - verify(facade, times(1)).transform(eq(sampleAppFolder), eq(SampleTransformationTemplate.class), eq(new Configuration(new File(currentDir), false))); - } - - @Test - public void testAutomaticResolution() throws IOException, ButterflyException { - doReturn(SampleTransformationTemplate.class).when(facade).automaticResolution(any(File.class)); - String arguments[] = {sampleAppFolder.getAbsolutePath()}; - butterflyCli.setOptionSet(arguments); - int status = butterflyCli.run().getExitStatus(); - - verify(facade, times(1)).automaticResolution(eq(sampleAppFolder)); - verify(facade, times(1)).transform(eq(sampleAppFolder), eq(SampleTransformationTemplate.class), eq(new Configuration(null, false))); - Assert.assertEquals(status, 0); - } - - @Test - public void testAutomaticResolutionFailed() throws IOException, ButterflyException { - String arguments[] = {sampleAppFolder.getAbsolutePath()}; - butterflyCli.setOptionSet(arguments); - int status = butterflyCli.run().getExitStatus(); - - verify(facade, times(1)).automaticResolution(eq(sampleAppFolder)); - verify(facade, times(0)).transform(eq(sampleAppFolder), eq(SampleTransformationTemplate.class), eq(new Configuration(null, false))); - Assert.assertEquals(status, 1); - } - - @Test - public void testUnexistentApplicationFolder() throws IOException, ButterflyException { - String arguments[] = {"unexistent_folder"}; - butterflyCli.setOptionSet(arguments); - int status = butterflyCli.run().getExitStatus(); - - verify(facade, times(0)).automaticResolution(eq(sampleAppFolder)); - verify(facade, times(0)).transform(eq(sampleAppFolder), eq(SampleTransformationTemplate.class), eq(new Configuration(null, false))); - Assert.assertEquals(status, 1); - } - - @Test - public void testTransformationWithInPlaceOutput() throws IOException, ButterflyException { - String currentDir = System.getProperty("user.dir"); - String arguments[] = {sampleAppFolder.getAbsolutePath(), "-t", "com.test.SampleTransformationTemplate", "-v", "-f"}; - butterflyCli.setOptionSet(arguments); - int status = butterflyCli.run().getExitStatus(); - - Assert.assertEquals(status, 0); - verify(facade, times(1)).transform(eq(sampleAppFolder), eq(SampleTransformationTemplate.class), eq(new Configuration())); - } -} \ No newline at end of file diff --git a/butterfly-cli/src/test/java/com/paypal/butterfly/cli/MiscIT.java b/butterfly-cli/src/test/java/com/paypal/butterfly/cli/MiscIT.java new file mode 100644 index 00000000..13ffc619 --- /dev/null +++ b/butterfly-cli/src/test/java/com/paypal/butterfly/cli/MiscIT.java @@ -0,0 +1,109 @@ +package com.paypal.butterfly.cli; + +import org.apache.commons.io.FileUtils; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.net.URISyntaxException; +import java.util.Collections; + +import static org.testng.Assert.*; + +/** + * Integration tests for Butterfly CLI when running non-transformation options + * + * IMPORTANT: this test requires running separately from {@link TransformIT}, + * since both tests rely on static members in the Butterfly CLI project. + * That is natural and acceptable, since the CLI is supposed to be only + * single-threaded and executed as a batch operation. In order to guarantee + * that during integration tests, Gradle has been configured + * (using test {forkEvery = 1}) to use one JVM per test class. + * Having said that, whenever running Butterfly CLI tests from an IDE, make + * sure you don't run them all under the same JVM. You can easily do so by + * running each integration test class individually. + * + * @author facarvalho + */ +public class MiscIT { + + @Test + public void helpTest() throws IOException, URISyntaxException { + + ButterflyCliRun run = new ButterflyCliApp().run(); + assertEquals(run.getExitStatus(), 0); + + // Ensuring run metadata is correct + assertEquals(run.getInputArguments(), new String[]{}); + assertEquals(run.getButterflyVersion(), "TEST"); + assertNull(run.getErrorMessage()); + assertNull(run.getExceptionMessage()); + assertEquals(run.getExtensions(), Collections.emptyList()); + assertNull(run.getLogFile()); + + // Capturing the console output + PrintStream systemOut = System.out; + File helpOut = File.createTempFile("butterfly-cli-help-output", null); + PrintStream helpStream = new PrintStream(helpOut); + System.setOut(helpStream); + + // Running help option three times (in different ways) to capture console output + assertEquals(new ButterflyCliApp().run().getExitStatus(), 0); + assertEquals(new ButterflyCliApp().run("-h").getExitStatus(), 0); + assertEquals(new ButterflyCliApp().run("-?").getExitStatus(), 0); + + // Closing captured console output stream, and restoring original system out + helpStream.close(); + System.setOut(systemOut); + + // Ensuring console output is as expected + File helpBaselineOut = new File(this.getClass().getResource("/helpOut.txt").toURI()); + assertTrue(FileUtils.contentEquals(helpBaselineOut, helpOut), "Generated help differs from test baseline\nTest baseline: " + helpBaselineOut + "\nGenerated result: " + helpOut + "\n"); + } + + @Test(dependsOnMethods = "helpTest") + public void extensionsListTest() throws IOException, URISyntaxException { + + // Capturing the console output + PrintStream systemOut = System.out; + File listOut = File.createTempFile("butterfly-cli-list-output", null); + PrintStream listStream = new PrintStream(listOut); + System.setOut(listStream); + + ButterflyCliRun run = new ButterflyCliApp().run("-l", "-r", "out/result-list.json"); + + // Closing captured console output stream, and restoring original system out + listStream.close(); + System.setOut(systemOut); + + // Ensuring run metadata is correct + assertEquals(run.getExitStatus(), 0, run.getExceptionMessage()); + assertEquals(run.getInputArguments(), new String[]{"-l", "-r", "out/result-list.json"}); + assertEquals(run.getButterflyVersion(), "TEST"); + assertNull(run.getErrorMessage()); + assertNull(run.getExceptionMessage()); + assertEquals(run.getExtensions().size(), 2); + assertNull(run.getLogFile()); + + // Ensuring console output is as expected + File listBaselineOut = new File(this.getClass().getResource("/extensionsListOut.txt").toURI()); + assertTrue(FileUtils.contentEquals(listBaselineOut, listOut), "Generated extensions list differs from test baseline\nTest baseline: " + listBaselineOut + "\nGenerated result: " + listOut + "\n"); + + // Ensuring result JSON file is as expected + jsonResultTest(run); + } + + private void jsonResultTest(ButterflyCliRun run) throws IOException, URISyntaxException { + File resultFile = new File(System.getProperty("user.dir"), "out/result-list.json"); + + assertTrue(resultFile.exists()); + assertTrue(resultFile.isFile()); + assertTrue(resultFile.length() > 0); + assertEquals(resultFile.getName(), "result-list.json"); + + File baselineResult = new File(this.getClass().getResource("/result-list.json").toURI()); + assertTrue(FileUtils.contentEquals(baselineResult, resultFile), "Generated JSON result differs from test baseline\nTest baseline: " + baselineResult + "\nGenerated result: " + resultFile + "\n"); + } + +} \ No newline at end of file diff --git a/butterfly-cli/src/test/java/com/paypal/butterfly/cli/SimpleInterpolator.java b/butterfly-cli/src/test/java/com/paypal/butterfly/cli/SimpleInterpolator.java new file mode 100644 index 00000000..14c0dac2 --- /dev/null +++ b/butterfly-cli/src/test/java/com/paypal/butterfly/cli/SimpleInterpolator.java @@ -0,0 +1,51 @@ +package com.paypal.butterfly.cli; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.attribute.FileAttribute; +import java.util.Map; + +/** + * Helper class used by integration tests to generate + * temporary files usually used as baseline for comparisons. + * These files are created based on a template file and values + * to be interpolated over the template. + * + * @author facarvalho + */ +abstract class SimpleInterpolator { + + /** + * Given a template file, and a map, returns a new temporary file. Its content is generated out of the template + * and interpolated values based on the map object, whose keys are characters used as placeholders (having % as predecessor) in the template file. + * + * @param templateFile the template file, containing content to be preserved, plus interpolation placeholders, marked as % followed by one character + * @param values a map whose keys are interpolation placeholder, and values are the values to be placed in the generated file + * @return the generated file + * @throws IOException if anything goes wrong when reading template file, or creating and writing the interpolated file + */ + static File generate(File templateFile, Map values) throws IOException { + File baseline = Files.createTempFile("butterfly-interpolated-file-", null, new FileAttribute[]{}).toFile(); + + try (Reader templateReader = new BufferedReader(new FileReader(templateFile)); + Writer baselineWriter = new BufferedWriter(new FileWriter(baseline)) + ) { + int b; + boolean token = false; + while ((b = templateReader.read()) != -1) { + char c = (char) b; + if (!token && c != '%') { + baselineWriter.write(c); + } else if (!token && c == '%') { + token = true; + } else if (token && values.containsKey(c)) { + baselineWriter.write(values.get(c)); + token = false; + } + } + } + + return baseline; + } + +} diff --git a/butterfly-cli/src/test/java/com/paypal/butterfly/cli/TransformIT.java b/butterfly-cli/src/test/java/com/paypal/butterfly/cli/TransformIT.java new file mode 100644 index 00000000..878fd0c1 --- /dev/null +++ b/butterfly-cli/src/test/java/com/paypal/butterfly/cli/TransformIT.java @@ -0,0 +1,133 @@ +package com.paypal.butterfly.cli; + +import com.test.BlankTemplate; +import org.apache.commons.io.FileUtils; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.attribute.FileAttribute; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.testng.Assert.*; + +/** + * Integration tests for Butterfly CLI when running a transformation. + * + * IMPORTANT: this test requires running separately from {@link MiscIT}, + * since both tests rely on static members in the Butterfly CLI project. + * That is natural and acceptable, since the CLI is supposed to be only + * single-threaded and executed as a batch operation. In order to guarantee + * that during integration tests, Gradle has been configured + * (using test {forkEvery = 1}) to use one JVM per test class. + * Having said that, whenever running Butterfly CLI tests from an IDE, make + * sure you don't run them all under the same JVM. You can easily do so by + * running each integration test class individually. + * + * @author facarvalho + */ +public class TransformIT { + + // TODO Add a tests for: verbose, debug, zip, same folder, custom output folder + + @Test + public void transformTest() throws IOException, URISyntaxException { + + // Capturing the console output + PrintStream systemOut = System.out; + File transformOut = File.createTempFile("butterfly-cli-transform-output-", null); + PrintStream transformStream = new PrintStream(transformOut); + System.setOut(transformStream); + + // Running a dummy transformation + String sampleAppPath = new File(this.getClass().getResource("/sample_app").toURI()).getAbsolutePath(); + ButterflyCliRun run = new ButterflyCliApp().run(sampleAppPath, "-r", "out/result-transform.json"); + + // Closing captured console output stream, and restoring original system out + transformStream.close(); + System.setOut(systemOut); + + // Ensuring transformation metadata is correct + assertEquals(run.getInputArguments(), new String[]{sampleAppPath, "-r", "out/result-transform.json"}); + assertEquals(run.getExitStatus(), 0, "Transformation failed: " + run.getErrorMessage()); + assertEquals(run.getButterflyVersion(), "TEST"); + assertNull(run.getErrorMessage()); + assertNull(run.getExceptionMessage()); + assertEquals(run.getExtensions(), Collections.emptyList()); + + // Ensuring console output, logfile and result JSON file are as expected + consoleOutputTest(run, transformOut); + logFileTest(run); + jsonResultTest(run); + } + + @Test(dependsOnMethods = "transformTest") + public void transformSameFolderBlankTemplateTest() throws IOException, URISyntaxException { + File sampleApp = new File(this.getClass().getResource("/sample_app").toURI()); + + File sampleAppCopy = Files.createTempDirectory("butterfly-blank-test-app", new FileAttribute[]{}).toFile(); + FileUtils.copyDirectory(sampleApp, sampleAppCopy); + + File testFile = new File(this.getClass().getResource("/butterfly.properties").toURI()); + ButterflyCliRun run = new ButterflyCliApp().run(sampleAppCopy.getAbsolutePath(), "-f", "-t", BlankTemplate.class.getName()); + + assertEquals(run.getExitStatus(), 0); + assertEquals(sampleAppCopy.listFiles().length, 1); + assertEquals(sampleAppCopy.listFiles()[0].getName(), testFile.getName()); + assertTrue(FileUtils.contentEquals(sampleAppCopy.listFiles()[0], testFile)); + } + + private void consoleOutputTest(ButterflyCliRun run, File transformOut) throws URISyntaxException, IOException { + Map baselineTransformValues = new HashMap<>(); + baselineTransformValues.put('1', run.getTransformationResult().getTransformedApplicationDir().getAbsolutePath()); + baselineTransformValues.put('2', run.getLogFile().getAbsolutePath()); + + File templateFile = new File(this.getClass().getResource("/transformOut.txt").toURI()); + File baselineTransform = SimpleInterpolator.generate(templateFile, baselineTransformValues); + assertTrue(FileUtils.contentEquals(baselineTransform, transformOut), "Generated output differs from test baseline\nTest baseline: " + baselineTransform + "\nGenerated output: " + transformOut + "\n"); + } + + private void logFileTest(ButterflyCliRun run) { + File logFile = run.getLogFile(); + + assertNotNull(run.getLogFile()); + assertTrue(logFile.exists()); + assertTrue(logFile.isFile()); + assertTrue(logFile.length() > 0); + assertEquals(logFile.getParent(), new File(System.getProperty("user.dir"), "logs").getAbsolutePath()); + assertTrue(logFile.getName().matches("sample_app_\\d*\\.log")); + } + + private void jsonResultTest(ButterflyCliRun run) throws IOException, URISyntaxException { + File resultFile = new File(System.getProperty("user.dir"), "out/result-transform.json"); + + assertTrue(resultFile.exists()); + assertTrue(resultFile.isFile()); + assertTrue(resultFile.length() > 0); + assertEquals(resultFile.getName(), "result-transform.json"); + + Map baselineResultValues = new HashMap<>(); + baselineResultValues.put('a', run.getLogFile().getAbsolutePath()); + baselineResultValues.put('b', run.getTransformationResult().getId()); + baselineResultValues.put('c', run.getTransformationResult().getTransformationRequest().getId()); + baselineResultValues.put('d', String.valueOf(run.getTransformationResult().getTransformationRequest().getTimestamp())); + baselineResultValues.put('e', run.getTransformationResult().getTransformationRequest().getDateTime()); + baselineResultValues.put('f', String.valueOf(run.getTransformationResult().getTimestamp())); + baselineResultValues.put('g', run.getTransformationResult().getDateTime()); + baselineResultValues.put('h', run.getTransformationResult().getTransformedApplicationDir().getAbsolutePath()); + baselineResultValues.put('i', run.getTransformationResult().getMetrics().get(0).getDateTime()); + baselineResultValues.put('j', String.valueOf(run.getTransformationResult().getMetrics().get(0).getTimestamp())); + baselineResultValues.put('l', String.valueOf(run.getTransformationResult().getTransformationRequest().getApplication().getFolder().getAbsolutePath())); + baselineResultValues.put('m', String.valueOf(System.getProperty("user.name"))); + + File templateFile = new File(this.getClass().getResource("/result-transform.json").toURI()); + File baselineResult = SimpleInterpolator.generate(templateFile, baselineResultValues); + assertTrue(FileUtils.contentEquals(baselineResult, resultFile), "Generated JSON result differs from test baseline\nTest baseline: " + baselineResult + "\nGenerated result: " + resultFile + "\n"); + } + +} \ No newline at end of file diff --git a/butterfly-cli/src/test/java/com/test/BlankTemplate.java b/butterfly-cli/src/test/java/com/test/BlankTemplate.java new file mode 100644 index 00000000..bdaa07eb --- /dev/null +++ b/butterfly-cli/src/test/java/com/test/BlankTemplate.java @@ -0,0 +1,31 @@ +package com.test; + +import com.paypal.butterfly.extensions.api.Extension; +import com.paypal.butterfly.extensions.api.TransformationTemplate; +import com.paypal.butterfly.utilities.operations.file.ApplyFile; + +import java.net.URL; + +/** + * @author facarvalho + */ +public class BlankTemplate extends TransformationTemplate { + + public BlankTemplate() { + setBlank(true); + + URL fileUrl = this.getClass().getResource("/butterfly.properties"); + add(new ApplyFile().setFileUrl(fileUrl).relative("")); + } + + @Override + public Class getExtensionClass() { + return SampleExtension1.class; + } + + @Override + public String getDescription() { + return "BlankTemplate for tests purposes"; + } + +} diff --git a/butterfly-cli/src/test/java/com/test/SampleTransformationTemplate.java b/butterfly-cli/src/test/java/com/test/DummyTemplate.java similarity index 75% rename from butterfly-cli/src/test/java/com/test/SampleTransformationTemplate.java rename to butterfly-cli/src/test/java/com/test/DummyTemplate.java index 79c1d8a1..d23abd8c 100644 --- a/butterfly-cli/src/test/java/com/test/SampleTransformationTemplate.java +++ b/butterfly-cli/src/test/java/com/test/DummyTemplate.java @@ -6,16 +6,16 @@ /** * @author facarvalho */ -public class SampleTransformationTemplate extends TransformationTemplate { +public class DummyTemplate extends TransformationTemplate { @Override public Class getExtensionClass() { - return SampleExtension.class; + return SampleExtension2.class; } @Override public String getDescription() { - return null; + return "DummyTemplate for tests purposes"; } @Override @@ -27,5 +27,4 @@ public String getApplicationType() { public String getApplicationName() { return null; } - -} +} \ No newline at end of file diff --git a/butterfly-cli/src/test/java/com/test/SampleExtension.java b/butterfly-cli/src/test/java/com/test/SampleExtension.java deleted file mode 100644 index b7fea376..00000000 --- a/butterfly-cli/src/test/java/com/test/SampleExtension.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.test; - -import com.paypal.butterfly.extensions.api.Extension; -import com.paypal.butterfly.extensions.api.TransformationTemplate; - -import java.io.File; - -/** - * @author facarvalho - */ -public class SampleExtension extends Extension { - - public SampleExtension() { - add(DummyTransformationTemplate.class); - add(SampleTransformationTemplate.class); - } - - @Override - public String getDescription() { - return null; - } - - @Override - public String getVersion() { - return null; - } - - @Override - public Class automaticResolution(File applicationFolder) { - return null; - } - -} - -// Adding this class as well just so SampleTransformationTemplate is -// the second to be added, then we can test shortcut as number 2, -// instead of 1, which would be too trivial -class DummyTransformationTemplate extends TransformationTemplate { - - @Override - public Class getExtensionClass() { - return null; - } - - @Override - public String getDescription() { - return null; - } - - @Override - public String getApplicationType() { - return null; - } - - @Override - public String getApplicationName() { - return null; - } -} \ No newline at end of file diff --git a/butterfly-cli/src/test/java/com/test/SampleExtension1.java b/butterfly-cli/src/test/java/com/test/SampleExtension1.java new file mode 100644 index 00000000..11ae9344 --- /dev/null +++ b/butterfly-cli/src/test/java/com/test/SampleExtension1.java @@ -0,0 +1,34 @@ +package com.test; + +import com.paypal.butterfly.extensions.api.Extension; +import com.paypal.butterfly.extensions.api.TransformationTemplate; + +import java.io.File; +import java.util.Optional; + +/** + * @author facarvalho + */ +public class SampleExtension1 extends Extension { + + public SampleExtension1() { + add(SampleTemplate.class); + add(BlankTemplate.class); + } + + @Override + public String getDescription() { + return "SampleExtension1 for tests purposes"; + } + + @Override + public String getVersion() { + return "2.0.0"; + } + + @Override + public Optional> automaticResolution(File applicationFolder) { + return Optional.of(SampleTemplate.class); + } + +} diff --git a/butterfly-cli/src/test/java/com/test/SampleExtension2.java b/butterfly-cli/src/test/java/com/test/SampleExtension2.java new file mode 100644 index 00000000..ae6e72ac --- /dev/null +++ b/butterfly-cli/src/test/java/com/test/SampleExtension2.java @@ -0,0 +1,33 @@ +package com.test; + +import com.paypal.butterfly.extensions.api.Extension; +import com.paypal.butterfly.extensions.api.TransformationTemplate; + +import java.io.File; +import java.util.Optional; + +/** + * @author facarvalho + */ +public class SampleExtension2 extends Extension { + + public SampleExtension2() { + add(DummyTemplate.class); + } + + @Override + public String getDescription() { + return "SampleExtension2 for tests purposes"; + } + + @Override + public String getVersion() { + return "1.5.0"; + } + + @Override + public Optional> automaticResolution(File applicationFolder) { + return Optional.empty(); + } + +} diff --git a/butterfly-core/src/test/java/com/paypal/butterfly/core/sample/SampleTransformationTemplate.java b/butterfly-cli/src/test/java/com/test/SampleTemplate.java similarity index 65% rename from butterfly-core/src/test/java/com/paypal/butterfly/core/sample/SampleTransformationTemplate.java rename to butterfly-cli/src/test/java/com/test/SampleTemplate.java index 0912ddcf..967bf9a6 100644 --- a/butterfly-core/src/test/java/com/paypal/butterfly/core/sample/SampleTransformationTemplate.java +++ b/butterfly-cli/src/test/java/com/test/SampleTemplate.java @@ -1,21 +1,21 @@ -package com.paypal.butterfly.core.sample; +package com.test; import com.paypal.butterfly.extensions.api.Extension; import com.paypal.butterfly.extensions.api.TransformationTemplate; /** - * Created by vkuncham on 11/7/2016. + * @author facarvalho */ -public class SampleTransformationTemplate extends TransformationTemplate { +public class SampleTemplate extends TransformationTemplate { @Override public Class getExtensionClass() { - return ExtensionSample.class; + return SampleExtension1.class; } @Override public String getDescription() { - return "Butterfly extension"; + return "SampleTemplate for tests purposes"; } @Override diff --git a/butterfly-cli/src/test/resources/butterfly.properties b/butterfly-cli/src/test/resources/butterfly.properties new file mode 100644 index 00000000..2617b441 --- /dev/null +++ b/butterfly-cli/src/test/resources/butterfly.properties @@ -0,0 +1 @@ +butterfly.version = TEST \ No newline at end of file diff --git a/butterfly-cli/src/test/resources/extensionsListOut.txt b/butterfly-cli/src/test/resources/extensionsListOut.txt new file mode 100644 index 00000000..8b703788 --- /dev/null +++ b/butterfly-cli/src/test/resources/extensionsListOut.txt @@ -0,0 +1,9 @@ +Butterfly application transformation tool (version TEST) +See registered extensions below (shortcut in parenthesis) + +- com.test.SampleExtension1: SampleExtension1 for tests purposes (version 2.0.0) + (1) - [TT] SampleExtension1:SampleTemplate SampleTemplate for tests purposes + (2) - [TT] SampleExtension1:BlankTemplate BlankTemplate for tests purposes + +- com.test.SampleExtension2: SampleExtension2 for tests purposes (version 1.5.0) + (3) - [TT] SampleExtension2:DummyTemplate DummyTemplate for tests purposes diff --git a/butterfly-cli/src/test/resources/helpOut.txt b/butterfly-cli/src/test/resources/helpOut.txt new file mode 100644 index 00000000..d4919e81 --- /dev/null +++ b/butterfly-cli/src/test/resources/helpOut.txt @@ -0,0 +1,207 @@ +Butterfly application transformation tool (version TEST) + +Usage: butterfly [options] [application folder] + +The following options are available: + +Option Description +------ ----------- +-?, -h Show this help +-d Runs Butterfly in debug mode +-f Transforms the application in the same folder + as the original content. Options (-o) or (- + z) are ignored if (-f) is specified +-l List all registered extensions and their + transformation templates +-o The folder location in the file system where + the transformed application should be + placed. It defaults to same location where + original application is. Transformed + application is placed under a new folder + whose name is same as original folder, plus + "-transformed-yyyyMMddHHmmssSSS" suffix +-p Transformation specific properties, used to + determine if certain operations should be + skipped or not, or how certain aspects of + the transformation should be executed. Use + it followed by a list of key value pairs + separated by semi-colons (example `-p + prop1=value1;prop2=value2`). If both options + (-p) and (-q) are set, (-q) is ignored. +-q Transformation specific properties, used to + determine if certain operations should be + skipped or not, or how certain aspects of + the transformation should be executed. Use + it pointing to a '.properties' file. If both + options (-p) and (-q) are set, (-q) is + ignored. +-r Creates a result file in JSON format + containing details, not about the + transformation itself, but about the CLI + execution +-s The shortcut number to the transformation + template to be executed. If both shortcut (- + s) and template class (-t) name are + supplied, the shortcut will be ignored. If + the chosen transformation template is an + upgrade template, then the application will + be upgraded all the way to the latest + version possible, unless upgrade version (- + u) is specified +-t