Package restx.core.shell

Source Code of restx.core.shell.AppShellCommand$ArchiveAppCommandRunner

package restx.core.shell;

import com.google.common.base.*;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import jline.console.completer.ArgumentCompleter;
import jline.console.completer.Completer;
import jline.console.completer.StringsCompleter;
import restx.AppSettings;
import restx.Apps;
import restx.RestxContext;
import restx.build.ModuleDescriptor;
import restx.build.RestxBuild;
import restx.build.RestxJsonSupport;
import restx.common.Archetype;
import restx.common.UUIDGenerator;
import restx.common.Version;
import restx.factory.Component;
import restx.factory.NamedComponent;
import restx.factory.SingletonFactoryMachine;
import restx.plugins.ModulesManager;
import restx.shell.RestxShell;
import restx.shell.ShellCommandRunner;
import restx.shell.ShellIvy;
import restx.shell.StdShellCommand;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Random;

import static java.nio.file.Files.newOutputStream;
import static restx.common.MoreFiles.extractZip;
import static restx.common.MorePreconditions.checkPresent;

/**
* User: xavierhanin
* Date: 4/10/13
* Time: 8:53 PM
*/
@Component
public class AppShellCommand extends StdShellCommand {
    private final UUIDGenerator uuidGenerator;

    public AppShellCommand(UUIDGenerator uuidGenerator) {
        super(ImmutableList.of("app"), "app related commands: creates a new app, run your app, ...");
        this.uuidGenerator = uuidGenerator;
    }

    @Override
    protected Optional<? extends ShellCommandRunner> doMatch(String line) {
        List<String> args = splitArgs(line);

        if (args.size() < 2) {
            return Optional.absent();
        }

        switch (args.get(1)) {
            case "new":
                return Optional.of(new NewAppCommandRunner());
            case "compile":
                return Optional.of(new CompileAppCommandRunner(args));
            case "generate-start-script":
                return Optional.of(new GenerateStartCommandRunner(args));
            case "run":
                return Optional.of(new RunAppCommandRunner(args));
            case "grab":
                return Optional.of(new GrabAppCommandRunner(args));
            case "archive":
                return Optional.of(new ArchiveAppCommandRunner(args));
        }

        return Optional.absent();
    }

    public static Path standardCachedAppPath(String appname) {
        return Paths.get(System.getProperty("restx.shell.home"), "apps/"+appname);
    }

    @Override
    public void man(Appendable appendable) throws IOException {
        super.man(appendable);
    }

    protected String resourceMan() {
        return "restx/core/shell/app.man";
    }

    @Override
    public Iterable<Completer> getCompleters() {
        return ImmutableList.<Completer>of(new ArgumentCompleter(
                new StringsCompleter("app"), new StringsCompleter("new", "run", "compile", "generate-start-script")));
    }

    static class NewAppDescriptor {
        String appName;
        String targetPath;
        String groupId;
        String artifactId;
        String mainPackage;
        String version;
        String buildFile;
        String signatureKey;
        String adminPassword;
        String defaultPort;
        String baseAPIPath;
        String javaVersion;
        String restxVersion;
        boolean generateHelloResource;
        boolean includeStatsModule;
        boolean useSrvuiLayout;
        String boostrapUIOption;
        String boostrapUITemplate;
    }

    class NewAppCommandRunner implements ShellCommandRunner {

        private Archetype srvMainTemplates = Archetype.buildArchetype("templates.srv.main");
        private Archetype srvHelloResourceTemplates = Archetype.buildArchetype("templates.srv.helloResource");
        private Archetype rootMainTemplates = Archetype.buildArchetype("templates.root.main");
        private Archetype rootNoNodeTemplates = Archetype.buildArchetype("templates.root.no-node");
        private Archetype rootGruntBowerTemplates = Archetype.buildArchetype("templates.root.grunt-bower");

        private final ImmutableMap<String, String> uiTemplatesPackageLocations = ImmutableMap.of(
                "html5", "https://github.com/restx/html5/archive/restx.zip",
                "angular-bootstrap", "https://github.com/restx/angular-bootstrap/archive/restx.zip",
                "angular-bootstrap-grunt-bower", "https://github.com/restx/angular-bootstrap-grunt-bower/archive/restx.zip"
        );

        @Override
        public void run(RestxShell shell) throws Exception {
            shell.printIn("Welcome to RESTX APP bootstrap!", RestxShell.AnsiCodes.ANSI_GREEN);
            shell.println("");
            shell.println("This command will ask you a few questions to generate your brand new RESTX app.");
            shell.println("For any question you can get help by answering '??' (without the quotes).");
            shell.println("");

            NewAppDescriptor descriptor = new NewAppDescriptor();
            descriptor.appName = "";
            while (Strings.isNullOrEmpty(descriptor.appName)) {
                descriptor.appName = shell.ask("App name? ", "",
                        "This is the name of the application you are creating.\n" +
                                "It can contain spaces, it's used mainly for documentation and to provide default for other values.\n" +
                                "Examples: Todo, Foo Bar, ...");
            }

            descriptor.targetPath = shell.ask("target directory [%s]? ",
                    descriptor.appName.replaceAll("\\s+", "-").toLowerCase(Locale.ENGLISH),
                    "This is the location where the app will be generated.\n" +
                            "You can use . if you want to generate in current directory.\n" +
                            "Note that restx will create the directory if it doesn't exist.");
            descriptor.groupId = shell.ask("group id [%s]? ",
                    descriptor.appName.replaceAll("\\s+", "-").toLowerCase(Locale.ENGLISH),
                    "This is the identifier of the group or organization producing the application.\n" +
                            "In the Maven world this is called a groupId, in Ivy it's called organization.\n" +
                            "It MUST NOT contain spaces nor columns (':'), and is usually a reversed domain name.\n" +
                            "Examples: io.restx, com.example, ...");
            descriptor.artifactId = shell.ask("artifact id [%s]? ",
                    descriptor.appName.replaceAll("\\s+", "-").toLowerCase(Locale.ENGLISH),
                    "This is the identifier of the app module.\n" +
                            "In the Maven world this is called an artifactId, in Ivy it's called module.\n" +
                            "It MUST NOT contain spaces nor columns (':'), and is usually a dash separated lower case word.\n" +
                            "Examples: myapp, todo, foo-app, ...")
                    .replaceAll("\\s+", "-");
            descriptor.mainPackage = shell.ask("main package [%s]? ",
                    descriptor.groupId.replaceAll("\\-", ".").toLowerCase(Locale.ENGLISH),
                    "This is the main package in which you will develop your application.\n" +
                            "In Java convention it should start with a reversed domain name followed by the app name\n" +
                            "but for applications (as opposed to APIs) we prefer to use a short name, like that app name.\n" +
                            "It MUST follow Java package names restrictions, so MUST NOT contain spaces\n" +
                            "Examples: myapp, com.example.todoapp, ...");
            descriptor.version = shell.ask("version [%s]? ", "0.1-SNAPSHOT",
                    "This is the name of the first version of the app you are targetting.\n" +
                            "It's recommended to use Maven convention to suffix it with -SNAPSHOT if you plan to use Maven for your app\n" +
                            "Examples: 0.1-SNAPSHOT, 1.0, ...");

            descriptor.buildFile = shell.ask("generate module descriptor (ivy/pom/none/all) [%s]? ", "all",
                    "This allows to generate a module descriptor for your app.\n" +
                            "Options:\n" +
                            "\t- 'ivy': get an Easyant compatible Ivy file generated for you.\n" +
                            "\t- 'pom': get a Maven POM generated for you.\n" +
                            "\t- 'all': get both a POM and an Ivy file.\n" +
                            "\t- 'none': get no module descriptor generated. WARNING: this will make it harder to build your app.\n" +
                            "If you don't know these tools, use default answer.\n"
            );

            descriptor.javaVersion = shell.ask("java version [%s]? ",
                    System.getProperty("java.version").replaceAll("^(\\d+\\.\\d+).*$", "$1"),
                    "The version of Java you want to use in your application.\n" +
                            "RESTX supports Java 7 (1.7) and Java 8 (1.8)\n" +
                            "Example: 1.7, 1.8");

            descriptor.restxVersion = shell.ask("restx version [%s]? ", Version.getVersion("io.restx", "restx-core"));

            List<String> list = Lists.newArrayList(uuidGenerator.doGenerate(),
                    String.valueOf(new Random().nextLong()), descriptor.appName, descriptor.artifactId);
            Collections.shuffle(list);
            descriptor.signatureKey = shell.ask("signature key (to sign cookies) [%s]? ",
                    Joiner.on(" ").join(list),
                    "This is used as salt for signing stuff exchanged with the client.\n" +
                            "Use something fancy or keep what is proposed by default, but make sure to not share that publicly.");

            descriptor.adminPassword = shell.ask("admin password (to authenticate on restx console) [%s]? ",
                    String.valueOf(new Random().nextInt(10000)),
                    "This is used as password for the admin user to authenticate on restx console.\n" +
                            "This is only a default way to authenticate out of the box, restx security is very flexible.");

            descriptor.defaultPort = shell.ask("default port [%s]? ", "8080",
                    "This is the default port used when using embedded version.\n" +
                            "Usually Java web containers use 8080, it may be a good idea to use a different port to avoid \n" +
                            "conflicts with another servlet container.\n" +
                            "You can also use port 80 if you want to serve your API directly with the embedded server\n" +
                            "and no reverse proxy in front of it. But beware that you may need admin privileges for that.\n" +
                            "Examples: 8080, 8086, 8000, 80");
            descriptor.baseAPIPath = shell.ask("base API path [%s]? ", "/api",
                    "This is the base API path on which RESTX will handle requests.\n" +
                            "Being focused on REST API only, RESTX is usually shared with either static or dynamic \n" +
                            "resources serving (HTML, CSS, JS, images, ...) and therefore is used to handle requests on\n" +
                            "only a sub path of the web app.\n" +
                            "If you plan to use it to serve requests from an API only domain (eg api.example.com)\n" +
                            "you can use '' (empty string) for this path.\n" +
                            "Examples: /api, /api/v2, /restx, ...");

            descriptor.generateHelloResource = shell.askBoolean("generate hello resource example [Y/n]? ", "y",
                    "This will generate an example resource with an associated spec test so that your boostrapped\n" +
                            "application can be used as soon as it has been generated.\n" +
                            "If this is the first app you generate with RESTX, it's probably a good idea to generate\n" +
                            "this example resource.\n" +
                            "If you already know RESTX by heart you shouldn't be reading this message anyway :)");

            descriptor.includeStatsModule = shell.askBoolean("include stats module and share anonymous stats on your app [Y/n]? ", "y",
                    "This will include the restx-stats-admin module which collects anonymous stats on your app\n" +
                            "and share them with the community.\n" +
                            "See http://restx.io/stats.html for more details.");

            descriptor.useSrvuiLayout = shell.askBoolean("do you want to use srv/ui layout [y/N]?", "n",
                    "This will organize your app in 2 modules:\n" +
                            "  - `srv`: for the server part, using RESTX to serve the REST API.\n" +
                            "  - `ui`:  for the front part, using your front end framework of choice (Angular, Ember, ...)\n" +
                            "If you prefer a different code organization between your front end and back end,\n" +
                            "like putting your front end resources in `src/main/webapp` as is usual for Java Web Development,\n" +
                            "don't use this option and organize your frontent as you like.");

            if (descriptor.useSrvuiLayout) {
                descriptor.boostrapUIOption = shell.ask("How do you want to bootstrap your UI (yo/restx/none) [restx]?", "restx",
                        "Select how to boostrap your UI:\n" +
                                "  - `yo`: use yeoman (you need to have yeoman installed).\n" +
                                "  - `restx`: RESTX has basic UI generation support.\n" +
                                "             select this option if you don't have yeoman installed but still want to have\n" +
                                "             basic UI boostrapped.\n" +
                                "  - `none`: don't bootstrap the UI, leave it empty." +
                                "            you can use this option if you want to bootstrap it yourself.\n");
                if ("yo".equalsIgnoreCase(descriptor.boostrapUIOption)) {
                    while (Strings.isNullOrEmpty(descriptor.boostrapUITemplate)) {
                        descriptor.boostrapUITemplate = shell.ask("Which generator do you want to use?:",
                            "",
                            "Select the yeoman generator you want to use.\n" +
                                    "Check http://yeoman.io/community-generators.html to get the list of generators.\n" +
                                    "Examples: angular, ember, mobile, ...");
                    }
                } else if ("restx".equalsIgnoreCase(descriptor.boostrapUIOption)) {
                    while (descriptor.boostrapUITemplate == null) {
                        String option = shell.ask("Select the template you want to use:\n" +
                                " [ 1] HTML5 Boilerplate\n" +
                                "      Use this if you want a very basic HTML5 bootstrap, with no additional tool required.\n" +
                                " [ 2] Angular, Twitter Bootstrap\n" +
                                "      Use this if you want a more convenient bootstrap, with no additional tool required.\n" +
                                " [ 3] Angular, Twitter Bootstrap. Use grunt for building and bower to manage dependencies.\n" +
                                "      Use this if you want a convenient bootstrap using the very popular frontend tools.\n" +
                                "      You will need:\n" +
                                "         - nodejs: http://nodejs.org/download/\n" +
                                "         - grunt:  http://gruntjs.com/getting-started\n" +
                                "         - bower:  http://bower.io/\n" +
                                "\n" +
                                " Which template do you want [1]? ",
                                "1",
                                "Select the template to use depending on your preferences and tools installed.\n" +
                                        "If you want more choices, you can either contribute to RESTX or use yeoman");
                        switch (option.trim()) {
                            case "1": descriptor.boostrapUITemplate = "html5"; break;
                            case "2": descriptor.boostrapUITemplate = "angular-bootstrap"; break;
                            case "3": descriptor.boostrapUITemplate = "angular-bootstrap-grunt-bower"; break;
                            default:  descriptor.boostrapUITemplate = null;
                        }
                    }
                }
            }

            Path appPath = generateApp(descriptor, shell);

            shell.cd(appPath);

            if (shell.askBoolean("Do you want to install its deps and run it now? [y/N]", "n",
                    "By answering yes restx will resolve and install the dependencies of the app and run it.\n" +
                            "You can always install the deps later by using the `deps install` command\n" +
                            "and run the app with the `app run` command")) {
                shell.println("restx> deps install");
                new DepsShellCommand().new InstallDepsCommandRunner().run(shell);
                shell.println("restx> app run");
                new RunAppCommandRunner(Collections.<String>emptyList()).run(shell);
            }
        }

        public Path generateApp(NewAppDescriptor descriptor, RestxShell shell) throws IOException {

            ImmutableMap scope = ImmutableMap.builder()
                    .put("appName", descriptor.appName)
                    .put("groupId", descriptor.groupId)
                    .put("artifactId", descriptor.artifactId)
                    .put("mainPackage", descriptor.mainPackage)
                    .put("packagePath", descriptor.mainPackage.replace('.', '/'))
                    .put("version", descriptor.version)
                    .put("signatureKey", descriptor.signatureKey)
                    .put("adminPassword", descriptor.adminPassword)
                    .put("defaultPort", descriptor.defaultPort)
                    .put("baseAPIPath", descriptor.baseAPIPath)
                    .put("javaVersion", descriptor.javaVersion)
                    .put("restxVersion", descriptor.restxVersion)
                    .put("includeStatsModule", descriptor.includeStatsModule)
                    .put("useSrvuiLayout", descriptor.useSrvuiLayout)
                    .build();

            Path appPath = shell.currentLocation().resolve(descriptor.targetPath);
            shell.println("scaffolding app to `" + appPath.toAbsolutePath() + "` ...");


            boolean useGruntBower = false;
            Path srvModulePath;
            if (descriptor.useSrvuiLayout) {
                Path uiModulePath = appPath.resolve("ui");
                rootMainTemplates.generate(appPath, scope);

                if ("yo".equalsIgnoreCase(descriptor.boostrapUIOption)) {
                    shell.println("you can scaffold ui with yeoman in `" + uiModulePath
                            + "` using `yo " + descriptor.boostrapUITemplate + "`");
                    // calling yo directly would be better, but yo is interactive, so we need to use INHERIT redirect
                    // input which closes stdin at the end of the process :(

                    useGruntBower = true; // assume users with yo will use grunt and bower
                } else if ("restx".equalsIgnoreCase(descriptor.boostrapUIOption)) {
                    shell.println("scaffolding ui with restx in `" + uiModulePath + "` ...");

                    Path uitplsPath = shell.installLocation().resolve("plugins/templates/ui");
                    File tplDir = uitplsPath.resolve(descriptor.boostrapUITemplate).toFile();
                    if (!tplDir.exists()) {
                        URL tplZipURL = new URL(uiTemplatesPackageLocations.get(descriptor.boostrapUITemplate));
                        uitplsPath.toFile().mkdirs();
                        shell.println("downloading UI template");
                        File zipFile = uitplsPath.resolve(descriptor.boostrapUITemplate + ".zip").toFile();
                        shell.download(tplZipURL, zipFile);

                        extractZip(zipFile, tplDir);
                        if (tplDir.list().length == 1 && tplDir.listFiles()[0].isDirectory()) {
                            // zip has a single root directory, we remove it
                            File dir = tplDir.listFiles()[0];
                            File[] files = dir.listFiles();
                            for (int i = 0; i < files.length; i++) {
                                File file = files[i];
                                Files.move(file, new File(tplDir, file.getName()));
                            }
                            dir.delete();
                        }
                    }

                    Archetype.buildArchetype(tplDir.toPath()).generate(uiModulePath, scope);
                    useGruntBower = uiModulePath.resolve("bower.json").toFile().exists();
                }

                if (useGruntBower) {
                    rootGruntBowerTemplates.generate(appPath, scope);
                } else {
                    rootNoNodeTemplates.generate(appPath, scope);
                }

                srvModulePath = appPath.resolve("srv");
                shell.println("scaffolding srv with restx in `" + srvModulePath + "` ...");
            } else {
                srvModulePath = appPath;
            }

            srvMainTemplates.generate(srvModulePath, scope);
            boolean generateIvy = "ivy".equalsIgnoreCase(descriptor.buildFile) || "all".equalsIgnoreCase(descriptor.buildFile);
            boolean generatePom = "pom".equalsIgnoreCase(descriptor.buildFile) || "all".equalsIgnoreCase(descriptor.buildFile);
            if (generateIvy) {
                shell.println("generating module.ivy ...");
                RestxBuild.convert(srvModulePath.toAbsolutePath() + "/md.restx.json", srvModulePath.toAbsolutePath() + "/module.ivy");
            }
            if (generatePom) {
                shell.println("generating pom.xml ...");
                RestxBuild.convert(srvModulePath.toAbsolutePath() + "/md.restx.json", srvModulePath.toAbsolutePath() + "/pom.xml");
            }

            if (descriptor.generateHelloResource) {
                shell.println("generating hello resource ...");
                srvHelloResourceTemplates.generate(srvModulePath, scope);
            }

            shell.printIn("Congratulations! - Your app is now ready in " + appPath.toAbsolutePath(), RestxShell.AnsiCodes.ANSI_GREEN);
            shell.println("");
            shell.println("");

            if (descriptor.useSrvuiLayout) {
                shell.println("Your app has 2 modules: ui and srv\n" +
                        "In srv, you can:\n" +
                        "  - open the module in your IDE by importing the Maven pom, and run the \n" +
                        "       `" + descriptor.mainPackage + ".AppServer` class to launch\n" +
                        "  - run it from restx shell, using:\n" +
                        "      deps install\n" +
                        "              to install its dependencies\n" +
                        "      app run\n" +
                        "              to run it\n");
                if (useGruntBower) {
                    shell.println(
                            "In ui you can:\n" +
                                    "  - open and edit your front end source files with your favorite editor\n" +
                                    "  - install local grunt with\n" +
                                    "      npm install\n" +
                                    "  - install app frontend dependencies listed in bower.json with\n" +
                                    "      bower install\n" +
                                    "  - run development server with:\n" +
                                    "      grunt server\n"
                    );
                } else {
                    shell.println(
                            "In ui you can:\n" +
                                    "  - open and edit your front end source files with your favorite editor\n" +
                                    "  - copy files to `dist` directory to make them available to RESTX web server\n" +
                                    "    hint: you can use the build.sh for that\n");
                }
                shell.println(
                        "At root level you can:\n" +
                        "  - build a production ready war using Maven (Linux/MacOS only):\n" +
                        "      mvn package");
            } else {
                shell.println("You can now:\n" +
                        "  - open the app in your IDE by importing the Maven pom, and run the \n" +
                        "       `" + descriptor.mainPackage + ".AppServer` class to launch\n" +
                        "  - run it from restx shell, using:\n" +
                        "      deps install\n" +
                        "              to install its dependencies\n" +
                        "      app run\n" +
                        "              to run it\n" +
                        "  - build a war using the selected build tool, eg\n" +
                        "      mvn package");
            }

            shell.printIn("Enjoy!", RestxShell.AnsiCodes.ANSI_GREEN);
            shell.println("");

            return srvModulePath;
        }
    }

    private class CompileAppCommandRunner implements ShellCommandRunner {
        private ShellAppRunner.CompileMode compileMode = ShellAppRunner.CompileMode.ALL;

        public CompileAppCommandRunner(List<String> args) {
        }

        @Override
        public void run(RestxShell shell) throws Exception {
            AppSettings appSettings = shell.getFactory()
                    .getComponent(AppSettings.class);
            compileMode.compile(
                    shell,
                    Paths.get(appSettings.targetClasses()),
                    Paths.get(appSettings.targetDependency()),
                    Paths.get(appSettings.mainSources()),
                    Paths.get(appSettings.mainResources()),
                    null);
        }
    }

    private class GenerateStartCommandRunner implements ShellCommandRunner {
        private String appClassName;

        public GenerateStartCommandRunner(List<String> args) {
            if (args.size() > 2) {
                appClassName = args.get(2);
            }
        }

        @Override
        public void run(RestxShell shell) throws Exception {
            AppSettings appSettings = shell.getFactory()
                    .getComponent(AppSettings.class);
            Optional<String> pack = Optional.absent();
            if (appClassName == null) {
                pack = Apps.with(appSettings)
                        .guessAppBasePackage(shell.currentLocation());
                if (!pack.isPresent()) {
                    shell.printIn("can't find base app package, src/main/java should contain a AppServer.java source file somewhere",
                            RestxShell.AnsiCodes.ANSI_RED);
                    shell.println("");
                    shell.println("alternatively you can provide the class to run with `app generate-start-script <class.to.Run>`");
                    return;
                }
                appClassName = pack.get() + ".AppServer";
            } else {
                pack = Optional.of(appClassName.substring(0, appClassName.lastIndexOf('.')));
            }



            String command = "java" +
                    " -cp \"" + appSettings.targetClasses() + ":" + appSettings.targetDependency() + "/*\"" +
                    " -Drestx.app.package=" + pack.get() +
                    " -Drestx.mode=prod"
                    ;

            File startSh = shell.currentLocation().resolve("start.sh").toFile();
            Files.write(
                    "#!/bin/sh\n\n" +
                            command + " $VM_OPTIONS " +
                            " " + appClassName + "\n",
                    startSh, Charsets.UTF_8);
            startSh.setExecutable(true);

            File startBat = shell.currentLocation().resolve("start.bat").toFile();
            Files.write(
                    command + " %VM_OPTIONS% " +
                    " " + appClassName + "\r\n",
                    startBat, Charsets.ISO_8859_1);

            shell.printIn("generated start scripts:\n" +
                    "\t" + startSh.getAbsolutePath() + "\n" +
                    "\t" + startBat.getAbsolutePath() + "\n",
                    RestxShell.AnsiCodes.ANSI_GREEN);
        }
    }

    private class RunAppCommandRunner implements ShellCommandRunner {
        private Optional<String> appClassNameArg;
        private Optional<String> appNameArg;
        private boolean quiet;
        private boolean daemon;
        private Optional<String> restxMode;
        private List<String> vmOptions = new ArrayList<>();

        public RunAppCommandRunner(List<String> args) {
            args = new ArrayList<>(args);
            quiet = false;
            daemon = true;
            appClassNameArg = Optional.absent();
            appNameArg = Optional.absent();
            restxMode = Optional.absent();

            while (args.size() > 2) {
                String arg = args.get(2);
                if (arg.equalsIgnoreCase("--quiet")) {
                    quiet = true;
                } else if (arg.startsWith("--fg")) {
                    daemon = false;
                } else if (arg.startsWith("--mode=") || arg.startsWith("-Drestx.mode=")) {
                    String mode = arg.startsWith("--mode=")?arg.substring("--mode=".length()):arg.substring("-Drestx.mode=".length());
                    restxMode = Optional.of(mode);
                } else if (arg.startsWith("-D") || arg.startsWith("-X")) {
                    vmOptions.add(arg);
                } else if (!appClassNameArg.isPresent() && !appNameArg.isPresent()) {
                    // If an existing restx app folder stands for given arg, considering it as the appName
                    // otherwise, arg will stand for a classname to execute
                    if(java.nio.file.Files.exists(standardCachedAppPath(arg))) {
                        appNameArg = Optional.of(arg);
                        restxMode = Optional.of(restxMode.or(RestxContext.Modes.PROD));
                    } else {
                        appClassNameArg = Optional.of(arg);
                    }
                } else {
                    throw new IllegalArgumentException("app run argument not recognized: " + arg);
                }
                args.remove(2);
            }
        }

        @Override
        public void run(final RestxShell shell) throws Exception {
            if(appNameArg.isPresent()) {
                shell.cd(standardCachedAppPath(appNameArg.get()));
            }

            boolean sourcesAvailable = Apps.with(shell.getFactory().getComponent(AppSettings.class)).sourcesAvailableIn(shell.currentLocation());

            String appClassName;
            ShellAppRunner.CompileMode compileMode;
            if(sourcesAvailable) {
                if(appClassNameArg.isPresent()) {
                    appClassName = appClassNameArg.get();
                } else {
                    Optional<String> appClassNameGuessedFromRestxModule = guessAppClassnameFromRestxModule(shell);
                    if(appClassNameGuessedFromRestxModule.isPresent()) {
                        appClassName = appClassNameGuessedFromRestxModule.get();
                    } else {
                        appClassName = guessAppClassnameFromSources(shell);
                    }
                }

                compileMode = (restxMode.isPresent() && RestxContext.Modes.PROD.equals(restxMode.get()))? ShellAppRunner.CompileMode.ALL: ShellAppRunner.CompileMode.MAIN_CLASS;

                if(appClassName == null) {
                    shell.printIn("can't find base app package, src/main/java should contain a AppServer.java source file somewhere",
                            RestxShell.AnsiCodes.ANSI_RED);
                    shell.println("");
                    shell.println("alternatively you can provide the class to run with `app run <class.to.Run>`");
                    return;
                }
            } else {
                appClassName = appClassNameArg
                    .or(guessAppClassnameFromRestxModule(shell))
                    .orNull();

                // Consider we're in prod mode, without any auto compilation feature (since we don't have any source folder)
                compileMode = ShellAppRunner.CompileMode.NO;
                restxMode = Optional.of(RestxContext.Modes.PROD);

                if(appClassName == null){
                    shell.printIn("can't find manifest.main.classname property in md.restx.json", RestxShell.AnsiCodes.ANSI_RED);
                    shell.println("");
                    shell.println("alternatively you can provide the class to run with `app run <class.to.Run>`");
                    return;
                }
            }

            if (!DepsShellCommand.depsUpToDate(shell)) {
                shell.println("restx> deps install");
                new DepsShellCommand().new InstallDepsCommandRunner().run(shell);
            }

            List<String> vmOptions = new ArrayList<>(this.vmOptions);
            if(restxMode.isPresent()) {
                vmOptions.add("-Drestx.mode="+restxMode.get());
            }

            String basePack = appClassName.substring(0, appClassName.lastIndexOf('.'));
            AppSettings appSettings = shell.getFactory()
                    .concat(new SingletonFactoryMachine<>(-10000, NamedComponent.of(String.class, "restx.app.package", basePack)))
                    .getComponent(AppSettings.class);
            new ShellAppRunner(appSettings, appClassName, compileMode, quiet, daemon, vmOptions)
                .run(shell);
        }

        private Optional<String> guessAppClassnameFromRestxModule(RestxShell shell) throws IOException {

            RestxJsonSupport restxJsonSupport = new RestxJsonSupport();

            Path restxJsonFile = shell.currentLocation().resolve(restxJsonSupport.getDefaultFileName());
            if(java.nio.file.Files.notExists(restxJsonFile)){
                return Optional.absent();
            }

            ModuleDescriptor moduleDescriptor = restxJsonSupport.parse(restxJsonFile);
            return Optional.fromNullable(moduleDescriptor.getProperties().get("manifest.main.classname"));
        }

        private String guessAppClassnameFromSources(RestxShell shell) {
            Optional<String> pack = Apps.with(shell.getFactory().getComponent(AppSettings.class))
                                            .guessAppBasePackage(shell.currentLocation());
            if (!pack.isPresent()) {
                return null;
            }
            return pack.get() + ".AppServer";
        }
    }

    private static enum GrabbingStrategy {
        FROM_GIT(){
            @Override
            protected boolean accept(String coordinates) {
                return coordinates.endsWith(".git");
            }

            @Override
            public String extractProjectNameFrom(String coordinates) {
                return extractExtensionlessFilenameFromUrl(coordinates.split("#")[0]);
            }

            @Override
            public void unpackCoordinatesTo(String coordinates, Path destinationDir, String projectName, RestxShell shell) throws IOException {
                String url = coordinates;
                Optional<String> ref = Optional.absent();
                if(url.contains("#")) {
                    url = coordinates.split("#")[0];
                    ref = Optional.of(coordinates.split("#")[1]);
                }

                try {
                    shell.println("Cloning "+url+"...");
                    Runtime.getRuntime().exec(new String[]{"git", "clone", url, "."}, new String[0], destinationDir.toFile()).waitFor();

                    if(ref.isPresent()) {
                        Runtime.getRuntime().exec(new String[]{"git", "checkout", ref.get()}, new String[0], destinationDir.toFile()).waitFor();
                    }
                } catch(InterruptedException e) {
                    throw Throwables.propagate(e);
                }
            }
        }, FROM_URL(){
            @Override
            protected boolean accept(String coordinates) {
                return coordinates.startsWith("file://")
                        || coordinates.startsWith("http://")
                        || coordinates.startsWith("https://");
            }
            @Override
            public String extractProjectNameFrom(String coordinates) {
                return extractExtensionlessFilenameFromUrl(coordinates);
            }
            @Override
            public void unpackCoordinatesTo(String coordinates, Path destinationDir, String projectName, RestxShell shell) throws IOException {
                handleSingleFileGrabbing(coordinates, destinationDir, projectName, shell, new SingleFileGrabber() {
                    @Override
                    public void grabSingleFileTo(String coordinates, Path destinationFile, RestxShell shell) throws IOException {
                        try {
                            URL url = new URL(coordinates);
                            try (InputStream urlStream = url.openStream();
                                 OutputStream destFileOS = newOutputStream(destinationFile)) {
                                ByteStreams.copy(urlStream, destFileOS);
                            }
                        } catch (MalformedURLException e) {
                            throw Throwables.propagate(e);
                        }
                    }
                });
            }
        }, FROM_GAV(){
            @Override
            protected boolean accept(String coordinates) {
                return ModulesManager.isMrid(coordinates);
            }
            @Override
            public String extractProjectNameFrom(String coordinates) {
                return ModulesManager.toMrid(coordinates).getName();
            }
            @Override
            public void unpackCoordinatesTo(String coordinates, final Path destinationDir, String projectName, RestxShell shell) throws IOException {
                handleSingleFileGrabbing(coordinates, destinationDir, projectName, shell, new SingleFileGrabber() {
                    @Override
                    public void grabSingleFileTo(String coordinates, Path destinationFile, RestxShell shell) throws IOException {
                        restx.plugins.ModuleDescriptor moduleDescriptor = new restx.plugins.ModuleDescriptor(coordinates, "app", "");
                        ModulesManager modulesManager = new ModulesManager(null, ShellIvy.loadIvy(shell));

                        List<File> files = modulesManager.download(
                                ImmutableList.of(moduleDescriptor),
                                destinationDir.toFile(),
                                new ModulesManager.DownloadOptions.Builder().transitive(false).build()
                        );
                        if(!files.get(0).equals(destinationFile.toFile())) {
                            Files.move(files.get(0), destinationFile.toFile());
                        }
                    }
                });
            }
        };

        public static Optional<GrabbingStrategy> fromCoordinates(String coordinates) {
            for(GrabbingStrategy grabbingStrategy : values()){
                if(grabbingStrategy.accept(coordinates)) {
                    return Optional.of(grabbingStrategy);
                }
            }
            return Optional.absent();
        }

        private static interface SingleFileGrabber {
            void grabSingleFileTo(String coordinates, Path destinationFile, RestxShell shell) throws IOException;
        }

        protected void handleSingleFileGrabbing(String coordinates, Path destinationDir, String projectName, RestxShell shell, SingleFileGrabber singleFileGrabber) throws IOException {
            Path jarFile = destinationDir.resolve(projectName+".jar");
            Files.createParentDirs(jarFile.toFile());

            singleFileGrabber.grabSingleFileTo(coordinates, jarFile, shell);

            AppSettings appSettings = shell.getFactory().getComponent(AppSettings.class);
            new RestxArchive(jarFile).unpack(destinationDir, appSettings);
            jarFile.toFile().delete();
        }

        protected static String extractExtensionlessFilenameFromUrl(String coordinates) {
            String filename = coordinates.substring(coordinates.lastIndexOf("/")+1);
            filename = filename.contains("?")?filename.substring(0, filename.indexOf("?")):filename;
            filename = filename.contains(".")?filename.substring(0, filename.lastIndexOf(".")):filename;
            return filename;
        }

        protected abstract boolean accept(String coordinates);
        public abstract String extractProjectNameFrom(String coordinates);
        public abstract void unpackCoordinatesTo(String coordinates, Path destinationDir, String projectName, RestxShell shell) throws IOException;
    }

    private class GrabAppCommandRunner implements ShellCommandRunner {

        private final String projectName;
        private final String coordinates;
        private final GrabbingStrategy grabbingStrategy;
        private final Path destinationDirectoy;

        public GrabAppCommandRunner(List<String> args) {
            args = new ArrayList<>(args);

            if(args.size() < 3) {
                throw new IllegalArgumentException("app grab : missing coordinates argument");
            }

            this.coordinates = args.get(2);
            this.grabbingStrategy = checkPresent(GrabbingStrategy.fromCoordinates(this.coordinates),
                    "app grab : cannot found a grabbing strategy for coordinates: " + this.coordinates);

            this.projectName = this.grabbingStrategy.extractProjectNameFrom(this.coordinates);
            this.destinationDirectoy = args.size() >= 4 ? Paths.get(args.get(3)) : standardCachedAppPath(projectName);
        }

        @Override
        public void run(RestxShell shell) throws Exception {
            Files.createParentDirs(destinationDirectoy.resolve("uselessUnexistingFile").toFile());

            this.grabbingStrategy.unpackCoordinatesTo(this.coordinates, this.destinationDirectoy, this.projectName, shell);

            shell.cd(destinationDirectoy);
        }
    }

    private class ArchiveAppCommandRunner implements ShellCommandRunner {
        private final Path jarFile;

        public ArchiveAppCommandRunner(List<String> args) {
            if(args.size() < 3) {
                throw new IllegalArgumentException("app archive : missing jarFile argument");
            }

            this.jarFile = Paths.get(args.get(2));
        }

        @Override
        public void run(RestxShell shell) throws Exception {
            AppSettings appSettings = shell.getFactory().getComponent(AppSettings.class);
            new RestxArchive(jarFile).pack(
                    shell.currentLocation(),
                    shell.currentLocation().resolve(appSettings.targetClasses()),
                    Arrays.asList("target", "tmp", "logs")
            );
        }
    }
}
TOP

Related Classes of restx.core.shell.AppShellCommand$ArchiveAppCommandRunner

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.