/* * Copyright (c) 2013-2014 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.werval.cli; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import io.werval.api.exceptions.WervalException; import io.werval.commands.DevShellCommand; import io.werval.commands.SecretCommand; import io.werval.commands.StartCommand; import io.werval.devshell.JavaWatcher; import io.werval.runtime.CryptoInstance; import io.werval.spi.dev.DevShellRebuildException; import io.werval.spi.dev.DevShellSPI.SourceWatcher; import io.werval.spi.dev.DevShellSPIAdapter; import io.werval.util.ClassLoaders; import io.werval.util.DeltreeFileVisitor; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.cli.PosixParser; import static io.werval.cli.BuildVersion.COMMIT; import static io.werval.cli.BuildVersion.DATE; import static io.werval.cli.BuildVersion.DIRTY; import static io.werval.cli.BuildVersion.VERSION; import static io.werval.util.Charsets.UTF_8; import static io.werval.util.InputStreams.readAllAsString; import static io.werval.util.Strings.EMPTY; import static io.werval.util.Strings.NEWLINE; import static io.werval.util.Strings.hasText; import static io.werval.util.Strings.isEmpty; import static io.werval.util.Strings.join; import static java.io.File.separator; /** * Damn Small Werval DevShell. */ public final class DamnSmallDevShell { private static final class SPI extends DevShellSPIAdapter { private final URL[] applicationClasspath; private final URL[] runtimeClasspath; private final Set<File> sourcesRoots; private final File classesDir; private SPI( URL[] applicationSources, URL[] applicationClasspath, URL[] runtimeClasspath, Set<File> sourcesRoots, SourceWatcher watcher, File classesDir ) { super( applicationSources, applicationClasspath, runtimeClasspath, sourcesRoots, watcher, false ); this.applicationClasspath = applicationClasspath; this.runtimeClasspath = runtimeClasspath; this.sourcesRoots = sourcesRoots; this.classesDir = classesDir; } @Override protected void doRebuild() { try { DamnSmallDevShell.rebuild( applicationClasspath, runtimeClasspath, sourcesRoots, classesDir ); } catch( Exception ex ) { throw new DevShellRebuildException( ex ); } } } private static final class ShutdownHook implements Runnable { private final File tmpDir; private ShutdownHook( File tmpDir ) { this.tmpDir = tmpDir; } @Override public void run() { try { if( tmpDir.exists() ) { Files.walkFileTree( tmpDir.toPath(), new DeltreeFileVisitor() ); } } catch( IOException ex ) { ex.printStackTrace( System.err ); } } } // figlet -f rectangles "Werval" private static final String LOGO; static { LOGO = "" + " _ _ _ _\n" + "| | | |___ ___ _ _ ___| |\n" + "| | | | -_| _| | | .'| |\n" + "|_____|___|_| \\_/|__,|_|\n" + "Werval v" + VERSION + "-" + COMMIT + ( DIRTY ? " (DIRTY)" : "" ) + "\n"; } public static void main( String[] args ) { Options options = declareOptions(); CommandLineParser parser = new PosixParser(); try { CommandLine cmd = parser.parse( options, args ); // Handle --help if( cmd.hasOption( "help" ) ) { PrintWriter out = new PrintWriter( System.out ); printHelp( options, out ); out.flush(); System.exit( 0 ); } // Handle --version if( cmd.hasOption( "version" ) ) { System.out.print( String.format( "Werval CLI v%s\n" + "Git commit: %s%s, built on: %s\n" + "Java version: %s, vendor: %s\n" + "Java home: %s\n" + "Default locale: %s, platform encoding: %s\n" + "OS name: %s, version: %s, arch: %s\n", VERSION, COMMIT, ( DIRTY ? " (DIRTY)" : "" ), DATE, System.getProperty( "java.version" ), System.getProperty( "java.vendor" ), System.getProperty( "java.home" ), Locale.getDefault().toString(), System.getProperty( "file.encoding" ), System.getProperty( "os.name" ), System.getProperty( "os.version" ), System.getProperty( "os.arch" ) ) ); System.out.flush(); System.exit( 0 ); } // Debug final boolean debug = cmd.hasOption( 'd' ); // Temporary directory final File tmpDir = new File( cmd.getOptionValue( 't', "build" + separator + "devshell.tmp" ) ); if( debug ) { System.out.println( "Temporary directory set to '" + tmpDir.getAbsolutePath() + "'." ); } // Handle commands @SuppressWarnings( "unchecked" ) List<String> commands = cmd.getArgList(); if( commands.isEmpty() ) { commands = Collections.singletonList( "start" ); } if( debug ) { System.out.println( "Commands to be executed: " + commands ); } Iterator<String> commandsIterator = commands.iterator(); while( commandsIterator.hasNext() ) { String command = commandsIterator.next(); switch( command ) { case "new": System.out.println( LOGO ); newCommand( commandsIterator.hasNext() ? commandsIterator.next() : "werval-application", cmd ); break; case "clean": cleanCommand( debug, tmpDir ); break; case "devshell": System.out.println( LOGO ); devshellCommand( debug, tmpDir, cmd ); break; case "start": System.out.println( LOGO ); startCommand( debug, tmpDir, cmd ); break; case "secret": secretCommand(); break; default: PrintWriter out = new PrintWriter( System.err ); System.err.println( "Unknown command: '" + command + "'" ); printHelp( options, out ); out.flush(); System.exit( 1 ); break; } } } catch( IllegalArgumentException | ParseException | IOException ex ) { PrintWriter out = new PrintWriter( System.err ); printHelp( options, out ); out.flush(); System.exit( 1 ); } catch( WervalException ex ) { ex.printStackTrace( System.err ); System.err.flush(); System.exit( 1 ); } } private static void newCommand( String name, CommandLine cmd ) throws IOException { File baseDir = new File( name ); File ctrlDir = new File( baseDir, "src" + separator + "main" + separator + "java" + separator + "controllers" ); File rsrcDir = new File( baseDir, "src" + separator + "main" + separator + "resources" ); Files.createDirectories( ctrlDir.toPath() ); Files.createDirectories( rsrcDir.toPath() ); // Generate secret String conf = "\napp.secret = " + CryptoInstance.newRandomSecret256BitsHex() + "\n"; Files.write( new File( rsrcDir, "application.conf" ).toPath(), conf.getBytes( UTF_8 ) ); // Generate controller String controller = "package controllers;\n\n" + "import io.werval.api.outcomes.Outcome;\n\n" + "public class Application {\n\n" + " public Outcome index() {\n" + " return new io.werval.controllers.Welcome().welcome();\n" + " }\n\n" + "}\n"; Files.write( new File( ctrlDir, "Application.java" ).toPath(), controller.getBytes( UTF_8 ) ); // Generate routes String routes = "\nGET / controllers.Application.index\n"; Files.write( new File( rsrcDir, "routes.conf" ).toPath(), routes.getBytes( UTF_8 ) ); // Generate Gradle build file String gradle = "buildscript {\n" + " repositories { jcenter() }\n" + " dependencies { classpath 'io.werval:io.werval.gradle:" + VERSION + "' }\n" + "}\n" + "apply plugin: 'io.werval.application'\n" + "\n" + "dependencies {\n" + "\n" + " // Add application compile dependencies here\n" + "\n" + " runtime 'ch.qos.logback:logback-classic:1.1.2'\n" + " // Add application runtime dependencies here\n" + "\n" + " // Add application test dependencies here\n" + "\n" + "}\n" + ""; Files.write( new File( baseDir, "build.gradle.example" ).toPath(), gradle.getBytes( UTF_8 ) ); // Generate Maven POM file String pom = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n" + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + " xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n" + " <modelVersion>4.0.0</modelVersion>\n" + "\n" + " <groupId>" + name + "</groupId>\n" + " <artifactId>" + name + "</artifactId>\n" + " <version>" + VERSION + "</version>\n" + "\n" + " <properties>\n" + " <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n" + " <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n" + " </properties>\n" + "\n" + " <repositories>\n" + " <repository>\n" + " <id>jcenter</id>\n" + " <url>https://jcenter.bintray.com/</url>\n" + " </repository>\n" + " </repositories>\n" + "\n" + " <dependencies>\n" + "\n" + " <dependency>\n" + " <groupId>io.werval</groupId>\n" + " <artifactId>io.werval.api</artifactId>\n" + " <version>" + VERSION + "</version>\n" + " </dependency>\n" + " <!-- Add application compile dependencies here -->\n" + "\n" + " <dependency>\n" + " <groupId>io.werval</groupId>\n" + " <artifactId>io.werval.server.bootstrap</artifactId>\n" + " <version>" + VERSION + "</version>\n" + " <scope>runtime</scope>\n" + " </dependency>\n" + " <dependency>\n" + " <groupId>ch.qos.logback</groupId>\n" + " <artifactId>logback-classic</artifactId>\n" + " <version>1.1.2</version>\n" + " <scope>runtime</scope>\n" + " </dependency>\n" + " <!-- Add application runtime dependencies here -->\n" + "\n" + " <dependency>\n" + " <groupId>io.werval</groupId>\n" + " <artifactId>io.werval.test</artifactId>\n" + " <version>" + VERSION + "</version>\n" + " <scope>test</scope>\n" + " </dependency>\n" + " <!-- Add application test dependencies here -->\n" + "\n" + " </dependencies>\n" + "\n" + " <pluginRepositories>\n" + " <pluginRepository>\n" + " <id>jcenter</id>\n" + " <url>https://jcenter.bintray.com/</url>\n" + " </pluginRepository>\n" + " </pluginRepositories>\n" + "\n" + " <build>\n" + " <plugins>\n" + " <plugin>\n" + " <artifactId>maven-compiler-plugin</artifactId>\n" + " <version>3.1</version>\n" + " <configuration>\n" + " <source>1.8</source>\n" + " <target>1.8</target>\n" + " </configuration>\n" + " </plugin>\n" + " <plugin>\n" + " <groupId>io.werval</groupId>\n" + " <artifactId>io.werval.maven</artifactId>\n" + " <version>" + VERSION + "</version>\n" + " </plugin>\n" + " <plugin>\n" + " <groupId>org.codehaus.mojo</groupId>\n" + " <artifactId>appassembler-maven-plugin</artifactId>\n" + " <version>1.8</version>\n" + " <executions>\n" + " <execution>\n" + " <id>app-assembly</id>\n" + " <!-- Sample Packaging -->\n" + " <phase>package</phase>\n" + " <goals><goal>assemble</goal></goals>\n" + " <configuration>\n" + " <repositoryName>lib</repositoryName>\n" + " <repositoryLayout>flat</repositoryLayout>\n" + " <programs>\n" + " <program>\n" + " <id>werval-sample-maven</id>\n" + " <mainClass>io.werval.server.bootstrap.Main</mainClass>\n" + " </program>\n" + " </programs>\n" + " </configuration>\n" + " </execution>\n" + " </executions>\n" + " </plugin>\n" + " </plugins>\n" + " </build>\n" + "\n" + "</project>\n"; Files.write( new File( baseDir, "pom.xml.example" ).toPath(), pom.getBytes( UTF_8 ) ); // Generate .gitignore String gitignore = "target\nbuild\n.devshell.lock\nbuild.gradle.example\npom.xml.example\n.gradle\n"; Files.write( new File( baseDir, ".gitignore" ).toPath(), gitignore.getBytes( UTF_8 ) ); // Inform user System.out.println( "New Werval Application generated in '" + baseDir.getAbsolutePath() + "'." ); } private static void cleanCommand( boolean debug, File tmpDir ) { try { // Clean if( tmpDir.exists() ) { Files.walkFileTree( tmpDir.toPath(), new DeltreeFileVisitor() ); } // Inform user System.out.println( "Temporary files " + ( debug ? "in '" + tmpDir.getAbsolutePath() + "' " : "" ) + "deleted." ); } catch( IOException ex ) { ex.printStackTrace( System.err ); } } private static void devshellCommand( boolean debug, File tmpDir, CommandLine cmd ) throws IOException { final File classesDir = createClassesDirectory( debug, tmpDir ); Set<File> sourceRoots = prepareSourcesRoots( debug, cmd ); Set<URL> applicationSourcesSet = new LinkedHashSet<>( sourceRoots.size() ); for( File sourceRoot : sourceRoots ) { applicationSourcesSet.add( sourceRoot.toURI().toURL() ); } URL[] applicationSources = applicationSourcesSet.toArray( new URL[ applicationSourcesSet.size() ] ); URL[] applicationClasspath = prepareApplicationClasspath( debug, classesDir ); URL[] runtimeClasspath = prepareRuntimeClasspath( debug, sourceRoots, cmd ); applySystemProperties( debug, cmd ); System.out.println( "Loading..." ); // Watch Sources SourceWatcher watcher = new JavaWatcher(); // First build rebuild( applicationClasspath, runtimeClasspath, sourceRoots, classesDir ); // Run DevShell Runtime.getRuntime().addShutdownHook( new Thread( new ShutdownHook( tmpDir ), "werval-cli-cleanup" ) ); new DevShellCommand( new SPI( applicationSources, applicationClasspath, runtimeClasspath, sourceRoots, watcher, classesDir ) ).run(); } private static void startCommand( boolean debug, File tmpDir, CommandLine cmd ) throws IOException, MalformedURLException { final File classesDir = createClassesDirectory( debug, tmpDir ); Set<File> sourcesRoots = prepareSourcesRoots( debug, cmd ); URL[] runtimeClasspath = prepareRuntimeClasspath( debug, sourcesRoots, cmd ); URL[] applicationClasspath = prepareApplicationClasspath( debug, classesDir ); applySystemProperties( debug, cmd ); System.out.println( "Loading..." ); // Build rebuild( applicationClasspath, runtimeClasspath, sourcesRoots, classesDir ); // Start System.out.println( "Starting!" ); Runtime.getRuntime().addShutdownHook( new Thread( new ShutdownHook( tmpDir ), "werval-cli-cleanup" ) ); List<URL> globalClasspath = new ArrayList<>( Arrays.asList( runtimeClasspath ) ); globalClasspath.addAll( Arrays.asList( applicationClasspath ) ); new StartCommand( StartCommand.ExecutionModel.FORK, io.werval.server.bootstrap.Main.class.getName(), new String[ 0 ], globalClasspath.toArray( new URL[ globalClasspath.size() ] ) ).run(); } private static File createClassesDirectory( boolean debug, File tmpDir ) throws IOException { final File classesDir = new File( tmpDir, "classes" ); Files.createDirectories( classesDir.toPath() ); if( debug ) { System.out.println( "Classes directory is: " + classesDir.getAbsolutePath() ); } return classesDir; } private static Set<File> prepareSourcesRoots( boolean debug, CommandLine cmd ) { String[] sourcesPaths = cmd.hasOption( 's' ) ? cmd.getOptionValues( 's' ) : new String[] { "src" + separator + "main" + separator + "java", "src" + separator + "main" + separator + "resources" }; Set<File> sourcesRoots = new LinkedHashSet<>(); for( String sourceRoot : sourcesPaths ) { sourcesRoots.add( new File( sourceRoot ) ); } if( debug ) { System.out.println( "Sources roots are: " + sourcesRoots ); } return sourcesRoots; } private static URL[] prepareRuntimeClasspath( boolean debug, Set<File> sourcesRoots, CommandLine cmd ) throws MalformedURLException { List<URL> classpathList = new ArrayList<>(); // First, current classpath classpathList.addAll( ClassLoaders.urlsOf( DamnSmallDevShell.class.getClassLoader() ) ); // Then add command line provided if( cmd.hasOption( 'c' ) ) { for( String url : cmd.getOptionValues( 'c' ) ) { classpathList.add( new URL( url ) ); } } // Append Application sources for( File sourceRoot : sourcesRoots ) { classpathList.add( sourceRoot.toURI().toURL() ); } URL[] runtimeClasspath = classpathList.toArray( new URL[ classpathList.size() ] ); if( debug ) { System.out.println( "Runtime Classpath is: " + classpathList ); } return runtimeClasspath; } private static URL[] prepareApplicationClasspath( boolean debug, File classesDir ) throws MalformedURLException { URL[] applicationClasspath = new URL[] { classesDir.toURI().toURL() }; if( debug ) { System.out.println( "Application Classpath is: " + Arrays.toString( applicationClasspath ) ); } return applicationClasspath; } private static void applySystemProperties( boolean debug, CommandLine cmd ) { Properties systemProperties = cmd.getOptionProperties( "D" ); for( Iterator<Map.Entry<Object, Object>> it = systemProperties.entrySet().iterator(); it.hasNext(); ) { Map.Entry<?, ?> entry = it.next(); System.setProperty( entry.getKey().toString(), entry.getValue().toString() ); } if( debug ) { System.out.println( "Applied System Properties are: " + systemProperties ); } } private static void secretCommand() { new SecretCommand().run(); } /* package */ static void rebuild( URL[] applicationClasspath, URL[] runtimeClasspath, Set<File> sourcesRoots, File classesDir ) { System.out.println( "Compiling Application..." ); String javacOutput = EMPTY; try { // Collect java files String javaFiles = EMPTY; for( File sourceRoot : sourcesRoots ) { if( sourceRoot.exists() ) { ProcessBuilder findBuilder = new ProcessBuilder( "find", sourceRoot.getAbsolutePath(), "-type", "f", "-iname", "*.java" ); Process find = findBuilder.start(); int returnCode = find.waitFor(); if( returnCode != 0 ) { throw new IOException( "Unable to find java source files in " + sourceRoot ); } javaFiles += NEWLINE + readAllAsString( find.getInputStream(), 4096, UTF_8 ); } } if( hasText( javaFiles ) ) { // Write list in a temporary file File javaListFile = new File( classesDir, ".devshell-java-list" ); try( FileWriter writer = new FileWriter( javaListFile ) ) { writer.write( javaFiles ); writer.close(); } // Compile String[] classpathStrings = new String[ runtimeClasspath.length ]; for( int idx = 0; idx < runtimeClasspath.length; idx++ ) { classpathStrings[idx] = runtimeClasspath[idx].toURI().toASCIIString(); } ProcessBuilder javacBuilder = new ProcessBuilder( "javac", "-encoding", "UTF-8", "-source", "1.8", "-d", classesDir.getAbsolutePath(), "-classpath", join( classpathStrings, ":" ), "@" + javaListFile.getAbsolutePath() ); Process javac = javacBuilder.start(); int returnCode = javac.waitFor(); if( returnCode != 0 ) { throw new IOException( "Unable to build java source files." ); } javacOutput = readAllAsString( javac.getInputStream(), 4096, UTF_8 ); } } catch( InterruptedException | IOException | URISyntaxException ex ) { throw new WervalException( "Unable to rebuild" + ( isEmpty( javacOutput ) ? "" : "\n" + javacOutput ), ex ); } } @SuppressWarnings( "static-access" ) private static Options declareOptions() { Option classpathOption = OptionBuilder .withArgName( "element" ) .hasArgs() .withDescription( "Set application classpath element. " + "Use this option several times to declare a full classpath. " ) .withLongOpt( "classpath" ) .create( 'c' ); Option sourcesOption = OptionBuilder .withArgName( "directory" ) .hasArgs() .withDescription( "Set application sources directories. " + "Use this option several times to declare multiple sources directories. " + "Defaults to 'src/main/java' and 'src/main/resources' in current directory." ) .withLongOpt( "sources" ) .create( 's' ); Option tmpdirOption = OptionBuilder .withArgName( "directory" ) .hasArgs() .withDescription( "Set temporary directory. Defaults to 'build/devshell.tmp' in current directory." ) .withLongOpt( "tmpdir" ) .create( 't' ); Option propertiesOption = OptionBuilder.withArgName( "property=value" ) .hasArgs( 2 ) .withValueSeparator() .withDescription( "Define a system property. " + "Use this option several times to define multiple system properties. " + "Particularly convenient when used to override application configuration." ) .withLongOpt( "define" ) .create( 'D' ); Option debugOption = OptionBuilder .withDescription( "Enable debug output." ) .withLongOpt( "debug" ) .create( 'd' ); Option versionOption = OptionBuilder .withDescription( "Display version information." ) .withLongOpt( "version" ) .create(); Option helpOption = OptionBuilder .withDescription( "Display help information." ) .withLongOpt( "help" ) .create(); Options options = new Options(); options.addOption( classpathOption ); options.addOption( sourcesOption ); options.addOption( tmpdirOption ); options.addOption( propertiesOption ); options.addOption( debugOption ); options.addOption( versionOption ); options.addOption( helpOption ); return options; } private static final class OptionsComparator implements Comparator<Option> { private static final List<String> OPTIONS_ORDER = Arrays.asList( new String[] { "classpath", "sources", "tmpdir", "define", "debug", "version", "help", } ); @Override public int compare( Option o1, Option o2 ) { Integer o1idx = OPTIONS_ORDER.indexOf( o1.getLongOpt() ); Integer o2idx = OPTIONS_ORDER.indexOf( o2.getLongOpt() ); return o1idx.compareTo( o2idx ); } } private static final int WIDTH = 80; private static void printHelp( Options options, PrintWriter out ) { HelpFormatter help = new HelpFormatter(); help.setOptionComparator( new OptionsComparator() ); help.printUsage( out, WIDTH, "io.werval.cli [options] [command(s)]" ); out.print( "\n" + " The Damn Small Werval DevShell\n" + " - do not manage dependencies ;\n" + " - do not allow you to extend the build ;\n" + " - do not assemble applications.\n" ); help.printWrapped( out, WIDTH, 2, "\n" + "Meaning you have to manage your application dependencies and assembly yourself. " + "Theses limitations make this DevShell suitable for quick prototyping only. " + "Prefer the Gradle or Maven build systems integration." ); out.println( "\n io.werval.cli is part of the Werval Development Kit - http://werval.io" ); out.println( "\n" + "Commands:\n\n" + " new <appdir> Create a new skeleton application in the 'appdir' directory.\n" + " secret Generate a new application secret.\n" + " clean Delete devshell temporary directory, see 'tmpdir' option.\n" + " devshell Run the Application in development mode.\n" + " start Run the Application in production mode.\n" + "\n" + " If no command is specified, 'start' is assumed." ); out.println( "\n" + "Options:" + "\n" ); help.printOptions( out, WIDTH, options, 2, 2 ); help.printWrapped( out, WIDTH, 2, "\n" + "All paths are relative to the current working directory, " + "except if they are absolute of course." ); help.printWrapped( out, WIDTH, 2, "\n" + "Licensed under the Apache License Version 2.0, http://www.apache.org/licenses/LICENSE-2.0" ); out.println(); } private DamnSmallDevShell() { } }