Java tutorial
/* * 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 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 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 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() { } }