diff --git a/gradle/assemble.gradle b/gradle/assemble.gradle index 7852fbf1ae6..eddeccc314a 100644 --- a/gradle/assemble.gradle +++ b/gradle/assemble.gradle @@ -6,7 +6,7 @@ def libsConfigurations = [] subprojects { subproject -> if(subproject.name == 'grails-dependencies') return if(subproject.name == 'grails-bom') return - if(subproject.name == 'grails-core') { + if(subproject.name == 'grails-shell' || subproject.name == 'grails-core') { configurations { libsConfigurations << libs { @@ -137,13 +137,57 @@ task sourcesJars(type: Sync) { from { sourcesFor(libsConfigurations*.copyRecursive { it.name.startsWith('grails-datastore') }.collect { it.transitive = false; it }) } } -task install(dependsOn: [populateDependencies]) { task -> +task grailsCreateStartScripts(type: GrailsCreateStartScripts) { + description = "Creates OS specific scripts to run grails-shell as a JVM application." + mainClass.set('org.grails.cli.GrailsCli') + applicationName = 'grails' + defaultJvmOpts = ["-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1", "-XX:CICompilerCount=3"] + outputDir = file('bin') + classpath = rootProject.childProjects['grails-shell'].configurations.runtimeClasspath + projectArtifacts = rootProject.childProjects['grails-shell'].tasks['jar'].outputs.files.collect { "dist/${it.name}" } + doLast { + ant.replace(file: file('bin/grails'), token: 'media/gradle.icns', value: 'media/icons/grails.icns') + ant.chmod(file: file('bin/grails'), perm: 'ugo+rx') + } +} + +class GrailsCreateStartScripts extends org.gradle.api.tasks.application.CreateStartScripts { + + @Input + Collection projectArtifacts=[] + + @org.gradle.api.tasks.TaskAction + void generate() { + def generator = new org.gradle.api.internal.plugins.StartScriptGenerator() + generator.unixStartScriptGenerator.template = project.rootProject.childProjects['grails-shell'].resources.text.fromFile('src/main/resources/unixStartScript.txt') + generator.applicationName = getApplicationName() + generator.mainClassName = getMainClassName() + generator.defaultJvmOpts = getDefaultJvmOpts() + generator.optsEnvironmentVar = getOptsEnvironmentVar() + generator.exitEnvironmentVar = getExitEnvironmentVar() + generator.classpath = projectArtifacts + getClasspath().resolvedConfiguration.resolvedArtifacts.collect { artifact -> + def dependency = artifact.moduleVersion.id + String installedFile = "lib/$dependency.group/$dependency.name/jars/$artifact.file.name" + if(dependency.group=='org.grails' && !project.file(installedFile).exists()) { + installedFile = "dist/$artifact.file.name" + } + installedFile + } + generator.scriptRelPath = "bin/${getUnixScript().name}" + generator.generateUnixScript(getUnixScript()) + generator.generateWindowsScript(getWindowsScript()) + } +} + +task install(dependsOn: [populateDependencies, grailsCreateStartScripts]) { task -> subprojects { Project project -> if(!project.name.startsWith('grails-test-suite')) { task.dependsOn("$project.name:publishToMavenLocal") } } } +//task install(dependsOn: [populateDependencies, grailsCreateStartScripts] + subprojects.findAll { !it.name.startsWith('grails-test-suite') } +// *.collect { Project p -> p.tasks.withType(PublishToMavenLocal)}) task zipDist(type: Zip, dependsOn: [sourcesJars, install]) { destinationDir = "${buildDir}/distributions" as File diff --git a/grails-shell/README.md b/grails-shell/README.md new file mode 100644 index 00000000000..4ecb23353d5 --- /dev/null +++ b/grails-shell/README.md @@ -0,0 +1,3 @@ +## grails-shell + +This subproject provides code related to the Grails CLI and plugin commands. diff --git a/grails-shell/build.gradle b/grails-shell/build.gradle new file mode 100644 index 00000000000..8092bda6bf9 --- /dev/null +++ b/grails-shell/build.gradle @@ -0,0 +1,66 @@ +apply plugin:'application' + +mainClassName = "org.grails.cli.GrailsCli" + +repositories { + mavenCentral() +} + +ext { + gradleToolingApiVersion = '7.3-20210825160000+0000' +} + +dependencies { + api project(":grails-bootstrap") + api project(":grails-gradle-model") + api "org.apache.ant:ant:$antVersion" + api "org.codehaus.groovy:groovy-ant:$groovyVersion" + api "org.codehaus.groovy:groovy-json:$groovyVersion" + api "org.codehaus.groovy:groovy-jmx:$groovyVersion" + api "org.fusesource.jansi:jansi:$jansiVersion" + api "jline:jline:$jlineVersion" + api "org.gradle:gradle-tooling-api:$gradleToolingApiVersion" + + api "org.springframework.boot:spring-boot-cli:$springBootVersion", { + exclude group: "org.codehaus.groovy", module: "groovy" + } + implementation("org.apache.maven:maven-resolver-provider:3.9.6") { + exclude group: "com.google.guava", module: "guava" + } + + implementation("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18") + implementation("org.apache.maven.resolver:maven-resolver-impl:1.9.18") + implementation("org.apache.maven.resolver:maven-resolver-transport-file:1.9.18") + implementation("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18") { + exclude group: "org.slf4j", module:"jcl-over-slf4j" + exclude group: "commons-codec", module:"commons-codec" + } + implementation("commons-codec:commons-codec:1.16.0") + + testImplementation "net.sf.expectit:expectit-core:0.9.0" + testImplementation "com.github.jnr:jnr-posix:3.1.18" + + runtimeOnly "org.slf4j:slf4j-simple:$slf4jVersion" + runtimeOnly "org.codehaus.plexus:plexus-component-api:1.0-alpha-33" + +} + +eclipse { + classpath { + file { + whenMerged { classpath -> + classpath.entries.find { entry -> entry.kind == 'src' && entry.path == "src/test/resources" }?.excludes=["profiles-repository/**", "gradle-sample/**"] + } + } + } +} + +apply from: "../gradle/integration-test.gradle" + +integrationTest { + // jline doesn't use jline.terminal system property when TERM is dumb so use different TERM value for tests + // https://github.com/jline/jline2/blob/6a1b6bf/src/main/java/jline/TerminalFactory.java#L54-L57 + environment 'TERM', 'xterm' + // execute in single thread + maxParallelForks = 1 +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/GrailsCli.groovy b/grails-shell/src/main/groovy/org/grails/cli/GrailsCli.groovy new file mode 100644 index 00000000000..dfa9874ab2a --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/GrailsCli.groovy @@ -0,0 +1,737 @@ +/* + * Copyright 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 org.grails.cli + +import grails.build.logging.GrailsConsole +import grails.build.proxy.SystemPropertiesAuthenticator +import grails.config.ConfigMap +import grails.io.support.SystemOutErrCapturer +import grails.io.support.SystemStreamsRedirector +import grails.util.BuildSettings +import grails.util.Environment +import groovy.transform.Canonical +import groovy.transform.CompileStatic +import jline.UnixTerminal +import jline.console.UserInterruptException +import jline.console.completer.ArgumentCompleter +import jline.console.completer.Completer +import jline.internal.NonBlockingInputStream +import org.gradle.tooling.BuildActionExecuter +import org.gradle.tooling.BuildCancelledException +import org.gradle.tooling.ProjectConnection +import org.grails.build.parsing.CommandLine +import org.grails.build.parsing.CommandLineParser +import org.grails.build.parsing.DefaultCommandLine +import org.grails.cli.gradle.ClasspathBuildAction +import org.grails.cli.gradle.GradleAsyncInvoker +import org.grails.cli.gradle.cache.MapReadingCachedGradleOperation +import org.grails.cli.interactive.completers.EscapingFileNameCompletor +import org.grails.cli.interactive.completers.RegexCompletor +import org.grails.cli.interactive.completers.SortedAggregateCompleter +import org.grails.cli.interactive.completers.StringsCompleter +import org.grails.cli.profile.* +import org.grails.cli.profile.commands.CommandCompleter +import org.grails.cli.profile.commands.CommandRegistry +import org.grails.cli.profile.repository.GrailsRepositoryConfiguration +import org.grails.cli.profile.repository.MavenProfileRepository +import org.grails.cli.profile.repository.StaticJarProfileRepository +import org.grails.config.CodeGenConfig +import org.grails.config.NavigableMap +import org.grails.exceptions.ExceptionUtils + +import java.util.concurrent.* + +/** + * Main class for the Grails command line. Handles interactive mode and running Grails commands within the context of a profile + * + * @author Lari Hotari + * @author Graeme Rocher + * + * @since 3.0 + */ +@CompileStatic +class GrailsCli { + static final String ARG_SPLIT_PATTERN = /(? profileRepositories = [MavenProfileRepository.DEFAULT_REPO] + + /** + * Obtains a value from USER_HOME/.grails/settings.yml + * + * @param key the property name to resolve + * @param targetType the expected type of the property value + * @param defaultValue The default value + */ + public static T getSetting(String key, Class targetType = Object.class, T defaultValue = null) { + def value = SETTINGS_MAP.get(key, defaultValue) + if(value == null) { + return null + } + + else if(targetType.isInstance(value)) { + return (T)value + } + else { + try { + return value.asType(targetType) + } catch (Throwable e) { + return null + } + } + } + /** + * Main method for running via the command line + * + * @param args The arguments + */ + public static void main(String[] args) { + + Authenticator.setDefault( getSetting( BuildSettings.AUTHENTICATOR, Authenticator, new SystemPropertiesAuthenticator() ) ) + def proxySelector = getSetting(BuildSettings.PROXY_SELECTOR, ProxySelector) + if(proxySelector != null) { + ProxySelector.setDefault( proxySelector ) + } + + GrailsCli cli=new GrailsCli() + try { + exit(cli.execute(args)) + } + catch(BuildCancelledException e) { + GrailsConsole.instance.addStatus("Build stopped.") + exit(0) + } + catch (Throwable e) { + e = ExceptionUtils.getRootCause(e) + GrailsConsole.instance.error("Error occurred running Grails CLI: $e.message", e) + exit(1) + } + } + + static void exit(int code) { + GrailsConsole.instance.cleanlyExit(code) + } + + static boolean isInteractiveModeActive() { + return interactiveModeActive + } + + static void tiggerAppLoad() { + GrailsCli.tiggerAppLoad = true + } + + private int getBaseUsage() { + System.out.println "Usage: \n\t $USAGE_MESSAGE \n\t $PLUGIN_USAGE_MESSAGE \n\n" + this.execute "list-profiles" + System.out.println "\nType 'grails help' or 'grails -h' for more information." + + return 1 + } + + /** + * Execute the given command + * + * @param args The arguments + * @return The exit status code + */ + public int execute(String... args) { + CommandLine mainCommandLine=cliParser.parse(args) + + if(mainCommandLine.hasOption(CommandLine.VERBOSE_ARGUMENT)) { + System.setProperty("grails.verbose", "true") + System.setProperty("grails.full.stacktrace", "true") + } + if(mainCommandLine.hasOption(CommandLine.STACKTRACE_ARGUMENT)) { + System.setProperty("grails.show.stacktrace", "true") + } + + if(mainCommandLine.hasOption(CommandLine.VERSION_ARGUMENT) || mainCommandLine.hasOption('v')) { + def console = GrailsConsole.instance + console.addStatus("Grails Version: ${GrailsCli.getPackage().implementationVersion}") + console.addStatus("JVM Version: ${System.getProperty('java.version')}") + exit(0) + } + + + + if(mainCommandLine.hasOption(CommandLine.HELP_ARGUMENT) || mainCommandLine.hasOption('h')) { + profileRepository = createMavenProfileRepository() + def cmd = CommandRegistry.getCommand("help", profileRepository) + cmd.handle(createExecutionContext(mainCommandLine)) + exit(0) + } + + if(mainCommandLine.environmentSet) { + System.setProperty(Environment.KEY, mainCommandLine.environment) + Environment.reset() + } + + File grailsAppDir=new File("grails-app") + File applicationGroovy =new File("Application.groovy") + File profileYml =new File("profile.yml") + if(!grailsAppDir.isDirectory() && !applicationGroovy.exists() && !profileYml.exists()) { + profileRepository = createMavenProfileRepository() + if(!mainCommandLine || !mainCommandLine.commandName) { + integrateGradle = false + def console = GrailsConsole.getInstance() + // force resolve of all profiles + profileRepository.getAllProfiles() + def commandNames = CommandRegistry.findCommands(profileRepository).collect() { Command cmd -> cmd.name } + console.reader.addCompleter(new StringsCompleter(commandNames)) + console.reader.addCompleter(new CommandCompleter(CommandRegistry.findCommands(profileRepository))) + profile = [handleCommand: { ExecutionContext context -> + + def cl = context.commandLine + def name = cl.commandName + def cmd = CommandRegistry.getCommand(name, profileRepository) + if(cmd != null) { + return executeCommandWithArgumentValidation(cmd, cl) + } + else { + console.error("Command not found [$name]") + return false + } + } ] as Profile + + startInteractiveMode(console) + return 0 + } + def cmd = CommandRegistry.getCommand(mainCommandLine.commandName, profileRepository) + if(cmd) { + return executeCommandWithArgumentValidation(cmd, mainCommandLine) ? 0 : 1 + } + else { + return getBaseUsage() + } + + } else { + initializeApplication(mainCommandLine) + if(mainCommandLine.commandName) { + return handleCommand(mainCommandLine) ? 0 : 1 + } else { + handleInteractiveMode() + } + } + return 0 + } + + protected boolean executeCommandWithArgumentValidation(Command cmd, CommandLine mainCommandLine) { + def arguments = cmd.description.arguments + def requiredArgs = arguments.count { CommandArgument arg -> arg.required } + if (mainCommandLine.remainingArgs.size() < requiredArgs) { + outputMissingArgumentsMessage cmd + return false + } else { + return cmd.handle(createExecutionContext(mainCommandLine)) + } + } + + protected void initializeApplication(CommandLine mainCommandLine) { + applicationConfig = loadApplicationConfig() + File profileYml = new File("profile.yml") + if (profileYml.exists()) { + // use the profile for profiles + applicationConfig.put(BuildSettings.PROFILE, "profile") + } + + GrailsConsole console = GrailsConsole.instance + console.ansiEnabled = !mainCommandLine.hasOption(CommandLine.NOANSI_ARGUMENT) + console.defaultInputMask = defaultInputMask + if (ansiEnabled != null) { + console.ansiEnabled = ansiEnabled + } + File baseDir = new File(".").canonicalFile + projectContext = new ProjectContextImpl(console, baseDir, applicationConfig) + initializeProfile() + } + + protected MavenProfileRepository createMavenProfileRepository() { + def profileRepos = getSetting(BuildSettings.PROFILE_REPOSITORIES, Map.class, Collections.emptyMap()) + if(!profileRepos.isEmpty()) { + profileRepositories.clear() + for (repoName in profileRepos.keySet()) { + def data = profileRepos.get(repoName) + if(data instanceof Map) { + def uri = data.get("url") + def snapshots = data.get('snapshotsEnabled') + if(uri != null) { + boolean enableSnapshots = snapshots != null ? Boolean.valueOf(snapshots.toString()) : false + GrailsRepositoryConfiguration repositoryConfiguration + final String username = data.get('username') + final String password = data.get('password') + if (username != null && password != null) { + repositoryConfiguration = new GrailsRepositoryConfiguration(repoName.toString(), new URI(uri.toString()), enableSnapshots, username, password) + } else { + repositoryConfiguration = new GrailsRepositoryConfiguration(repoName.toString(), new URI(uri.toString()), enableSnapshots) + } + profileRepositories.add(repositoryConfiguration) + } + } + } + } + return new MavenProfileRepository(profileRepositories) + } + + protected void outputMissingArgumentsMessage(Command cmd) { + def console = GrailsConsole.instance + console.error("Command $cmd.name is missing required arguments:") + for (CommandArgument arg in cmd.description.arguments.findAll { CommandArgument ca -> ca.required }) { + console.log("* $arg.name - $arg.description") + } + } + + ExecutionContext createExecutionContext(CommandLine commandLine) { + new ExecutionContextImpl(commandLine, projectContext) + } + + Boolean handleCommand( CommandLine commandLine ) { + + handleCommand(createExecutionContext(commandLine)) + } + + Boolean handleCommand( ExecutionContext context ) { + def console = GrailsConsole.getInstance() + synchronized(GrailsCli) { + try { + currentExecutionContext = context + if (handleBuiltInCommands(context)) { + return true + } + + def mainCommandLine = context.getCommandLine() + if(mainCommandLine.hasOption(CommandLine.STACKTRACE_ARGUMENT)) { + console.setStacktrace(true); + } else { + console.setStacktrace(false); + } + + if(mainCommandLine.hasOption(CommandLine.VERBOSE_ARGUMENT)) { + System.setProperty("grails.verbose", "true") + System.setProperty("grails.full.stacktrace", "true") + } + else { + System.setProperty("grails.verbose", "false") + System.setProperty("grails.full.stacktrace", "false") + } + if (profile.handleCommand(context)) { + if(tiggerAppLoad) { + console.updateStatus("Initializing application. Please wait...") + try { + initializeApplication(context.commandLine) + setupCompleters() + } finally { + tiggerAppLoad = false + } + } + return true; + } + return false + } + catch(Throwable e) { + console.error("Command [${context.commandLine.commandName}] error: ${e.message}", e) + return false + } finally { + currentExecutionContext = null + } + } + } + + + private void handleInteractiveMode() { + GrailsConsole console = setupCompleters() + startInteractiveMode(console) + } + + protected GrailsConsole setupCompleters() { + System.setProperty(Environment.INTERACTIVE_MODE_ENABLED, "true") + GrailsConsole console = projectContext.console + + def consoleReader = console.reader + consoleReader.setHandleUserInterrupt(true) + def completers = aggregateCompleter.getCompleters() + + console.resetCompleters() + // add bang operator completer + completers.add(new ArgumentCompleter( + new RegexCompletor("!\\w+"), new EscapingFileNameCompletor()) + ) + + completers.addAll((profile.getCompleters(projectContext) ?: []) as Collection) + consoleReader.addCompleter(aggregateCompleter) + return console + } + + protected void startInteractiveMode(GrailsConsole console) { + console.updateStatus("Starting interactive mode...") + ExecutorService commandExecutor = Executors.newFixedThreadPool(1) + try { + interactiveModeLoop(console, commandExecutor) + } finally { + commandExecutor.shutdownNow() + } + } + + private void interactiveModeLoop(GrailsConsole console, ExecutorService commandExecutor) { + NonBlockingInputStream nonBlockingInput = (NonBlockingInputStream)console.reader.getInput() + interactiveModeActive = true + boolean firstRun = true + while(keepRunning) { + try { + if(firstRun) { + console.addStatus("Enter a command name to run. Use TAB for completion:") + firstRun = false + } + String commandLine = console.showPrompt() + if(commandLine==null) { + // CTRL-D was pressed, exit interactive mode + exitInteractiveMode() + } else if (commandLine.trim()) { + if(nonBlockingInput.isNonBlockingEnabled()) { + handleCommandWithCancellationSupport(console, commandLine, commandExecutor, nonBlockingInput) + } else { + handleCommand( cliParser.parseString(commandLine)) + } + } + } catch (BuildCancelledException cancelledException) { + console.updateStatus("Build stopped.") + }catch (UserInterruptException e) { + exitInteractiveMode() + } catch (Throwable e) { + console.error "Caught exception ${e.message}", e + } + } + } + + private Boolean handleCommandWithCancellationSupport(GrailsConsole console, String commandLine, ExecutorService commandExecutor, NonBlockingInputStream nonBlockingInput) { + ExecutionContext executionContext = createExecutionContext( cliParser.parseString(commandLine)) + Future commandFuture = commandExecutor.submit({ handleCommand(executionContext) } as Callable) + def terminal = console.reader.terminal + if (terminal instanceof UnixTerminal) { + ((UnixTerminal) terminal).disableInterruptCharacter() + } + try { + while(!commandFuture.done) { + if(nonBlockingInput.nonBlockingEnabled) { + int peeked = nonBlockingInput.peek(100L) + if(peeked > 0) { + // read peeked character from buffer + nonBlockingInput.read(1L) + if(peeked == KEYPRESS_CTRL_C || peeked == KEYPRESS_ESC) { + executionContext.console.log(' ') + executionContext.console.updateStatus("Stopping build. Please wait...") + executionContext.cancel() + } + } + } + } + } finally { + if (terminal instanceof UnixTerminal) { + ((UnixTerminal) terminal).enableInterruptCharacter() + } + } + if(!commandFuture.isCancelled()) { + try { + return commandFuture.get() + } catch (ExecutionException e) { + throw e.cause + } + } else { + return false + } + } + + private initializeProfile() { + BuildSettings.TARGET_DIR?.mkdirs() + + if(!new File(BuildSettings.BASE_DIR, "profile.yml").exists()) { + populateContextLoader() + } + else { + this.profileRepository = createMavenProfileRepository() + } + + String profileName = applicationConfig.get(BuildSettings.PROFILE) ?: getSetting(BuildSettings.PROFILE, String, DEFAULT_PROFILE_NAME) + this.profile = profileRepository.getProfile(profileName) + + if(profile == null) { + throw new IllegalStateException("No profile found for name [$profileName].") + } + } + + protected void populateContextLoader() { + try { + if(new File(BuildSettings.BASE_DIR, "build.gradle").exists()) { + def dependencyMap = new MapReadingCachedGradleOperation>(projectContext, ".dependencies") { + + @Override + void updateStatusMessage() { + GrailsConsole.instance.updateStatus("Resolving Dependencies. Please wait...") + } + + @Override + List createMapValue(Object value) { + if(value instanceof List) { + return ((List)value).collect() { new URL(it.toString()) } as List + } + else { + return [] + } + } + + + @Override + Map> readFromGradle(ProjectConnection connection) { + def config = applicationConfig + + BuildActionExecuter buildActionExecuter = connection.action(new ClasspathBuildAction()) + buildActionExecuter.standardOutput = System.out + buildActionExecuter.standardError = System.err + buildActionExecuter.withArguments("-Dgrails.profile=${config.navigate("grails", "profile")}") + + def grailsClasspath = buildActionExecuter.run() + if(grailsClasspath.error) { + GrailsConsole.instance.error("${grailsClasspath.error} Type 'gradle dependencies' for more information") + exit 1 + } + return [ + dependencies: grailsClasspath.dependencies, + profiles: grailsClasspath.profileDependencies + ] + } + }.call() + + def urls = (List)dependencyMap.get("dependencies") + try { + // add tools.jar + urls.add(new File("${System.getenv('JAVA_HOME')}/lib/tools.jar").toURI().toURL()) + } catch (Throwable e) { + // ignore + } + def profiles = (List)dependencyMap.get("profiles") + URLClassLoader classLoader = new URLClassLoader(urls as URL[], Thread.currentThread().contextClassLoader) + this.profileRepository = new StaticJarProfileRepository(classLoader, profiles as URL[]) + Thread.currentThread().contextClassLoader = classLoader + } + } catch (Throwable e) { + e = ExceptionUtils.getRootCause(e) + GrailsConsole.instance.error("Error initializing classpath: $e.message", e) + exit(1) + } + } + + + private CodeGenConfig loadApplicationConfig() { + CodeGenConfig config = new CodeGenConfig() + File applicationYml = new File("grails-app/conf/application.yml") + File applicationGroovy = new File("grails-app/conf/application.groovy") + if(applicationYml.exists()) { + config.loadYml(applicationYml) + } + if(applicationGroovy.exists()) { + config.loadGroovy(applicationGroovy) + } + config + } + + private boolean handleBuiltInCommands(ExecutionContext context) { + CommandLine commandLine = context.commandLine + def commandName = commandLine.commandName + + if(commandName && commandName.size()>1 && commandName.startsWith('!')) { + return executeProcess(context, commandLine.rawArguments) + } + else { + switch(commandName) { + case '!': + return bang(context) + case 'exit': + exitInteractiveMode() + return true + break + case 'quit': + exitInteractiveMode() + return true + break + } + } + + return false + } + + protected boolean executeProcess(ExecutionContext context, String[] args) { + def console = context.console + try { + args[0] = args[0].substring(1) + def process = new ProcessBuilder(args).redirectErrorStream(true).start() + console.log process.inputStream.getText('UTF-8') + return true + } catch (e) { + console.error "Error occurred executing process: $e.message" + return false + } + } + + /** + * Removes '\' escape characters from the given string. + */ + private String unescape(String str) { + return str.replace('\\', '') + } + + protected Boolean bang(ExecutionContext context) { + def console = context.console + def history = console.reader.history + + //move one step back to ! + history.previous() + + if (!history.previous()) { + console.error "! not valid. Can not repeat without history" + } + + //another step to previous command + String historicalCommand = history.current() + if (historicalCommand.startsWith("!")) { + console.error "Can not repeat command: $historicalCommand" + } + else { + return handleCommand(cliParser.parseString(historicalCommand)) + } + return false + } + + private void exitInteractiveMode() { + keepRunning = false + try { + GradleAsyncInvoker.POOL.shutdownNow() + } catch (Throwable e) { + // ignore + } + } + + + static class ExecutionContextImpl implements ExecutionContext { + CommandLine commandLine + @Delegate(excludes = ['getConsole', 'getBaseDir']) ProjectContext projectContext + GrailsConsole console = GrailsConsole.getInstance() + + ExecutionContextImpl(CodeGenConfig config) { + this(new DefaultCommandLine(), new ProjectContextImpl(GrailsConsole.instance, new File("."), config)) + } + + ExecutionContextImpl(CommandLine commandLine, ProjectContext projectContext) { + this.commandLine = commandLine + this.projectContext = projectContext + if(projectContext?.console) { + console = projectContext.console + } + } + + private List cancelListeners=[] + + @Override //Fully qualified name to work around Groovy bug + void addCancelledListener(org.grails.cli.profile.CommandCancellationListener listener) { + cancelListeners << listener + } + + @Override + void cancel() { + for(CommandCancellationListener listener : cancelListeners) { + try { + listener.commandCancelled() + } catch (Exception e) { + console.error("Error notifying listener about cancelling command", e) + } + } + } + + @Override + File getBaseDir() { + this.projectContext?.baseDir ?: new File(".") + } + } + + @Canonical + private static class ProjectContextImpl implements ProjectContext { + GrailsConsole console = GrailsConsole.getInstance() + File baseDir + CodeGenConfig grailsConfig + + @Override + public String navigateConfig(String... path) { + grailsConfig.navigate(path) + } + + @Override + ConfigMap getConfig() { + return grailsConfig + } + + @Override + public T navigateConfigForType(Class requiredType, String... path) { + grailsConfig.navigate(requiredType, path) + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/boot/GrailsApplicationCompilerAutoConfiguration.java b/grails-shell/src/main/groovy/org/grails/cli/boot/GrailsApplicationCompilerAutoConfiguration.java new file mode 100644 index 00000000000..62349e84a63 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/boot/GrailsApplicationCompilerAutoConfiguration.java @@ -0,0 +1,189 @@ +/* + * Copyright 2014 original 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 org.grails.cli.boot; + +import grails.util.Environment; +import groovy.lang.Grab; +import groovy.lang.GroovyClassLoader; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.ClassExpression; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.stmt.ReturnStatement; +import org.codehaus.groovy.classgen.GeneratorContext; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.springframework.boot.cli.compiler.AstUtils; +import org.springframework.boot.cli.compiler.CompilerAutoConfiguration; +import org.springframework.boot.cli.compiler.DependencyCustomizer; +import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration; +import org.springframework.boot.cli.compiler.autoconfigure.SpringMvcCompilerAutoConfiguration; +import org.springframework.boot.cli.compiler.dependencies.Dependency; +import org.springframework.boot.cli.compiler.dependencies.DependencyManagement; +import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext; + +import java.lang.reflect.Modifier; +import java.util.*; + + +/** + * A {@link org.springframework.boot.cli.compiler.CompilerAutoConfiguration} for Grails Micro Service applications + * + * @author Graeme Rocher + * @since 3.0 + */ +public class GrailsApplicationCompilerAutoConfiguration extends CompilerAutoConfiguration { + + public static final String[] DEFAULT_IMPORTS = new String[]{ + "grails.persistence", + "grails.gorm", + "grails.rest", + "grails.artefact", + "grails.web", + "grails.boot.config" }; + public static final String ENABLE_AUTO_CONFIGURATION = "org.springframework.boot.autoconfigure.EnableAutoConfiguration"; + public static final ClassNode ENABLE_AUTO_CONFIGURATION_CLASS_NODE = ClassHelper.make(ENABLE_AUTO_CONFIGURATION); + ClassNode lastMatch = null; + + @Override + public boolean matches(ClassNode classNode) { + boolean matches = AstUtils.hasAtLeastOneAnnotation(classNode, "grails.persistence.Entity", "grails.rest.Resource", "Resource", "grails.artefact.Artefact", "grails.web.Controller"); + if(matches) lastMatch = classNode; + return matches; + } + + + @Override + public void applyDependencies(DependencyCustomizer dependencies) throws CompilationFailedException { + addManagedDependencies(dependencies); + if(lastMatch != null) { + lastMatch.addAnnotation(createGrabAnnotation("org.grails", "grails-dependencies", Environment.class.getPackage().getImplementationVersion(), null, "pom", true)); + lastMatch.addAnnotation(createGrabAnnotation("org.grails", "grails-web-boot", Environment.class.getPackage().getImplementationVersion(), null, null, true)); + } + new SpringMvcCompilerAutoConfiguration().applyDependencies(dependencies); + } + + private void addManagedDependencies(DependencyCustomizer dependencies) { + final List current = dependencies + .getDependencyResolutionContext().getManagedDependencies(); + final DependencyResolutionContext resolutionContext = dependencies.getDependencyResolutionContext(); + resolutionContext.addDependencyManagement(new GrailsDependencies(current)); + resolutionContext.addDependencyManagement(getAdditionalDependencies()); + } + + protected DependencyManagement getAdditionalDependencies() { + return new GrailsDependencyVersions(); + } + + + public static AnnotationNode createGrabAnnotation(String group, String module, + String version, String classifier, String type, boolean transitive) { + AnnotationNode annotationNode = new AnnotationNode(new ClassNode(Grab.class)); + annotationNode.addMember("group", new ConstantExpression(group)); + annotationNode.addMember("module", new ConstantExpression(module)); + annotationNode.addMember("version", new ConstantExpression(version)); + if (classifier != null) { + annotationNode.addMember("classifier", new ConstantExpression(classifier)); + } + if (type != null) { + annotationNode.addMember("type", new ConstantExpression(type)); + } + annotationNode.addMember("transitive", new ConstantExpression(transitive)); + annotationNode.addMember("initClass", new ConstantExpression(false)); + return annotationNode; + } + + + @Override + public void applyImports(ImportCustomizer imports) throws CompilationFailedException { + imports.addStarImports(DEFAULT_IMPORTS); + new SpringMvcCompilerAutoConfiguration().applyImports(imports); + } + + @Override + public void applyToMainClass(GroovyClassLoader loader, GroovyCompilerConfiguration configuration, GeneratorContext generatorContext, SourceUnit source, ClassNode classNode) throws CompilationFailedException { + + // if we arrive here then there is no 'Application' class and we need to add one automatically + ClassNode applicationClassNode = new ClassNode("Application", Modifier.PUBLIC, ClassHelper.make("grails.boot.config.GrailsAutoConfiguration")); + AnnotationNode enableAutoAnnotation = new AnnotationNode(ENABLE_AUTO_CONFIGURATION_CLASS_NODE); + try { + enableAutoAnnotation.addMember("exclude", new ClassExpression( ClassHelper.make("org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration")) ); + } catch (Throwable e) { + // ignore + } + applicationClassNode.addAnnotation(enableAutoAnnotation); + applicationClassNode.setModule(source.getAST()); + applicationClassNode.addMethod("shouldScanDefaultPackage", Modifier.PUBLIC, ClassHelper.Boolean_TYPE, new Parameter[0], null, new ReturnStatement(new ConstantExpression(Boolean.TRUE))); + source.getAST().getClasses().add(0, applicationClassNode); + classNode.addAnnotation(new AnnotationNode(ClassHelper.make("org.grails.boot.internal.EnableAutoConfiguration"))); + } + + class GrailsDependencies implements DependencyManagement { + + private Map groupAndArtifactToDependency = new HashMap(); + + private Map artifactToGroupAndArtifact = new HashMap(); + private List dependencies = new ArrayList(); + + public GrailsDependencies(List dependencies) { + for (org.eclipse.aether.graph.Dependency dependency : dependencies) { + String groupId = dependency.getArtifact().getGroupId(); + String artifactId = dependency.getArtifact().getArtifactId(); + String version = dependency.getArtifact().getVersion(); + + List exclusions = new ArrayList(); + Dependency value = new Dependency(groupId, artifactId, version, exclusions); + this.dependencies.add(value); + groupAndArtifactToDependency.put(groupId + ":" + artifactId, value); + artifactToGroupAndArtifact.put(artifactId, groupId + ":" + artifactId); + } + } + +// @Override +// public Dependency find(String groupId, String artifactId) { +// return groupAndArtifactToDependency.get(groupId + ":" + artifactId); +// } + + + @Override + public List getDependencies() { + return dependencies; + } + + @Override + public String getSpringBootVersion() { + return find("spring-boot").getVersion(); + } + + @Override + public Dependency find(String artifactId) { + String groupAndArtifact = artifactToGroupAndArtifact.get(artifactId); + if (groupAndArtifact==null) { + return null; + } + return groupAndArtifactToDependency.get(groupAndArtifact); + } + +// @Override +// public Iterator iterator() { +// return groupAndArtifactToDependency.values().iterator(); +// } + } + +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/boot/GrailsDependencyVersions.groovy b/grails-shell/src/main/groovy/org/grails/cli/boot/GrailsDependencyVersions.groovy new file mode 100644 index 00000000000..f0704d4589c --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/boot/GrailsDependencyVersions.groovy @@ -0,0 +1,113 @@ +/* + * Copyright 2014 original 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 org.grails.cli.boot + +import groovy.grape.Grape +import groovy.grape.GrapeEngine +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.util.slurpersupport.GPathResult +import org.springframework.boot.cli.compiler.dependencies.Dependency +import org.springframework.boot.cli.compiler.dependencies.DependencyManagement + + +/** + * Introduces dependency management based on a published BOM file + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class GrailsDependencyVersions implements DependencyManagement { + + protected Map groupAndArtifactToDependency = [:] + protected Map artifactToGroupAndArtifact = [:] + protected List dependencies = [] + protected Map versionProperties = [:] + + GrailsDependencyVersions() { + this(getDefaultEngine()) + } + + GrailsDependencyVersions(Map bomCoords) { + this(getDefaultEngine(), bomCoords) + } + + GrailsDependencyVersions(GrapeEngine grape) { + this(grape, [group: "org.grails", module: "grails-bom", version: GrailsDependencyVersions.package.implementationVersion, type: "pom"]) + } + + GrailsDependencyVersions(GrapeEngine grape, Map bomCoords) { + def results = grape.resolve(null, bomCoords) + + for(URI u in results) { + def pom = new XmlSlurper().parseText(u.toURL().text) + addDependencyManagement(pom) + } + } + + static GrapeEngine getDefaultEngine() { + def grape = Grape.getInstance() + grape.addResolver([name:"grailsCentral", root:"https://repo.grails.org/grails/core"] as Map) + grape + } + + @CompileDynamic + void addDependencyManagement(GPathResult pom) { + pom.dependencyManagement.dependencies.dependency.each { dep -> + addDependency(dep.groupId.text(), dep.artifactId.text(), dep.version.text()) + } + versionProperties = pom.properties.'*'.collectEntries { [(it.name()): it.text()] } + } + + protected void addDependency(String group, String artifactId, String version) { + def groupAndArtifactId = "$group:$artifactId".toString() + artifactToGroupAndArtifact[artifactId] = groupAndArtifactId + + def dep = new Dependency(group, artifactId, version) + dependencies.add(dep) + groupAndArtifactToDependency[groupAndArtifactId] = dep + } + + Dependency find(String groupId, String artifactId) { + return groupAndArtifactToDependency["$groupId:$artifactId".toString()] + } + + @Override + List getDependencies() { + return dependencies + } + + Map getVersionProperties() { + return versionProperties + } + + @Override + String getSpringBootVersion() { + return find("spring-boot").getVersion() + } + + @Override + Dependency find(String artifactId) { + def groupAndArtifact = artifactToGroupAndArtifact[artifactId] + if(groupAndArtifact) + return groupAndArtifactToDependency[groupAndArtifact] + } + + Iterator iterator() { + return groupAndArtifactToDependency.values().iterator() + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/boot/GrailsTestCompilerAutoConfiguration.groovy b/grails-shell/src/main/groovy/org/grails/cli/boot/GrailsTestCompilerAutoConfiguration.groovy new file mode 100644 index 00000000000..794a2184776 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/boot/GrailsTestCompilerAutoConfiguration.groovy @@ -0,0 +1,66 @@ +/* + * Copyright 2014 original 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 org.grails.cli.boot + +import grails.util.Environment +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.AnnotationNode +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.control.CompilationFailedException +import org.codehaus.groovy.control.customizers.ImportCustomizer +import org.springframework.boot.cli.compiler.AstUtils +import org.springframework.boot.cli.compiler.CompilerAutoConfiguration +import org.springframework.boot.cli.compiler.DependencyCustomizer + + +/** + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class GrailsTestCompilerAutoConfiguration extends CompilerAutoConfiguration { + + public static final String[] DEFAULT_IMPORTS = [ + "spock.lang", + "grails.test.mixin", + "grails.test.mixin.integration", + "grails.test.mixin.support", + "grails.artefact" ] as String[] + + ClassNode lastMatch = null + + @Override + boolean matches(ClassNode classNode) { + def matches = AstUtils.subclasses(classNode, "Specification") + if(matches) { + lastMatch = classNode + } + return matches + } + + @Override + void applyImports(ImportCustomizer imports) throws CompilationFailedException { + imports.addStarImports(DEFAULT_IMPORTS); + } + + @Override + void applyDependencies(DependencyCustomizer dependencies) throws CompilationFailedException { + if(lastMatch != null) { + def annotation = GrailsApplicationCompilerAutoConfiguration.createGrabAnnotation("org.grails", "grails-plugin-testing", Environment.class.getPackage().getImplementationVersion(), null, null, true) + lastMatch.addAnnotation(annotation); + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/boot/SpringInvoker.groovy b/grails-shell/src/main/groovy/org/grails/cli/boot/SpringInvoker.groovy new file mode 100644 index 00000000000..f6b246e8e69 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/boot/SpringInvoker.groovy @@ -0,0 +1,101 @@ +/* + * Copyright 2014 original 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 org.grails.cli.boot + +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors +import org.springframework.boot.cli.command.CommandFactory +import org.springframework.boot.cli.command.CommandRunner + + +/** + * Allows invocation of Spring commands from command scripts + * + * @author Graeme Rocher + * @since 3.0 + */ +@Singleton(strict = false) +@CompileStatic +class SpringInvoker { + + CommandRunner runner = new CommandRunner("spring"); + + private SpringInvoker() { + addServiceLoaderCommands(runner) + } + + private static void addServiceLoaderCommands(CommandRunner runner) { + ServiceLoader factories = ServiceLoader.load( + CommandFactory.class, Thread.currentThread().contextClassLoader) + factories.each { CommandFactory factory -> + runner.addCommands factory.getCommands() + } + } + + @Override + Object invokeMethod(String name, Object args) { + if(args instanceof Object[]) { + + List argList = [name] + argList.addAll( ((Object[])args).collect() { it.toString() } ) + + def currentThread = Thread.currentThread() + def existing = currentThread.contextClassLoader + try { + currentThread.contextClassLoader = new Slf4jBindingAwareClassLoader() + return runner.runAndHandleErrors(argList as String[]) + } finally { + currentThread.contextClassLoader = existing + } + } + return null + } + + @InheritConstructors + static class Slf4jBindingAwareClassLoader extends URLClassLoader { + @Override + Enumeration getResources(String name) throws IOException { + if("org/slf4j/impl/StaticLoggerBinder.class" == name) { + def resources = super.getResources(name) + def oneRes = (URL)resources.find() { URL url -> !url.toString().contains('slf4j-simple') } + if(oneRes) { + + return new Enumeration() { + URL current = oneRes + @Override + boolean hasMoreElements() { + current != null + } + + @Override + URL nextElement() { + URL i = current + current = null + return i + } + } + } + else { + return resources + } + } + else { + return super.getResources(name) + } + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/ClasspathBuildAction.groovy b/grails-shell/src/main/groovy/org/grails/cli/gradle/ClasspathBuildAction.groovy new file mode 100644 index 00000000000..7f884ea6d6c --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/ClasspathBuildAction.groovy @@ -0,0 +1,33 @@ +/* + * Copyright 2014 original 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 org.grails.cli.gradle + +import org.gradle.tooling.BuildAction +import org.gradle.tooling.BuildController +import org.grails.gradle.plugin.model.GrailsClasspath + +/** + * Gets the EclipseProject which helps obtain the classpath necessary + * + * @author Graeme Rocher + * @since 3.0 + */ +class ClasspathBuildAction implements BuildAction, Serializable { + @Override + GrailsClasspath execute(BuildController controller) { + controller.getModel(GrailsClasspath) + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/FetchAllTaskSelectorsBuildAction.java b/grails-shell/src/main/groovy/org/grails/cli/gradle/FetchAllTaskSelectorsBuildAction.java new file mode 100644 index 00000000000..1fb440e1828 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/FetchAllTaskSelectorsBuildAction.java @@ -0,0 +1,85 @@ +/* + * Copyright 2014 original 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 org.grails.cli.gradle; +import java.io.File; +import java.io.Serializable; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.gradle.tooling.BuildAction; +import org.gradle.tooling.BuildController; +import org.gradle.tooling.model.Task; +import org.gradle.tooling.model.TaskSelector; +import org.gradle.tooling.model.gradle.BasicGradleProject; +import org.gradle.tooling.model.gradle.BuildInvocations; +import org.grails.cli.gradle.FetchAllTaskSelectorsBuildAction.AllTasksModel; + +/** + * A {@link org.gradle.tooling.BuildAction} that calculates all the tasks from the Gradle build + * + * @author Lari Hotari + * @since 3.0 + * + */ +public class FetchAllTaskSelectorsBuildAction implements BuildAction { + private static final long serialVersionUID = 1L; + private final String currentProjectPath; + + public FetchAllTaskSelectorsBuildAction(File currentProjectDir) { + this.currentProjectPath = currentProjectDir.getAbsolutePath(); + } + + public AllTasksModel execute(BuildController controller) { + AllTasksModel model = new AllTasksModel(); + Map> allTaskSelectors = new LinkedHashMap>(); + model.allTaskSelectors = allTaskSelectors; + Map> allTasks = new LinkedHashMap>(); + model.allTasks = allTasks; + Map projectPaths = new HashMap(); + model.projectPaths = projectPaths; + for (BasicGradleProject project: controller.getBuildModel().getProjects()) { + BuildInvocations entryPointsForProject = controller.getModel(project, BuildInvocations.class); + Set selectorNames = new LinkedHashSet(); + for (TaskSelector selector : entryPointsForProject.getTaskSelectors()) { + selectorNames.add(selector.getName()); + } + allTaskSelectors.put(project.getName(), selectorNames); + + Set taskNames = new LinkedHashSet(); + for (Task task : entryPointsForProject.getTasks()) { + taskNames.add(task.getName()); + } + allTasks.put(project.getName(), taskNames); + + projectPaths.put(project.getName(), project.getPath()); + if(project.getProjectDirectory().getAbsolutePath().equals(currentProjectPath)) { + model.currentProject = project.getName(); + } + } + return model; + } + + public static class AllTasksModel implements Serializable { + private static final long serialVersionUID = 1L; + public Map> allTasks; + public Map> allTaskSelectors; + public Map projectPaths; + public String currentProject; + } +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/GradleAsyncInvoker.groovy b/grails-shell/src/main/groovy/org/grails/cli/gradle/GradleAsyncInvoker.groovy new file mode 100644 index 00000000000..45b05bd39a1 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/GradleAsyncInvoker.groovy @@ -0,0 +1,62 @@ +/* + * Copyright 2014 original 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 org.grails.cli.gradle + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + + + +/** + * @author Graeme Rocher + */ +@CompileStatic +class GradleAsyncInvoker { + GradleInvoker invoker + + public static final ExecutorService POOL = Executors.newFixedThreadPool(4); + + static { + Runtime.addShutdownHook { + try { + Thread.start { + if(!POOL.isTerminated()) { + POOL.shutdownNow() + } + }.join(1000) + } catch (Throwable e) { + // ignore + } + } + } + + GradleAsyncInvoker(GradleInvoker invoker) { + this.invoker = invoker + } + + @Override + @CompileDynamic + Object invokeMethod(String name, Object args) { + POOL.submit { + invoker."$name"(*args) + } + } + + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/GradleInvoker.groovy b/grails-shell/src/main/groovy/org/grails/cli/gradle/GradleInvoker.groovy new file mode 100644 index 00000000000..b5f707819f8 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/GradleInvoker.groovy @@ -0,0 +1,72 @@ +/* + * Copyright 2014 original 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 org.grails.cli.gradle + +import grails.util.Environment +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.gradle.tooling.BuildLauncher +import org.grails.build.parsing.CommandLine +import org.grails.cli.profile.ExecutionContext + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.FutureTask + + +/** + * Allow dynamic invocation of Gradle tasks + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class GradleInvoker { + + ExecutionContext executionContext + + GradleInvoker(ExecutionContext executionContext) { + this.executionContext = executionContext + } + + @Override + Object invokeMethod(String name, Object args) { + Object[] argArray = (Object[]) args + + + GradleUtil.runBuildWithConsoleOutput(executionContext) { BuildLauncher buildLauncher -> + buildLauncher.forTasks(name.split(' ')) + List arguments = [] + arguments << "-Dgrails.env=${Environment.current.name}".toString() + + + def commandLine = executionContext.commandLine + if(commandLine.hasOption(CommandLine.STACKTRACE_ARGUMENT)) { + arguments << '--stacktrace' + arguments << '-Dgrails.full.stacktrace=true' + } + + arguments.addAll argArray.collect() { it.toString() } + buildLauncher.withArguments( arguments as String[]) + } + } + + GradleAsyncInvoker getAsync() { + return new GradleAsyncInvoker(this) + } + + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/GradleUtil.groovy b/grails-shell/src/main/groovy/org/grails/cli/gradle/GradleUtil.groovy new file mode 100644 index 00000000000..ba4b672bbcc --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/GradleUtil.groovy @@ -0,0 +1,140 @@ +/* + * Copyright 2014 original 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 org.grails.cli.gradle + +import grails.build.logging.GrailsConsole +import grails.io.support.SystemOutErrCapturer +import grails.io.support.SystemStreamsRedirector +import groovy.transform.CompileStatic +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.FromString +import groovy.transform.stc.SimpleType +import org.gradle.tooling.BuildAction +import org.gradle.tooling.BuildActionExecuter +import org.gradle.tooling.BuildLauncher +import org.gradle.tooling.GradleConnector +import org.gradle.tooling.LongRunningOperation +import org.gradle.tooling.ProjectConnection +import org.gradle.tooling.internal.consumer.DefaultCancellationTokenSource +import org.grails.build.logging.GrailsConsoleErrorPrintStream +import org.grails.build.logging.GrailsConsolePrintStream +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.ProjectContext + +/** + * Utility methods for interacting with Gradle + * + * @since 3.0 + * @author Graeme Rocher + * @author Lari Hotari + */ +@CompileStatic +class GradleUtil { + private static final boolean DEFAULT_SUPPRESS_OUTPUT = true + + public static ProjectConnection openGradleConnection(File baseDir) { + GradleConnector gradleConnector = GradleConnector.newConnector().forProjectDirectory(baseDir) + if (System.getenv("GRAILS_GRADLE_HOME")) { + gradleConnector.useInstallation(new File(System.getenv("GRAILS_GRADLE_HOME"))) + } else { + def userHome = System.getProperty("user.home") + if (userHome) { + File gradleFile = new File(baseDir, "gradle.properties") + if (gradleFile.exists() && gradleFile.canRead()) { + Properties gradleProperties = new Properties() + gradleProperties.load(gradleFile.newInputStream()) + String gradleWrapperVersion = gradleProperties.getProperty("gradleWrapperVersion") + + File sdkManGradle = new File("$userHome/.sdkman/candidates/gradle/$gradleWrapperVersion") + if (sdkManGradle.exists() && sdkManGradle.isDirectory()) { + gradleConnector.useInstallation(sdkManGradle) + } + } + } + } + + gradleConnector.connect() + } + + public static T withProjectConnection(File baseDir, boolean suppressOutput = DEFAULT_SUPPRESS_OUTPUT, + @ClosureParams(value = SimpleType.class, options = "org.gradle.tooling.ProjectConnection") Closure closure) { + ProjectConnection projectConnection = openGradleConnection(baseDir) + try { + if (suppressOutput) { + SystemOutErrCapturer.withNullOutput { + closure(projectConnection) + } + } else { + SystemStreamsRedirector.withOriginalIO { + closure(projectConnection) + } + } + } finally { + projectConnection.close(); + } + } + + public static void runBuildWithConsoleOutput(ExecutionContext context, + @ClosureParams(value = SimpleType.class, options = "org.gradle.tooling.BuildLauncher") Closure buildLauncherCustomizationClosure) { + withProjectConnection(context.getBaseDir(), DEFAULT_SUPPRESS_OUTPUT) { ProjectConnection projectConnection -> + BuildLauncher launcher = projectConnection.newBuild() + setupConsoleOutput(context, launcher) + wireCancellationSupport(context, launcher) + buildLauncherCustomizationClosure.call(launcher) + launcher.run() + } + } + + public static LongRunningOperation setupConsoleOutput(ProjectContext context, LongRunningOperation operation) { + GrailsConsole grailsConsole = context.console + operation.colorOutput = grailsConsole.ansiEnabled + operation.standardOutput = new GrailsConsolePrintStream( grailsConsole.out ) + operation.standardError = new GrailsConsoleErrorPrintStream( grailsConsole.err ) + operation + } + + public static T runBuildActionWithConsoleOutput(ProjectContext context, BuildAction buildAction) { + // workaround for GROOVY-7211, static type checking problem when default parameters are used + runBuildActionWithConsoleOutput(context, buildAction, null) + } + + public static T runBuildActionWithConsoleOutput(ProjectContext context, BuildAction buildAction, + @ClosureParams(value = FromString.class, options = "org.gradle.tooling.BuildActionExecuter") Closure buildActionExecuterCustomizationClosure) { + withProjectConnection(context.getBaseDir(), DEFAULT_SUPPRESS_OUTPUT) { ProjectConnection projectConnection -> + runBuildActionWithConsoleOutput(projectConnection, context, buildAction, buildActionExecuterCustomizationClosure) + } + } + + public static T runBuildActionWithConsoleOutput(ProjectConnection connection, ProjectContext context, BuildAction buildAction) { + // workaround for GROOVY-7211, static type checking problem when default parameters are used + runBuildActionWithConsoleOutput(connection, context, buildAction, null) + } + + public static T runBuildActionWithConsoleOutput(ProjectConnection connection, ProjectContext context, BuildAction buildAction, @ClosureParams(value=FromString.class, options="org.gradle.tooling.BuildActionExecuter") Closure buildActionExecuterCustomizationClosure) { + BuildActionExecuter buildActionExecuter = connection.action(buildAction) + setupConsoleOutput(context, buildActionExecuter) + buildActionExecuterCustomizationClosure?.call(buildActionExecuter) + return buildActionExecuter.run() + } + + public static wireCancellationSupport(ExecutionContext context, BuildLauncher buildLauncher) { + DefaultCancellationTokenSource cancellationTokenSource = new DefaultCancellationTokenSource() + buildLauncher.withCancellationToken(cancellationTokenSource.token()) + context.addCancelledListener({ + cancellationTokenSource.cancel() + }) + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/cache/CachedGradleOperation.groovy b/grails-shell/src/main/groovy/org/grails/cli/gradle/cache/CachedGradleOperation.groovy new file mode 100644 index 00000000000..e38f6249a38 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/cache/CachedGradleOperation.groovy @@ -0,0 +1,97 @@ +/* + * Copyright 2014 original 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 org.grails.cli.gradle.cache + +import grails.util.BuildSettings +import groovy.transform.CompileStatic +import org.gradle.tooling.GradleConnector +import org.gradle.tooling.ProjectConnection +import org.gradle.tooling.internal.consumer.ConnectorServices +import org.gradle.tooling.internal.consumer.DefaultGradleConnector +import org.grails.cli.gradle.GradleUtil +import org.grails.cli.profile.ProjectContext + +import java.util.concurrent.Callable + +/** + * Utility class for performing cached operations that retrieve data from Gradle. Since these operations are expensive we want to cache the data to avoid unnecessarily calling Gradle + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +abstract class CachedGradleOperation implements Callable { + + protected String fileName + protected ProjectContext projectContext + + CachedGradleOperation(ProjectContext projectContext, String fileName) { + this.fileName = fileName + this.projectContext = projectContext + } + + abstract T readFromCached(File f) + + abstract void writeToCache(PrintWriter writer, T data) + + abstract T readFromGradle(ProjectConnection connection) + + @Override + T call() throws Exception { + def depsFile = new File(BuildSettings.TARGET_DIR, fileName) + try { + if(depsFile.exists() && depsFile.lastModified() > new File(projectContext.baseDir, "build.gradle").lastModified()) { + T cached = readFromCached(depsFile) + if(cached) { + return cached + } + + } + } catch (Throwable e) { + throw e + } + + try { + ProjectConnection projectConnection = GradleUtil.openGradleConnection(projectContext.baseDir) + try { + updateStatusMessage() + def data = readFromGradle(projectConnection) + storeData(data) + return data + } finally { + projectConnection.close() + } + } finally { + DefaultGradleConnector.close() + ConnectorServices.reset() + } + } + + void updateStatusMessage() { + // no-op + } + + protected void storeData(T data) { + try { + def depsFile = new File(BuildSettings.TARGET_DIR, fileName) + depsFile.withPrintWriter { PrintWriter writer -> + writeToCache(writer, data) + } + } catch (Throwable e) { + // ignore + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/cache/ListReadingCachedGradleOperation.groovy b/grails-shell/src/main/groovy/org/grails/cli/gradle/cache/ListReadingCachedGradleOperation.groovy new file mode 100644 index 00000000000..dbaf84b2bad --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/cache/ListReadingCachedGradleOperation.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2014 original 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 org.grails.cli.gradle.cache + +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors +import org.grails.cli.profile.ProjectContext + + +/** + * A {@link CachedGradleOperation} that reads and writes a list of values + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +@InheritConstructors +abstract class ListReadingCachedGradleOperation extends CachedGradleOperation>{ + + @Override + List readFromCached(File f) { + return f.text.split('\n').collect() { String str -> createListEntry(str) } + } + + protected abstract T createListEntry(String str) + + @Override + void writeToCache(PrintWriter writer, List data) { + for (url in data) writer.println(url.toString()) + } + + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/cache/MapReadingCachedGradleOperation.groovy b/grails-shell/src/main/groovy/org/grails/cli/gradle/cache/MapReadingCachedGradleOperation.groovy new file mode 100644 index 00000000000..fa0adaef5cc --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/cache/MapReadingCachedGradleOperation.groovy @@ -0,0 +1,68 @@ +/* + * Copyright 2015 original 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 org.grails.cli.gradle.cache + +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors +import org.gradle.tooling.ProjectConnection +import org.yaml.snakeyaml.DumperOptions +import org.yaml.snakeyaml.LoaderOptions +import org.yaml.snakeyaml.Yaml +import org.yaml.snakeyaml.constructor.SafeConstructor +import org.yaml.snakeyaml.representer.Representer + + +/** + * Cached Gradle operation that reads a Map + * + * @author Graeme Rocher + * @since 3.1 + */ +@InheritConstructors +@CompileStatic +abstract class MapReadingCachedGradleOperation extends CachedGradleOperation> { + @Override + Map readFromCached(File f) { + def map = (Map) f.withReader { BufferedReader r -> + new Yaml(new SafeConstructor(new LoaderOptions())).load(r) + } + Map newMap = [:] + + for(entry in map.entrySet()) { + newMap.put(entry.key, createMapValue(entry.value)) + } + return newMap + } + + abstract V createMapValue(Object value) + + @Override + void writeToCache(PrintWriter writer, Map data) { + def options = new DumperOptions() + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK) + Map toWrite = data.collectEntries { String key, V val -> + if(val instanceof Iterable) { + return [(key):val.collect() { it.toString() }] + } + else { + return [(key):val.toString()] + } + } + new Yaml(new SafeConstructor(new LoaderOptions()), new Representer(options), options).dump(toWrite, writer) + } + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy new file mode 100644 index 00000000000..04a2c0169ab --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy @@ -0,0 +1,72 @@ +/* + * Copyright 2014 original 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 org.grails.cli.gradle.commands +import groovy.transform.CompileStatic +import jline.console.completer.Completer +import org.gradle.tooling.BuildLauncher +import org.grails.cli.gradle.GradleUtil +import org.grails.cli.interactive.completers.ClosureCompleter +import org.grails.cli.profile.* +/** + * A command for invoking Gradle commands + * + * @author Graeme Rocher + */ +@CompileStatic +class GradleCommand implements ProjectCommand, Completer, ProjectContextAware { + public static final String GRADLE = "gradle" + + final String name = GRADLE + final CommandDescription description = new CommandDescription(name, "Allows running of Gradle tasks", "gradle [task name]") + ProjectContext projectContext + + private ReadGradleTasks readTasks + private Completer completer + + void setProjectContext(ProjectContext projectContext) { + this.projectContext = projectContext + initializeCompleter() + } + + @Override + boolean handle(ExecutionContext context) { + GradleUtil.runBuildWithConsoleOutput(context) { BuildLauncher buildLauncher -> + def args = context.commandLine.remainingArgsString?.trim() + if(args) { + buildLauncher.withArguments(args) + } + } + return true + } + + @Override + int complete(String buffer, int cursor, List candidates) { + initializeCompleter() + + if(completer) + return completer.complete(buffer, cursor, candidates) + else + return cursor + } + + private void initializeCompleter() { + if (completer == null && projectContext) { + readTasks = new ReadGradleTasks(projectContext) + completer = new ClosureCompleter((Closure>) { readTasks.call() }) + } + } + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/commands/GradleTaskCommandAdapter.groovy b/grails-shell/src/main/groovy/org/grails/cli/gradle/commands/GradleTaskCommandAdapter.groovy new file mode 100644 index 00000000000..c50ff2a2e1a --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/commands/GradleTaskCommandAdapter.groovy @@ -0,0 +1,77 @@ +/* + * Copyright 2014 original 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 org.grails.cli.gradle.commands + +import grails.util.Described +import grails.util.GrailsNameUtils +import grails.util.Named +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.grails.cli.gradle.GradleInvoker +import org.grails.cli.profile.CommandDescription +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileCommand + +/** + * Adapts a {@link Named} command into a Gradle task execution + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class GradleTaskCommandAdapter implements ProfileCommand { + + Profile profile + final Named adapted + + GradleTaskCommandAdapter(Profile profile, Named adapted) { + this.profile = profile + this.adapted = adapted + } + + @Override + CommandDescription getDescription() { + String description + if(adapted instanceof Described) { + description = ((Described)adapted).description + } + else { + description = "" + } + return new CommandDescription(adapted.name, description) + } + + @Override + @CompileDynamic + boolean handle(ExecutionContext executionContext) { + GradleInvoker invoker = new GradleInvoker(executionContext) + + def commandLine = executionContext.commandLine + if (commandLine.remainingArgs || commandLine.undeclaredOptions) { + invoker."${GrailsNameUtils.getPropertyNameForLowerCaseHyphenSeparatedName(adapted.name)}"("-Pargs=${commandLine.remainingArgsWithOptionsString}") + } else { + invoker."${GrailsNameUtils.getPropertyNameForLowerCaseHyphenSeparatedName(adapted.name)}"() + } + + return true + } + + @Override + String getName() { + return adapted.name + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/gradle/commands/ReadGradleTasks.groovy b/grails-shell/src/main/groovy/org/grails/cli/gradle/commands/ReadGradleTasks.groovy new file mode 100644 index 00000000000..c9d2778e6bf --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/gradle/commands/ReadGradleTasks.groovy @@ -0,0 +1,68 @@ + +/* + * Copyright 2014 original 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 org.grails.cli.gradle.commands + +import grails.io.support.SystemOutErrCapturer +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors +import org.gradle.tooling.ProjectConnection +import org.grails.cli.gradle.FetchAllTaskSelectorsBuildAction +import org.grails.cli.gradle.cache.ListReadingCachedGradleOperation +import org.grails.cli.profile.ProjectContext + +/** + * @author Graeme Rocher + */ +@CompileStatic +class ReadGradleTasks extends ListReadingCachedGradleOperation { + + private static final Closure taskNameFormatter = { String projectPath, String taskName -> + if(projectPath == ':') { + ":$taskName".toString() + } else { + "$projectPath:$taskName".toString() + } + } + + ReadGradleTasks(ProjectContext projectContext) { + super(projectContext, ".gradle-tasks") + } + + @Override + protected String createListEntry(String str) { str } + + @Override + List readFromGradle(ProjectConnection connection) { + SystemOutErrCapturer.withNullOutput { + FetchAllTaskSelectorsBuildAction.AllTasksModel allTasksModel = (FetchAllTaskSelectorsBuildAction.AllTasksModel)connection.action(new FetchAllTaskSelectorsBuildAction(projectContext.getBaseDir())).run() + Collection allTaskSelectors=[] + + if (allTasksModel.currentProject) { + allTaskSelectors.addAll(allTasksModel.allTaskSelectors.get(allTasksModel.currentProject)) + } + + allTasksModel.projectPaths.each { String projectName, String projectPath -> + allTasksModel.allTasks.get(projectName).each { String taskName -> + allTaskSelectors.add(taskNameFormatter(projectPath, taskName)) + } + } + + allTaskSelectors.unique().toList() + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/AllClassCompleter.groovy b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/AllClassCompleter.groovy new file mode 100644 index 00000000000..e4d19394245 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/AllClassCompleter.groovy @@ -0,0 +1,34 @@ +/* + * Copyright 2014 original 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 org.grails.cli.interactive.completers + +import grails.util.BuildSettings + + + +/** + * A completer that completes all classes in the project + * + * @author Graeme Rocher + * @since 3.0 + */ +class AllClassCompleter extends ClassNameCompleter { + AllClassCompleter() { + super(new File(BuildSettings.BASE_DIR, "grails-app") + ?.listFiles() + ?.findAll() { File f -> f.isDirectory() && !f.isHidden() && !f.name.startsWith('.') } as File[]) + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/ClassNameCompleter.groovy b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/ClassNameCompleter.groovy new file mode 100644 index 00000000000..e599e44cde2 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/ClassNameCompleter.groovy @@ -0,0 +1,99 @@ +/* + * Copyright 2014 original 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 org.grails.cli.interactive.completers + +import groovy.transform.CompileStatic +import org.grails.io.support.PathMatchingResourcePatternResolver +import org.grails.io.support.Resource + +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.ConcurrentSkipListSet + + +/** + * A completer that completes class names + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class ClassNameCompleter extends StringsCompleter { + + private static Map> RESOURCE_SCAN_CACHE = [:] + private static Collection allCompeters = new ConcurrentLinkedQueue<>() + PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver() + + private File[] baseDirs + + ClassNameCompleter(File baseDir) { + initialize(baseDir) + } + + ClassNameCompleter(File... baseDirs) { + initialize(baseDirs) + } + + static void refreshAll() { + Thread.start { + RESOURCE_SCAN_CACHE.clear() + Collection competers = new ArrayList<>(allCompeters) + for (ClassNameCompleter completer : competers) { + completer.refresh() + } + } + } + + private void refresh() { + if(!baseDirs) return + initialize(baseDirs) + } + + private void initialize(File... baseDirs) { + try { + if(!baseDirs) return + this.baseDirs = baseDirs + if(!allCompeters.contains(this)) + allCompeters << this + SortedSet allStrings = new ConcurrentSkipListSet<>() + for(File baseDir in baseDirs) { + def pattern = "file:${baseDir}/**/*.groovy".toString() + SortedSet strings = RESOURCE_SCAN_CACHE[pattern] + if(strings == null) { + strings = new TreeSet<>() + RESOURCE_SCAN_CACHE[pattern] = strings + def resources = resourcePatternResolver.getResources(pattern) + for (res in resources) { + if(isValidResource(res)) { + def path = res.file.canonicalPath + def basePath = baseDir.canonicalPath + path = (path - basePath)[1..-8] + path = path.replace(File.separatorChar, '.' as char) + strings << path + } + } + } + allStrings.addAll(strings) + } + setStrings(allStrings) + } catch (Throwable e) { + // ignore + } + } + + boolean isValidResource(Resource resource) { + true + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy new file mode 100644 index 00000000000..f2e8dfcad26 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2014 original 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 org.grails.cli.interactive.completers + +import groovy.transform.CompileStatic +import jline.console.completer.Completer + + + +/** + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class ClosureCompleter implements Completer { + private Closure> closure + private Completer completer + + public ClosureCompleter(Closure> closure) { + this.closure = closure + } + + Completer getCompleter() { + if(completer == null) { + completer = new jline.console.completer.StringsCompleter(closure.call()) + } + completer + } + + @Override + public int complete(String buffer, int cursor, List candidates) { + getCompleter().complete(buffer, cursor, candidates) + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/DomainClassCompleter.groovy b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/DomainClassCompleter.groovy new file mode 100644 index 00000000000..2588bcacc12 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/DomainClassCompleter.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2014 original 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 org.grails.cli.interactive.completers + +import grails.util.BuildSettings +import groovy.transform.CompileStatic +import org.grails.io.support.GrailsResourceUtils +import org.grails.io.support.Resource + +/** + * A completer for domain classes + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class DomainClassCompleter extends ClassNameCompleter { + DomainClassCompleter() { + super(new File(BuildSettings.BASE_DIR, "grails-app/domain")) + } + + @Override + boolean isValidResource(Resource resource) { + GrailsResourceUtils.isDomainClass(resource.getURL()) + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy new file mode 100644 index 00000000000..398be115569 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy @@ -0,0 +1,46 @@ +/* + * Copyright 2011 SpringSource. + * + * 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 org.grails.cli.interactive.completers + +import jline.console.completer.FileNameCompleter + + +/** + * JLine Completor that does file path matching like FileNameCompletor, + * but in addition it escapes whitespace in completions with the '\' + * character. + * + * @author Peter Ledbrook + * @since 2.0 + */ +class EscapingFileNameCompletor extends FileNameCompleter { + /** + *

Gets FileNameCompletor to create a list of candidates and then + * inserts '\' before any whitespace characters in each of the candidates. + * If a candidate ends in a whitespace character, then that is not + * escaped.

+ */ + int complete(String buffer, int cursor, List candidates) { + int retval = super.complete(buffer, cursor, candidates) + + int count = candidates.size() + for (int i = 0; i < count; i++) { + candidates[i] = candidates[i].replaceAll(/(\s)(?!$)/, '\\\\$1') + } + + return retval + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy new file mode 100644 index 00000000000..1dc06e3f8cf --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy @@ -0,0 +1,56 @@ +/* + * Copyright 2011 SpringSource + * + * 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 org.grails.cli.interactive.completers + +import jline.console.completer.Completer + +import java.util.regex.Pattern + +/** + * JLine Completor that accepts a string if it matches a given regular + * expression pattern. + * + * @author Peter Ledbrook + * @since 2.0 + */ +class RegexCompletor implements Completer { + Pattern pattern + + RegexCompletor(String pattern) { + this(Pattern.compile(pattern)) + } + + RegexCompletor(Pattern pattern) { + this.pattern = pattern + } + + /** + *

Check whether the whole buffer matches the configured pattern. + * If it does, the buffer is added to the candidates list + * (which indicates acceptance of the buffer string) and returns 0, + * i.e. the start of the buffer. This mimics the behaviour of SimpleCompletor. + *

+ *

If the buffer doesn't match the configured pattern, this returns + * -1 and the candidates list is left empty.

+ */ + int complete(String buffer, int cursor, List candidates) { + if (buffer ==~ pattern) { + candidates << buffer + return 0 + } + else return -1 + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy new file mode 100644 index 00000000000..252ccdb7840 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy @@ -0,0 +1,58 @@ +/* + * Copyright 2011 SpringSource + * + * 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 org.grails.cli.interactive.completers + +import jline.console.completer.Completer +import jline.console.completer.StringsCompleter + + +/** + * JLine Completor that mixes a fixed set of options with file path matches. + * Fixed options that match will appear first, followed by file path matches. + * + * @author Peter Ledbrook + * @since 2.0 + */ +class SimpleOrFileNameCompletor implements Completer { + private simpleCompletor + private fileNameCompletor + + SimpleOrFileNameCompletor(List fixedOptions) { + this(fixedOptions as String[]) + } + + SimpleOrFileNameCompletor(String[] fixedOptions) { + simpleCompletor = new StringsCompleter(fixedOptions) + fileNameCompletor = new EscapingFileNameCompletor() + } + + int complete(String buffer, int cursor, List candidates) { + // Try the simple completor first... + def retval = simpleCompletor.complete(buffer, cursor, candidates) + + // ...and then the file path completor. By using the given candidate + // list with both completors we aggregate the results automatically. + def fileRetval = fileNameCompletor.complete(buffer, cursor, candidates) + + // If the simple completor has matched, we return its value, otherwise + // we return whatever the file path matcher returned. This ensures that + // both simple completor and file path completor candidates appear + // correctly in the command prompt. If neither competors have matches, + // we of course return -1. + if (retval == -1) retval = fileRetval + return candidates ? retval : -1 + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java new file mode 100644 index 00000000000..8d8018bd5d9 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java @@ -0,0 +1,133 @@ +/* + * Copyright 2015 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 org.grails.cli.interactive.completers; + +import jline.console.completer.Completer; + +import java.util.*; + +import static jline.internal.Preconditions.checkNotNull; + +/** + * Copied from jline AggregateCompleter + * + * sorts aggregated completions + * + */ +public class SortedAggregateCompleter + implements Completer +{ + private final List completers = new ArrayList(); + + public SortedAggregateCompleter() { + // empty + } + + /** + * Construct an AggregateCompleter with the given collection of completers. + * The completers will be used in the iteration order of the collection. + * + * @param completers the collection of completers + */ + public SortedAggregateCompleter(final Collection completers) { + checkNotNull(completers); + this.completers.addAll(completers); + } + + /** + * Construct an AggregateCompleter with the given completers. + * The completers will be used in the order given. + * + * @param completers the completers + */ + public SortedAggregateCompleter(final Completer... completers) { + this(Arrays.asList(completers)); + } + + /** + * Retrieve the collection of completers currently being aggregated. + * + * @return the aggregated completers + */ + public Collection getCompleters() { + return completers; + } + + /** + * Perform a completion operation across all aggregated completers. + * + * @see Completer#complete(String, int, java.util.List) + * @return the highest completion return value from all completers + */ + public int complete(final String buffer, final int cursor, final List candidates) { + // buffer could be null + checkNotNull(candidates); + + List completions = new ArrayList(completers.size()); + + // Run each completer, saving its completion results + int max = -1; + for (Completer completer : completers) { + Completion completion = new Completion(candidates); + completion.complete(completer, buffer, cursor); + + // Compute the max cursor position + max = Math.max(max, completion.cursor); + + completions.add(completion); + } + + SortedSet allCandidates = new TreeSet<>(); + + // Append candidates from completions which have the same cursor position as max + for (Completion completion : completions) { + if (completion.cursor == max) { + allCandidates.addAll(completion.candidates); + } + } + + candidates.addAll(allCandidates); + + return max; + } + + /** + * @return a string representing the aggregated completers + */ + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "completers=" + completers + + '}'; + } + + private class Completion + { + public final List candidates; + + public int cursor; + + public Completion(final List candidates) { + checkNotNull(candidates); + this.candidates = new LinkedList(candidates); + } + + public void complete(final Completer completer, final String buffer, final int cursor) { + checkNotNull(completer); + this.cursor = completer.complete(buffer, cursor, candidates); + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java new file mode 100644 index 00000000000..16d00d811e9 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java @@ -0,0 +1,75 @@ +/* Copyright 2012 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 org.grails.cli.interactive.completers; + +import jline.console.completer.Completer; + +import java.util.*; + +import static jline.internal.Preconditions.checkNotNull; + +/** + * A completer that completes based on a collection of Strings + * + * @author Graeme Rocher + * @since 3.0 + */ +public class StringsCompleter + implements Completer +{ + private SortedSet strings = new TreeSet(); + + public StringsCompleter() { + // empty + } + + public StringsCompleter(final Collection strings) { + checkNotNull(strings); + getStrings().addAll(strings); + } + + public StringsCompleter(final String... strings) { + this(Arrays.asList(strings)); + } + + public SortedSet getStrings() { + return strings; + } + + + public void setStrings(SortedSet strings) { + this.strings = strings; + } + + public int complete(final String buffer, final int cursor, final List candidates) { + // buffer could be null + checkNotNull(candidates); + + if (buffer == null) { + candidates.addAll(getStrings()); + } + else { + for (String match : getStrings().tailSet(buffer)) { + if (!match.startsWith(buffer)) { + break; + } + + candidates.add(match); + } + } + + return candidates.isEmpty() ? -1 : 0; + } +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/TestsCompleter.groovy b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/TestsCompleter.groovy new file mode 100644 index 00000000000..4c3b3a9c229 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/interactive/completers/TestsCompleter.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2015 original 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 org.grails.cli.interactive.completers + +import grails.util.BuildSettings +import groovy.transform.CompileStatic +import org.grails.io.support.Resource + + + +/** + * A completer that completes the names of the tests in the project + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class TestsCompleter extends ClassNameCompleter { + TestsCompleter() { + super(new File(BuildSettings.BASE_DIR, "src/test/groovy"), new File(BuildSettings.BASE_DIR, "src/integration-test/groovy")) + } + + @Override + boolean isValidResource(Resource resource) { + def fn = resource.filename + fn.endsWith('Spec.groovy') || fn.endsWith('Tests.groovy') + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy new file mode 100644 index 00000000000..9b87bdc4dce --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy @@ -0,0 +1,522 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile + +import grails.io.IOUtils +import grails.util.BuildSettings +import grails.util.CosineSimilarity +import groovy.transform.CompileStatic +import groovy.transform.ToString +import jline.console.completer.ArgumentCompleter +import jline.console.completer.Completer +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.graph.Dependency +import org.eclipse.aether.graph.Exclusion +import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector +import org.grails.build.parsing.ScriptNameResolver +import org.grails.cli.interactive.completers.StringsCompleter +import org.grails.cli.profile.commands.CommandRegistry +import org.grails.cli.profile.commands.DefaultMultiStepCommand +import org.grails.cli.profile.commands.script.GroovyScriptCommand +import org.grails.config.NavigableMap +import org.grails.io.support.Resource +import org.yaml.snakeyaml.LoaderOptions +import org.yaml.snakeyaml.Yaml +import org.yaml.snakeyaml.constructor.SafeConstructor + +import static org.grails.cli.profile.ProfileUtil.createDependency + +/** + * Abstract implementation of the profile class + * + * @author Graeme Rocher + * @since 3.1 + */ +@CompileStatic +@ToString(includes = ['name']) +abstract class AbstractProfile implements Profile { + protected final Resource profileDir + protected String name + protected List parentProfiles + protected Map commandsByName + protected NavigableMap navigableConfig + protected ProfileRepository profileRepository + protected List dependencies = [] + protected List repositories = [] + protected List parentNames = [] + protected List buildRepositories = [] + protected List buildPlugins = [] + protected List buildExcludes = [] + protected List skeletonExcludes = [] + protected List binaryExtensions = [] + protected List executablePatterns = [] + protected final List internalCommands = [] + protected List buildMerge = null + protected List features = [] + protected Set defaultFeaturesNames = [] + protected Set requiredFeatureNames = [] + protected String parentTargetFolder + protected final ClassLoader classLoader + protected ExclusionDependencySelector exclusionDependencySelector = new ExclusionDependencySelector() + protected String description = ""; + protected String instructions = ""; + protected String version = BuildSettings.package.implementationVersion + + AbstractProfile(Resource profileDir) { + this(profileDir, AbstractProfile.getClassLoader()) + } + + AbstractProfile(Resource profileDir, ClassLoader classLoader) { + this.classLoader = classLoader + this.profileDir = profileDir + + + def url = profileDir.getURL() + def jarFile = IOUtils.findJarFile(url) + def pattern = ~/.+-(\d.+)\.jar/ + + + def path + if(jarFile != null) { + path = jarFile.name + } + else if(url != null){ + def p = url.path + path = p.substring(0, p.indexOf('.jar') + 4) + } + if(path) { + def matcher = pattern.matcher(path) + if(matcher.matches()) { + this.version = matcher.group(1) + } + } + } + + String getVersion() { + return version + } + + protected void initialize() { + def profileYml = profileDir.createRelative("profile.yml") + Map profileConfig = new Yaml(new SafeConstructor(new LoaderOptions())).> load(profileYml.getInputStream()) + + name = profileConfig.get("name")?.toString() + description = profileConfig.get("description")?.toString() ?: '' + instructions = profileConfig.get("instructions")?.toString() ?: '' + + def parents = profileConfig.get("extends") + if(parents) { + parentNames = parents.toString().split(',').collect() { String name -> name.trim() } + } + if(this.name == null) { + throw new IllegalStateException("Profile name not set. Profile for path ${profileDir.URL} is invalid") + } + def map = new NavigableMap() + map.merge(profileConfig) + navigableConfig = map + def commandsByName = profileConfig.get("commands") + if(commandsByName instanceof Map) { + def commandsMap = (Map) commandsByName + for(clsName in commandsMap.keySet()) { + def fileName = commandsMap[clsName].toString() + if(fileName.endsWith(".groovy")) { + GroovyScriptCommand cmd = (GroovyScriptCommand)classLoader.loadClass(clsName.toString()).newInstance() + cmd.profile = this + cmd.profileRepository = profileRepository + internalCommands.add cmd + } + else if(fileName.endsWith('.yml')) { + def yamlCommand = profileDir.createRelative("commands/$fileName") + if(yamlCommand.exists()) { + Map data = new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlCommand.getInputStream()) + Command cmd = new DefaultMultiStepCommand(clsName.toString(), this, data) + Object minArguments = data?.minArguments + cmd.minArguments = minArguments instanceof Integer ? (Integer)minArguments : 1 + internalCommands.add cmd + } + + } + } + } + + def featuresConfig = profileConfig.get("features") + if(featuresConfig instanceof Map) { + Map featureMap = (Map) featuresConfig + def featureList = (List) featureMap.get("provided") ?: Collections.emptyList() + def defaultFeatures = (List) featureMap.get("defaults") ?: Collections.emptyList() + def requiredFeatures = (List) featureMap.get("required") ?: Collections.emptyList() + for (fn in featureList) { + def featureData = profileDir.createRelative("features/${fn}/feature.yml") + if(featureData.exists()) { + def f = new DefaultFeature(this, fn.toString(), profileDir.createRelative("features/$fn/")) + features.add f + } + } + + defaultFeaturesNames.addAll((List) defaultFeatures) + requiredFeatureNames.addAll((List) requiredFeatures) + } + + + + def dependenciesConfig = profileConfig.get("dependencies") + + if (dependenciesConfig instanceof List) { + List exclusions =[] + for (entry in dependenciesConfig) { + if (entry instanceof Map) { + def scope = (String) entry.scope + String coords = (String) entry.coords + if (scope == 'excludes') { + def artifact = new DefaultArtifact(coords) + exclusions.add new Exclusion(artifact.groupId ?: null, artifact.artifactId ?: null, artifact.classifier ?: null, artifact.extension ?: null) + } else { + Dependency dependency = createDependency(coords, scope, entry) + dependencies.add(dependency) + } + } + exclusionDependencySelector = new ExclusionDependencySelector(exclusions) + } + } + + this.repositories = (List)navigableConfig.get("repositories", []) + + this.buildRepositories = (List)navigableConfig.get("build.repositories", []) + this.buildPlugins = (List)navigableConfig.get("build.plugins", []) + this.buildExcludes = (List)navigableConfig.get("build.excludes", []) + this.buildMerge = (List)navigableConfig.get("build.merge", null) + this.parentTargetFolder = (String)navigableConfig.get("skeleton.parent.target", null) + this.skeletonExcludes = (List)navigableConfig.get("skeleton.excludes", []) + this.binaryExtensions = (List)navigableConfig.get("skeleton.binaryExtensions", []) + this.executablePatterns = (List)navigableConfig.get("skeleton.executable", []) + } + + String getDescription() { + description + } + + String getInstructions() { + instructions + } + + Set getBinaryExtensions() { + Set calculatedBinaryExtensions = [] + def parents = getExtends() + for(profile in parents) { + calculatedBinaryExtensions.addAll(profile.binaryExtensions) + } + calculatedBinaryExtensions.addAll(binaryExtensions) + return calculatedBinaryExtensions + } + + Set getExecutablePatterns() { + Set calculatedExecutablePatterns = [] + def parents = getExtends() + for(profile in parents) { + calculatedExecutablePatterns.addAll(profile.executablePatterns) + } + calculatedExecutablePatterns.addAll(executablePatterns) + return calculatedExecutablePatterns + } + + @Override + Iterable getDefaultFeatures() { + getFeatures().findAll() { Feature f -> defaultFeaturesNames.contains(f.name) } + } + + @Override + Iterable getRequiredFeatures() { + def requiredFeatureInstances = getFeatures().findAll() { Feature f -> requiredFeatureNames.contains(f.name) } + if(requiredFeatureInstances.size() != requiredFeatureNames.size()) { + throw new IllegalStateException("One or more required features were not found on the classpath. Required features: $requiredFeatureNames") + } + return requiredFeatureInstances + } + + @Override + Iterable getFeatures() { + Set calculatedFeatures = [] + calculatedFeatures.addAll(features) + def parents = getExtends() + for(profile in parents) { + calculatedFeatures.addAll profile.features + } + return calculatedFeatures + } + + @Override + List getBuildMergeProfileNames() { + if(buildMerge != null) { + return this.buildMerge + } + else { + List mergeNames = [] + for(parent in getExtends()) { + mergeNames.add(parent.name) + } + mergeNames.add(name) + return mergeNames + } + } + + @Override + List getBuildRepositories() { + List calculatedRepositories = [] + def parents = getExtends() + for(profile in parents) { + calculatedRepositories.addAll(profile.buildRepositories) + } + calculatedRepositories.addAll(buildRepositories) + return calculatedRepositories + } + + @Override + List getBuildPlugins() { + List calculatedPlugins = [] + def parents = getExtends() + for(profile in parents) { + def dependencies = profile.buildPlugins + for(dep in dependencies) { + if(!buildExcludes.contains(dep)) + calculatedPlugins.add(dep) + } + } + calculatedPlugins.addAll(buildPlugins) + return calculatedPlugins + } + + @Override + List getRepositories() { + List calculatedRepositories = [] + def parents = getExtends() + for(profile in parents) { + calculatedRepositories.addAll(profile.repositories) + } + calculatedRepositories.addAll(repositories) + return calculatedRepositories + } + + List getDependencies() { + List calculatedDependencies = [] + def parents = getExtends() + for(profile in parents) { + def dependencies = profile.dependencies + for(dep in dependencies) { + if(exclusionDependencySelector.selectDependency(dep)) { + calculatedDependencies.add(dep) + } + } + } + calculatedDependencies.addAll(dependencies) + return calculatedDependencies + } + + ProfileRepository getProfileRepository() { + return profileRepository + } + + void setProfileRepository(ProfileRepository profileRepository) { + this.profileRepository = profileRepository + } + + Resource getProfileDir() { + return profileDir + } + + + @Override + NavigableMap getConfiguration() { + navigableConfig + } + + @Override + Resource getTemplate(String path) { + return profileDir.createRelative("templates/$path") + } + + @Override + public Iterable getExtends() { + return parentNames.collect() { String name -> + def parent = profileRepository.getProfile(name, true) + if(parent == null) { + throw new IllegalStateException("Profile [$name] declares an invalid dependency on parent profile [$name]") + } + return parent + } + } + + @Override + public Iterable getCompleters(ProjectContext context) { + def commands = getCommands(context) + + Collection completers = [] + + for(Command cmd in commands) { + def description = cmd.description + + def commandNameCompleter = new StringsCompleter(cmd.name) + if(cmd instanceof Completer) { + completers << new ArgumentCompleter(commandNameCompleter, (Completer)cmd) + }else { + if(description.completer) { + if(description.flags) { + completers << new ArgumentCompleter(commandNameCompleter, + description.completer, + new StringsCompleter(description.flags.collect() { CommandArgument arg -> "-$arg.name".toString() })) + } + else { + completers << new ArgumentCompleter(commandNameCompleter, description.completer) + } + + } + else { + if(description.flags) { + completers << new ArgumentCompleter(commandNameCompleter, new StringsCompleter(description.flags.collect() { CommandArgument arg -> "-$arg.name".toString() })) + } + else { + completers << commandNameCompleter + } + } + } + } + + return completers + } + + @Override + Command getCommand(ProjectContext context, String name) { + getCommands(context) + return commandsByName[name] + } + + @Override + Iterable getCommands(ProjectContext context) { + if(commandsByName == null) { + commandsByName = new LinkedHashMap() + List excludes = [] + def registerCommand = { Command command -> + def name = command.name + if(!commandsByName.containsKey(name) && !excludes.contains(name)) { + if(command instanceof ProfileRepositoryAware) { + ((ProfileRepositoryAware)command).setProfileRepository(profileRepository) + } + commandsByName.put(name, command) + def desc = command.description + def synonyms = desc.synonyms + if(synonyms) { + for(String syn in synonyms) { + commandsByName.put(syn, command) + } + } + if(command instanceof ProjectContextAware) { + ((ProjectContextAware)command).projectContext = context + } + if(command instanceof ProfileCommand) { + ((ProfileCommand)command).profile = this + } + } + } + + CommandRegistry.findCommands(this).each(registerCommand) + + def parents = getExtends() + if(parents) { + excludes = (List)configuration.navigate("command", "excludes") ?: [] + registerParentCommands(context, parents, registerCommand) + } + } + return commandsByName.values() + } + + protected void registerParentCommands(ProjectContext context, Iterable parents, Closure registerCommand) { + for (parent in parents) { + parent.getCommands(context).each registerCommand + + def extended = parent.extends + if(extended) { + registerParentCommands context, extended, registerCommand + } + } + } + + @Override + boolean hasCommand(ProjectContext context, String name) { + getCommands(context) // ensure initialization + return commandsByName.containsKey(name) + } + + @Override + boolean handleCommand(ExecutionContext context) { + getCommands(context) // ensure initialization + + def commandLine = context.commandLine + def commandName = commandLine.commandName + def cmd = commandsByName[commandName] + if(cmd) { + def requiredArguments = cmd?.description?.arguments + int requiredArgumentCount = requiredArguments?.findAll() { CommandArgument ca -> ca.required }?.size() ?: 0 + if(commandLine.remainingArgs.size() < requiredArgumentCount) { + context.console.error "Command [$commandName] missing required arguments: ${requiredArguments*.name}. Type 'grails help $commandName' for more info." + return false + } + else { + return cmd.handle(context) + } + } + else { + // Apply command name expansion (rA for run-app, tA for test-app etc.) + cmd = commandsByName.values().find() { Command c -> + ScriptNameResolver.resolvesTo(commandName, c.name) + } + if(cmd) { + return cmd.handle(context) + } + else { + context.console.error("Command not found ${context.commandLine.commandName}") + def mostSimilar = CosineSimilarity.mostSimilar(commandName, commandsByName.keySet()) + List topMatches = mostSimilar.subList(0, Math.min(3, mostSimilar.size())); + if(topMatches) { + context.console.log("Did you mean: ${topMatches.join(' or ')}?") + } + return false + } + + } + } + + @Override + String getParentSkeletonDir() { + this.parentTargetFolder + } + + @Override + File getParentSkeletonDir(File parent) { + if (parentSkeletonDir) { + new File(parent, parentSkeletonDir) + } else { + parent + } + } + + List getSkeletonExcludes() { + this.skeletonExcludes + } + + @Override + String getName() { + name + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/AbstractStep.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/AbstractStep.groovy new file mode 100644 index 00000000000..f85da99b0b9 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/AbstractStep.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile + +import org.grails.build.parsing.CommandLine + +/** + * Abstract implementation of the {@link Step} interface + * + * @author Graeme Rocher + */ +abstract class AbstractStep implements Step { + ProfileCommand command + Map parameters + + AbstractStep(ProfileCommand command, Map parameters) { + this.command = command + this.parameters = parameters + } + + /** + * Obtains details of the given flag if it has been set by the user + * + * @param name The name of the flag + * @return The flag information, or null if it isn't set by the user + */ + def flag(CommandLine commandLine, String name) { + def value = commandLine?.undeclaredOptions?.get(name) + return value ?: null + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/Command.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/Command.groovy new file mode 100644 index 00000000000..60f8b52663d --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/Command.groovy @@ -0,0 +1,42 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile + +import grails.util.Named + +/** + * An interface that represents a command to be executed by the Grails command line. Commands are by default global, + * however a command can be made specific to a particular {@link Profile} by implementation the {@link ProfileCommand} interface. + * + * @author Graeme Rocher + * @since 3.0 + */ +interface Command extends Named { + + /** + * @return The description of the command + */ + CommandDescription getDescription() + + /** + * run the command + * + * @param executionContext The {@link ExecutionContext} + * + * @return Whether the command should continue + */ + boolean handle(ExecutionContext executionContext) +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/CommandArgument.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/CommandArgument.groovy new file mode 100644 index 00000000000..b8dbe7fb056 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/CommandArgument.groovy @@ -0,0 +1,49 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile + +import groovy.transform.CompileStatic + + + +/** + * Represents argument to a command + * + * @author Graeme Rocher + * @since + */ +@CompileStatic +class CommandArgument { + /** + * The name of the argument + */ + String name + /** + * The description of the argument + */ + String description + + /** + * Whether the argument is required or not + */ + boolean required = true + + /** + * The string argument this argument translates into + */ + String target + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/CommandCancellationListener.java b/grails-shell/src/main/groovy/org/grails/cli/profile/CommandCancellationListener.java new file mode 100644 index 00000000000..eeebc4c0ecc --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/CommandCancellationListener.java @@ -0,0 +1,29 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile; + +/** + * A listener for listening for cancellation of {@link org.grails.cli.profile.Command} executions + * + * @author Lari Hotari + * @author Graeme Rocher + */ +public interface CommandCancellationListener { + /** + * Fired when a {@link org.grails.cli.profile.Command} is cancelled + */ + public void commandCancelled(); +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy new file mode 100644 index 00000000000..2f3b2edea9d --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy @@ -0,0 +1,152 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile + +import groovy.transform.Canonical +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import jline.console.completer.Completer + +/** + * Describes a {@link Command} + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +@Canonical +class CommandDescription { + /** + * The name of the command + */ + String name + /** + * The description of the command + */ + String description + /** + * The usage instructions for the command + */ + String usage + + /** + * Any names that should also map to this command + */ + Collection synonyms = [] + + /** + * A completer for the command + */ + Completer completer = null + + private Map arguments = new LinkedHashMap<>() + private Map flags = new LinkedHashMap<>() + + /** + * Returns an argument for the given name or null if it doesn't exist + * @param name The name + * @return The argument or null + */ + CommandArgument getArgument(String name) { + arguments[name] + } + + /** + * Returns a flag for the given name or null if it doesn't exist + * @param name The name + * @return The argument or null + */ + CommandArgument getFlag(String name) { + flags[name] + } + + /** + * Arguments to the command + */ + Collection getArguments() { + arguments.values() + } + + /** + * Flags to the command. These differ as they are optional and are prefixed with a hyphen (Example -debug) + */ + Collection getFlags() { + flags.values() + } + + /** + * Adds a synonyms for this command + * + * @param synonyms The synonyms + * @return This command description + */ + CommandDescription synonyms(String...synonyms) { + this.synonyms.addAll(synonyms) + return this + } + /** + * Sets the completer + * + * @param completer The class of the completer to set + * @return The description instance + */ + CommandDescription completer(Class completer) { + this.completer = completer.newInstance() + return this + } + + /** + * Sets the completer + * + * @param completer The completer to set + * @return The description instance + */ + CommandDescription completer(Completer completer) { + this.completer = completer + return this + } + + /** + * Adds an argument for the given named arguments + * + * @param args The named arguments + */ + @CompileDynamic + CommandDescription argument(Map args) { + def arg = new CommandArgument(args) + def name = arg.name + if(name) { + arguments[name] = arg + } + return this + } + + /** + * Adds a flag for the given named arguments + * + * @param args The named arguments + */ + @CompileDynamic + CommandDescription flag(Map args) { + def arg = new CommandArgument(args) + def name = arg.name + if(name) { + arg.required = false + flags[name] = arg + } + return this + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/CommandException.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/CommandException.groovy new file mode 100644 index 00000000000..7137f7dc378 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/CommandException.groovy @@ -0,0 +1,27 @@ +package org.grails.cli.profile + +import groovy.transform.InheritConstructors + +/* + * Copyright 2014 original 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. + */ + +/** + * @author Graeme Rocher + * @since 3.0 + */ +@InheritConstructors +class CommandException extends RuntimeException{ +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/DefaultFeature.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/DefaultFeature.groovy new file mode 100644 index 00000000000..b5e14277d84 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/DefaultFeature.groovy @@ -0,0 +1,98 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import org.eclipse.aether.graph.Dependency +import org.grails.config.NavigableMap +import org.grails.io.support.Resource +import org.yaml.snakeyaml.LoaderOptions +import org.yaml.snakeyaml.Yaml +import org.yaml.snakeyaml.constructor.SafeConstructor + +import static org.grails.cli.profile.ProfileUtil.createDependency + +/** + * Default implementation of the {@link Feature} interface + * + * @author Graeme Rocher + * @since 3.1 + */ +@EqualsAndHashCode(includes = ['name']) +@ToString(includes = ['profile', 'name']) +@CompileStatic +class DefaultFeature implements Feature { + final Profile profile + final String name + final Resource location + final NavigableMap configuration = new NavigableMap() + final List dependencies = [] + final List buildPlugins + final List buildRepositories + + DefaultFeature(Profile profile, String name, Resource location) { + this.profile = profile + this.name = name + this.location = location + def featureYml = location.createRelative("feature.yml") + Map featureConfig = new Yaml(new SafeConstructor(new LoaderOptions())).>load(featureYml.getInputStream()) + configuration.merge(featureConfig) + def dependenciesConfig = configuration.get("dependencies") + + if(dependenciesConfig instanceof List) { + for(entry in ((List) dependenciesConfig)) { + if (entry instanceof Map) { + def scope = (String) entry.scope + def os = entry.os + if (os && !isSupportedOs(os.toString())) { + continue + } + String coords = (String) entry.coords + Dependency dependency = createDependency(coords, scope, entry) + dependencies.add(dependency) + } + } + } + this.buildPlugins = (List)configuration.get("build.plugins", []) + this.buildRepositories = (List) configuration.get("build.repositories", []) + } + + @Override + String getDescription() { + configuration.get("description", '').toString() + } + + static boolean isSupportedOs(String os) { + os = os.toLowerCase(Locale.ENGLISH).trim() + String osName = System.getProperty("os.name")?.toLowerCase(Locale.ENGLISH) ?: "unix" + switch (os) { + case "windows": + return osName.contains("windows") + case "osx": + return osName.contains("mac os x") || osName.contains("darwin") || osName.contains("osx") + case "unix": + return osName.contains("mac os x") || osName.contains("darwin") || osName.contains("osx") || + osName.contains("sunos") || osName.contains("solaris") || osName.contains("linux") || + osName.contains("freebsd") + default: + return false + } + } + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/ExecutionContext.java b/grails-shell/src/main/groovy/org/grails/cli/profile/ExecutionContext.java new file mode 100644 index 00000000000..05c1edc053b --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/ExecutionContext.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile; + +import org.grails.build.parsing.CommandLine; + + +/** + * Context for the execution of {@link org.grails.cli.profile.Command} instances within a {@link org.grails.cli.profile.Profile} + * + * @author Lari Hotari + * @author Graeme Rocher + */ +public interface ExecutionContext extends ProjectContext { + + /** + * @return The parsed command line arguments as an instance of {@link org.grails.build.parsing.CommandLine} + */ + CommandLine getCommandLine(); + + /** + * Allows cancelling of the running command + */ + void cancel(); + + /** + * Attaches a listener for cancellation events + * + * @param listener The {@link CommandCancellationListener} + */ + void addCancelledListener(CommandCancellationListener listener); +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/Feature.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/Feature.groovy new file mode 100644 index 00000000000..e393a616d26 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/Feature.groovy @@ -0,0 +1,70 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile + +import org.eclipse.aether.graph.Dependency +import org.grails.config.NavigableMap +import org.grails.io.support.Resource + + +/** + * An interface that describes a feature of a profile. Different profiles may share many common features even if the profile itself is different. + * + * @author Graeme Rocher + * @since 3.1 + */ +interface Feature { + + /** + * @return The profile this feature belongs to + */ + Profile getProfile() + + /** + * @return The name of the feature + */ + String getName() + + /** + * @return The description of the profile + */ + String getDescription() + + /** + * @return The location of the feature + */ + Resource getLocation() + + /** + * @return The dependency definitions for this feature + */ + List getDependencies() + + /** + * @return The build plugin names + */ + List getBuildPlugins() + + /** + * @return The build repositories url + */ + List getBuildRepositories() + + /** + * @return The configuration for the feature + */ + NavigableMap getConfiguration() +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/FileSystemProfile.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/FileSystemProfile.groovy new file mode 100644 index 00000000000..f926df0ba1a --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/FileSystemProfile.groovy @@ -0,0 +1,38 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile + +import grails.util.BuildSettings +import groovy.transform.CompileStatic +import org.grails.io.support.FileSystemResource +import org.grails.io.support.Resource + + + +/** + * Simple disk based implementation of the {@link Profile} interface + * + * @author Graeme Rocher + * @since 1.0 + */ +@CompileStatic +class FileSystemProfile extends ResourceProfile { + + FileSystemProfile(ProfileRepository repository, File profileDir) { + super(repository, profileDir.name, new FileSystemResource(profileDir)) + } + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/MultiStepCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/MultiStepCommand.groovy new file mode 100644 index 00000000000..0813457854a --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/MultiStepCommand.groovy @@ -0,0 +1,57 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile + +import org.grails.cli.profile.commands.events.CommandEvents + +/** + * A command that executes multiple steps + * + * @author Graeme Rocher + * @since 3.0 + */ +abstract class MultiStepCommand implements ProfileCommand, CommandEvents { + + String name + Profile profile + int minArguments = 1 + + MultiStepCommand(String name, Profile profile) { + this.name = name + this.profile = profile + } + /** + * @return The steps that make up the command + */ + abstract List getSteps() + + @Override + boolean handle(ExecutionContext context) { + if(minArguments > 0 && (!context.commandLine.getRemainingArgs() || context.commandLine.getRemainingArgs().size() < minArguments)) { + context.console.error("Expecting ${minArguments ? 'an argument' : minArguments + ' arguments'} to $name.") + context.console.info("${description.usage}") + return true + } + notify("${name}Start", context) + for(AbstractStep step : getSteps()) { + if(!step.handle(context)) { + break + } + } + notify("${name}End", context) + true + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/Profile.java b/grails-shell/src/main/groovy/org/grails/cli/profile/Profile.java new file mode 100644 index 00000000000..b373b3eb802 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/Profile.java @@ -0,0 +1,183 @@ +/* + * Copyright 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 org.grails.cli.profile; + +import jline.console.completer.Completer; +import org.eclipse.aether.graph.Dependency; +import org.grails.config.NavigableMap; +import org.grails.io.support.Resource; + +import java.io.File; +import java.util.List; +import java.util.Set; + +/** + * A Profile defines an active code generation and command execution policy. For example the "web" profile allows + * the execution of code gen and build commands that relate to web applications + * + * @author Graeme Rocher + * @author Lari Hotari + * + * @since 3.0 + */ +public interface Profile { + + /** + * @return The name of the profile + */ + String getName(); + + /** + * @return The version of the profile + */ + String getVersion(); + + /** + * @return The description of the profile + */ + String getDescription(); + + /** + * @return The list of file extensions which should be treated as binary + */ + Set getBinaryExtensions(); + + /** + * @return The list of file patterns which should be executable in the resulting application + */ + Set getExecutablePatterns(); + + /** + * @return Text to display after an application has been created with the profile + */ + String getInstructions(); + + /** + * @return The features for this profile + */ + Iterable getFeatures(); + + /** + * @return The default features for this profile + */ + Iterable getDefaultFeatures(); + + /** + * @return The required features for this profile + */ + Iterable getRequiredFeatures(); + + /** + * The other {@link org.grails.cli.profile.Profile} instances that this {@link org.grails.cli.profile.Profile} extends + * @return zero or many {@link org.grails.cli.profile.Profile} instance that this profile extends from + */ + Iterable getExtends(); + + /** + * @return The maven repository definitions for this profile + */ + List getRepositories(); + + /** + * @return The dependency definitions for this profile + */ + List getDependencies(); + + /** + * @return The profiles configuration + */ + NavigableMap getConfiguration(); + + /** + * @return The directory where the profile is located locally + */ + Resource getProfileDir(); + + /** + * Obtain a template by path + * + * @param path The path to template + * @return The resource or null if it doesn't exist + */ + Resource getTemplate(String path); + + /** + * Obtain a command by name + * + * @param name Obtain a command by name + * @return The command + */ + Command getCommand(ProjectContext context, String name); + + /** + * The profile completers + * @param context The {@link org.grails.cli.profile.ProjectContext} instance + * @return An {@link java.lang.Iterable} of {@link jline.console.completer.Completer} instances + */ + Iterable getCompleters(ProjectContext context); + + /** + * The profile {@link org.grails.cli.profile.Command} instances + * + * @param context The {@link ProjectContext} instance + * @return An {@link java.lang.Iterable} of {@link org.grails.cli.profile.Command} instances + */ + Iterable getCommands(ProjectContext context); + + /** + * Whether a command executes for the given context and name + * @param context The {@link org.grails.cli.profile.ProjectContext} + * @param name The command name + * @return True if the command does exist + */ + boolean hasCommand(ProjectContext context, String name); + /** + * Obtains a {@link Command} + * + * @return True if the command was handled + */ + boolean handleCommand(ExecutionContext context); + + /** + * @return The buildscript maven repository definitions for this profile + */ + List getBuildRepositories(); + + /** + * @return The profile names to participate in build merge + */ + List getBuildMergeProfileNames(); + + /** + * @return The list of build plugins for this profile + */ + List getBuildPlugins(); + + /** + * @return The subfolder the parent profile(s) skeleton should be copied into + */ + String getParentSkeletonDir(); + + /** + * @return The directory the parent profile(s) skeleton should be copied into + */ + File getParentSkeletonDir(File parent); + + /** + * @return A list of paths to exclude from the skeleton. Used in ant fileset exclude: + */ + List getSkeletonExcludes(); +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileCommand.groovy new file mode 100644 index 00000000000..eacdfef9582 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileCommand.groovy @@ -0,0 +1,37 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile + +/** + * A {@link Command} applicable only to a certain {@link Profile} + * + * @author Graeme Rocher + * @since 3.0 + */ +interface ProfileCommand extends Command { + /** + * @return The profile of the command + */ + Profile getProfile() + + /** + * Sets the command profile + * + * @param profile The profile + */ + void setProfile(Profile profile) + +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileRepository.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileRepository.groovy new file mode 100644 index 00000000000..3427b97b84d --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileRepository.groovy @@ -0,0 +1,74 @@ +package org.grails.cli.profile + +import org.eclipse.aether.artifact.Artifact +import org.grails.io.support.Resource + +/* + * Copyright 2014 original 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. + */ + +/** + * + * A repository of {@link Profile} instances + * + * @author Graeme Rocher + * @since 3.0 + */ +interface ProfileRepository { + + String DEFAULT_PROFILE_NAME = 'web' + + /** + * Obtains a named {@link Profile} + * @param profileName The name of the profile + * @return The {@link Profile} or null + */ + Profile getProfile(String profileName) + + /** + * Obtains a named {@link Profile} + * @param profileName The name of the profile + * @param parentProfile Whether or not the profile is a parent of another profile + * @return The {@link Profile} or null + */ + Profile getProfile(String profileName, Boolean parentProfile) + + /** + * The directory where the profile is located + * + * @param profile The name of the profile + * @return The directory where the profile is located or null if it doesn't exist + */ + Resource getProfileDirectory(String profile) + + /** + * Returns the given profile with all dependencies in topological order where + * given profile is last in the order. + * + * @param profile The {@link Profile} instance + * @return The {@link Profile} and its dependencies + */ + List getProfileAndDependencies(Profile profile) + + /** + * @return All the available profiles in the repository + */ + List getAllProfiles() + + /** + * @return The {@link Artifact} that resolves to the profile + */ + Artifact getProfileArtifact(String profileName) +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileRepositoryAware.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileRepositoryAware.groovy new file mode 100644 index 00000000000..9eb9f88e43e --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileRepositoryAware.groovy @@ -0,0 +1,26 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile + +/** + * For commands and steps that need to be made aware of the {@link ProfileRepository} to implement + * + * @author Graeme Rocher + * @since 3.0 + */ +interface ProfileRepositoryAware { + void setProfileRepository(ProfileRepository profileRepository) +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileUtil.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileUtil.groovy new file mode 100644 index 00000000000..03a680ed4d7 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/ProfileUtil.groovy @@ -0,0 +1,32 @@ +package org.grails.cli.profile + +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.graph.Dependency +import org.eclipse.aether.graph.Exclusion + +/** + * The utility class for the Grails profiles. + * + * @author Puneet Behl + * @since 4.1 + */ +class ProfileUtil { + + static Dependency createDependency(String coords, String scope, Map configEntry) { + if (coords.count(':') == 1) { + coords = "$coords:BOM" + } + Dependency dependency = new Dependency(new DefaultArtifact(coords), scope.toString()) + if (configEntry.containsKey('excludes')) { + List dependencyExclusions = new ArrayList<>() + List excludes = (List) configEntry.excludes + for (ex in excludes) { + if (ex instanceof Map) { + dependencyExclusions.add(new Exclusion((String) ex.group, (String) ex.module, (String) ex.classifier, (String) ex.extension)) + } + } + dependency = dependency.setExclusions(dependencyExclusions) + } + dependency + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/ProjectCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/ProjectCommand.groovy new file mode 100644 index 00000000000..06d43e2b17c --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/ProjectCommand.groovy @@ -0,0 +1,25 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile + +/** + * A marker interface for commands that are global, but apply only within the context of a project + * + * @author Graeme Rocher + * @since 3.0 + */ +interface ProjectCommand extends Command { +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/ProjectContext.java b/grails-shell/src/main/groovy/org/grails/cli/profile/ProjectContext.java new file mode 100644 index 00000000000..a753c266335 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/ProjectContext.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile; + +import grails.build.logging.GrailsConsole; +import grails.config.ConfigMap; + +import java.io.File; + +/** + * The project context used by a {@link org.grails.cli.profile.Profile} + * + * @author Lari Hotari + * @author Graeme Rocher + */ +public interface ProjectContext { + /** + * @return The {@link grails.build.logging.GrailsConsole} instance + */ + GrailsConsole getConsole(); + + /** + * + * @return The base directory of the project + */ + File getBaseDir(); + + /** + * @return The codegen config + */ + ConfigMap getConfig(); + + /** + * Obtains a value from the codegen configuration + * + * @param path The path to value + * @return The value or null if not set + */ + String navigateConfig(String... path); + + /** + * Obtains a value of the given type from the codegen configuration + * + * @param requiredType The required return type + * @param path The path to value + * @return The value or null if not set + */ + T navigateConfigForType(Class requiredType, String... path); +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/ProjectContextAware.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/ProjectContextAware.groovy new file mode 100644 index 00000000000..45f80580366 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/ProjectContextAware.groovy @@ -0,0 +1,27 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile + +/** + * Interface for components that want to be made aware of the proxy context + * + * @author Graeme Rocher + * @since 3.0 + */ +interface ProjectContextAware { + + void setProjectContext(ProjectContext projectContext) +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/ResourceProfile.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/ResourceProfile.groovy new file mode 100644 index 00000000000..b5007e5e232 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/ResourceProfile.groovy @@ -0,0 +1,66 @@ +/* + * Copyright 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 org.grails.cli.profile + +import groovy.transform.CompileStatic +import org.grails.config.NavigableMap +import org.grails.io.support.Resource +import org.yaml.snakeyaml.Yaml +/** + * A profile that operates against abstract {@link Resource} references + * + * + * @since 3.0 + * @author Lari Hotari + * @author Graeme Rocher + */ +@CompileStatic +class ResourceProfile extends AbstractProfile implements Profile { + + + ResourceProfile(ProfileRepository repository, String name, Resource profileDir) { + super(profileDir) + super.name = name + this.profileRepository = repository + initialize() + } + + @Override + String getName() { + super.name + } + + public static Profile create(ProfileRepository repository, String name, Resource profileDir) { + Profile profile = new ResourceProfile(repository, name, profileDir) + return profile + } + + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + ResourceProfile that = (ResourceProfile) o + + if (name != that.name) return false + + return true + } + + int hashCode() { + return (name != null ? name.hashCode() : 0) + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/Step.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/Step.groovy new file mode 100644 index 00000000000..0e0fb5827a5 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/Step.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile + + +/** + * Represents a step within a {@link Command}. Commands are made up of 1 or many steps. + * + * @author Graeme Rocher + * @since 3.0 + */ +interface Step { + + /** + * @return The name of the step + */ + String getName() + /** + * @return The parameters to the step + */ + Map getParameters() + + /** + * @return The command that this step is part of + */ + Command getCommand() + + /** + * Handles the command logic + * + * @param context The {@link ExecutionContext} instead + * + * @return True if the command should proceed to the next step, false otherwise + */ + boolean handle(ExecutionContext context) + +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy new file mode 100644 index 00000000000..b6d6b5710d7 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy @@ -0,0 +1,66 @@ +package org.grails.cli.profile.commands + +import jline.console.completer.ArgumentCompleter +import jline.console.completer.Completer +import org.grails.build.parsing.CommandLine +import org.grails.build.parsing.CommandLineParser +import org.grails.cli.profile.Command +import org.grails.cli.profile.CommandDescription + +/* + * Copyright 2014 original 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. + */ + +/** + * @author graemerocher + */ +abstract class ArgumentCompletingCommand implements Command, Completer { + + CommandLineParser cliParser = new CommandLineParser() + + @Override + final int complete(String buffer, int cursor, List candidates) { + def desc = getDescription() + def commandLine = cliParser.parseString(buffer) + return complete(commandLine, desc, candidates, cursor) + } + + protected int complete(CommandLine commandLine, CommandDescription desc, List candidates, int cursor) { + def invalidOptions = commandLine.undeclaredOptions.keySet().findAll { String str -> + desc.getFlag(str.trim()) == null + } + + def lastOption = commandLine.lastOption() + + + for (arg in desc.flags) { + def argName = arg.name + def flag = "-$argName".toString() + if (!commandLine.hasOption(arg.name)) { + if (lastOption) { + def lastArg = lastOption.key + if (arg.name.startsWith(lastArg)) { + candidates.add("${argName.substring(lastArg.length())} ".toString()) + } else if (!invalidOptions) { + candidates.add "$flag ".toString() + } + } else { + candidates.add "$flag ".toString() + } + } + } + return cursor + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ClosureExecutingCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ClosureExecutingCommand.groovy new file mode 100644 index 00000000000..ef5155a3f0e --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ClosureExecutingCommand.groovy @@ -0,0 +1,50 @@ +package org.grails.cli.profile.commands + +import org.grails.cli.profile.Command +import org.grails.cli.profile.CommandDescription +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileCommand + +/* + * Copyright 2014 original 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. + */ + +/** + * A command that executes a closure + * + * @author Graeme Rocher + * @since 3.0 + */ +class ClosureExecutingCommand implements ProfileCommand { + String name + Closure callable + Profile profile + + ClosureExecutingCommand(String name, Closure callable) { + this.name = name + this.callable = callable + } + + @Override + CommandDescription getDescription() { + new CommandDescription(name) + } + + @Override + boolean handle(ExecutionContext executionContext) { + callable.call(executionContext) + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy new file mode 100644 index 00000000000..34ab7dcfa7c --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy @@ -0,0 +1,51 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile.commands + +import jline.console.completer.Completer +import org.grails.cli.profile.Command + +/** + * A completer for commands + * + * @author Graeme Rocher + * @since 3.1 + */ +class CommandCompleter implements Completer { + + Collection commands + + CommandCompleter(Collection commands) { + this.commands = commands + } + + @Override + int complete(String buffer, int cursor, List candidates) { + def cmd = commands.find() { + def trimmed = buffer.trim() + if(trimmed.split(/\s/).size() > 1) { + return trimmed.startsWith( it.name ) + } + else { + return trimmed == it.name + } + } + if(cmd instanceof Completer) { + return ((Completer)cmd).complete(buffer, cursor, candidates) + } + return cursor + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CommandRegistry.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CommandRegistry.groovy new file mode 100644 index 00000000000..c8bd4f32432 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CommandRegistry.groovy @@ -0,0 +1,101 @@ +package org.grails.cli.profile.commands + +import groovy.transform.CompileStatic +import org.grails.cli.GrailsCli +import org.grails.cli.profile.Command +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileCommand +import org.grails.cli.profile.ProfileRepository +import org.grails.cli.profile.ProfileRepositoryAware +import org.grails.cli.profile.ProjectCommand +import org.grails.cli.profile.commands.factory.CommandFactory +import org.grails.config.CodeGenConfig + +/* + * Copyright 2014 original 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. + */ + +/** + * Registry of available commands + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class CommandRegistry { + + private static Map registeredCommands = [:] + private static List registeredCommandFactories = [] + + static { + def commands = ServiceLoader.load(Command).iterator() + + while(commands.hasNext()) { + Command command = commands.next() + registeredCommands[command.name] = command + } + + def commandFactories = ServiceLoader.load(CommandFactory).iterator() + while(commandFactories.hasNext()) { + CommandFactory commandFactory = commandFactories.next() + + registeredCommandFactories << commandFactory + } + } + + /** + * Returns a command for the given name and repository + * + * @param name The command name + * @param repository The {@link ProfileRepository} instance + * @return A command or null of non exists + */ + static Command getCommand(String name, ProfileRepository repository) { + def command = registeredCommands[name] + if(command instanceof ProfileRepositoryAware) { + command.profileRepository = repository + } + return command + } + + static Collection findCommands( ProfileRepository repository ) { + registeredCommands.values().collect() { Command cmd -> + if(cmd instanceof ProfileRepositoryAware) { + ((ProfileRepositoryAware)cmd).profileRepository = repository + } + return cmd + } + } + + static Collection findCommands( Profile profile, boolean inherited = false ) { + Collection commands = [] + + for(CommandFactory cf in registeredCommandFactories) { + def factoryCommands = cf.findCommands(profile, inherited) + def condition = { Command c -> c.name == 'events' } + def eventCommands = factoryCommands.findAll(condition) + for(ec in eventCommands) { + ec.handle(new GrailsCli.ExecutionContextImpl(new CodeGenConfig(profile.configuration))) + } + factoryCommands.removeAll(condition) + commands.addAll factoryCommands + } + + commands.addAll( registeredCommands.values() + .findAll { Command c -> (c instanceof ProjectCommand) || (c instanceof ProfileCommand) && ((ProfileCommand)c).profile == profile } + ) + return commands + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy new file mode 100644 index 00000000000..b2884de7eea --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy @@ -0,0 +1,778 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands + +import grails.build.logging.GrailsConsole +import grails.io.IOUtils +import grails.util.Environment +import grails.util.GrailsNameUtils +import groovy.ant.AntBuilder +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.transform.TypeCheckingMode +import org.eclipse.aether.graph.Dependency +import org.grails.build.logging.GrailsConsoleAntBuilder +import org.grails.build.parsing.CommandLine +import org.grails.cli.GrailsCli +import org.grails.cli.profile.CommandDescription +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.Feature +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileRepository +import org.grails.cli.profile.ProfileRepositoryAware +import org.grails.cli.profile.commands.io.GradleDependency +import org.grails.cli.profile.repository.MavenProfileRepository +import org.grails.io.support.FileSystemResource +import org.grails.io.support.Resource + +import java.nio.file.DirectoryNotEmptyException +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import java.util.stream.Stream + +/** + * Command for creating Grails applications + * + * @author Graeme Rocher + * @author Lari Hotari + * @since 3.0 + */ +@CompileStatic +class CreateAppCommand extends ArgumentCompletingCommand implements ProfileRepositoryAware { + private static final String GRAILS_VERSION_FALLBACK_IN_IDE_ENVIRONMENTS_FOR_RUNNING_TESTS ='4.0.0.BUILD-SNAPSHOT' + public static final String NAME = "create-app" + public static final String PROFILE_FLAG = "profile" + public static final String FEATURES_FLAG = "features" + public static final String ENCODING = System.getProperty("file.encoding") ?: "UTF-8" + public static final String INPLACE_FLAG = "inplace" + + protected static final String APPLICATION_YML = "application.yml" + protected static final String BUILD_GRADLE = "build.gradle" + protected static final String GRADLE_PROPERTIES = "gradle.properties" + public static final String UNZIP_PROFILE_TEMP_DIR = "tempgrailsapp" + + ProfileRepository profileRepository + Map variables = [:] + String appname + String groupname + String defaultpackagename + File targetDirectory + + CommandDescription description = new CommandDescription(name, "Creates an application", "create-app [NAME] --profile=web") + + CreateAppCommand() { + populateDescription() + description.flag(name: INPLACE_FLAG, description: "Used to create an application using the current directory") + description.flag(name: PROFILE_FLAG, description: "The profile to use", required:false) + description.flag(name: FEATURES_FLAG, description: "The features to use", required:false) + } + + protected void populateDescription() { + description.argument(name: "Application Name", description: "The name of the application to create.", required: false) + } + + @Override + String getName() { + return NAME + } + + @Override + protected int complete(CommandLine commandLine, CommandDescription desc, List candidates, int cursor) { + def lastOption = commandLine.lastOption() + if(lastOption != null) { + // if value == true it means no profile is specified and only the flag is present + def profileNames = profileRepository.allProfiles.collect() { Profile p -> p.name } + if(lastOption.key == PROFILE_FLAG) { + def val = lastOption.value + if( val == true) { + candidates.addAll(profileNames) + return cursor + } + else if(!profileNames.contains(val)) { + def valStr = val.toString() + + def candidateProfiles = profileNames.findAll { String pn -> + pn.startsWith(valStr) + }.collect() { String pn -> + "${pn.substring(valStr.size())} ".toString() + } + candidates.addAll candidateProfiles + return cursor + } + } + else if(lastOption.key == FEATURES_FLAG) { + def val = lastOption.value + def profile = profileRepository.getProfile(commandLine.hasOption(PROFILE_FLAG) ? commandLine.optionValue(PROFILE_FLAG).toString() : getDefaultProfile()) + def featureNames = profile.features.collect() { Feature f -> f.name } + if( val == true) { + candidates.addAll(featureNames) + return cursor + } + else if(!profileNames.contains(val)) { + def valStr = val.toString() + if(valStr.endsWith(',')) { + def specified = valStr.split(',') + candidates.addAll(featureNames.findAll { String f -> + !specified.contains(f) + }) + return cursor + } + + def candidatesFeatures = featureNames.findAll { String pn -> + pn.startsWith(valStr) + }.collect() { String pn -> + "${pn.substring(valStr.size())} ".toString() + } + candidates.addAll candidatesFeatures + return cursor + } + } + } + return super.complete(commandLine, desc, candidates, cursor) + } + + protected File getDestinationDirectory(File srcFile) { + String searchDir = "skeleton" + File srcDir = srcFile.parentFile + File destDir + if (srcDir.absolutePath.endsWith(searchDir)) { + destDir = targetDirectory + } else { + int index = srcDir.absolutePath.lastIndexOf(searchDir) + searchDir.size() + 1 + String relativePath = (srcDir.absolutePath - srcDir.absolutePath.substring(0,index)) + destDir = new File(targetDirectory, relativePath) + } + destDir + } + + protected void appendFeatureFiles(File skeletonDir) { + def ymlFiles = findAllFilesByName(skeletonDir, APPLICATION_YML) + def buildGradleFiles = findAllFilesByName(skeletonDir, BUILD_GRADLE) + def gradlePropertiesFiles = findAllFilesByName(skeletonDir, GRADLE_PROPERTIES) + + ymlFiles.each { File newYml -> + File oldYml = new File(getDestinationDirectory(newYml), APPLICATION_YML) + String oldText = (oldYml.isFile()) ? oldYml.getText(ENCODING) : null + if (oldText) { + appendToYmlSubDocument(newYml, oldText, oldYml) + } else { + oldYml.text = newYml.getText(ENCODING) + } + + } + buildGradleFiles.each { File srcFile -> + File destFile = new File(getDestinationDirectory(srcFile), BUILD_GRADLE) + destFile.text = destFile.getText(ENCODING) + System.lineSeparator() + srcFile.getText(ENCODING) + } + + gradlePropertiesFiles.each { File srcFile-> + File destFile = new File(getDestinationDirectory(srcFile), GRADLE_PROPERTIES) + if (!destFile.exists()) { + destFile.createNewFile() + } + destFile.append(srcFile.getText(ENCODING)) +// destFile.text = destFile.getText(ENCODING) + srcFile.getText(ENCODING) + } + } + + protected void buildTargetFolders(Profile profile, Map targetDir, File projectDir) { + if (!targetDir.containsKey(profile)) { + targetDir[profile] = projectDir + } + profile.extends.each { Profile p -> + if (profile.parentSkeletonDir) { + targetDir[p] = profile.getParentSkeletonDir(projectDir) + } else { + targetDir[p] = targetDir[profile] + } + buildTargetFolders(p, targetDir, projectDir) + } + } + + + Set findAllFilesByName(File projectDir, String fileName) { + Set files = (Set)[] + if (projectDir.exists()) { + Files.walkFileTree(projectDir.absoluteFile.toPath(), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes mainAtts) + throws IOException { + if (path.fileName.toString() == fileName) { + files.add(path.toFile()) + } + return FileVisitResult.CONTINUE; + } + }) + } + files + } + + boolean handle(CreateAppCommandObject cmd) { + if (profileRepository == null) throw new IllegalStateException("Property 'profileRepository' must be set") + + String profileName = cmd.profileName + + Profile profileInstance = profileRepository.getProfile(profileName) + if (!validateProfile(profileInstance, profileName)) { + return false + } + + List features = evaluateFeatures(profileInstance, cmd.features).toList() + + if (profileInstance) { + if (!initializeGroupAndName(cmd.appName, cmd.inplace)) { + return false + } + + initializeVariables(profileName, cmd.grailsVersion) + + if(profileRepository instanceof MavenProfileRepository) { + MavenProfileRepository mpr = (MavenProfileRepository)profileRepository + String gormDep = mpr.profileDependencyVersions.versionProperties.get('gorm.version') + if(gormDep != null) { + variables['gorm.version'] = gormDep + } + String groovyDep = mpr.profileDependencyVersions.versionProperties.get('groovy.version') + if(groovyDep != null) { + variables['groovy.version'] = groovyDep + } + String grailsGradlePluginVersion = mpr.profileDependencyVersions.versionProperties.get('grails-gradle-plugin.version') + if (grailsGradlePluginVersion != null) { + variables['grails-gradle-plugin.version'] = grailsGradlePluginVersion + } + } + + Path appFullDirectory = Paths.get(cmd.baseDir.path, appname) + + File projectTargetDirectory = cmd.inplace ? new File(".").canonicalFile : appFullDirectory.toAbsolutePath().normalize().toFile() + + if (projectTargetDirectory.exists() && !isDirectoryEmpty(projectTargetDirectory)) { + GrailsConsole.getInstance().error(new DirectoryNotEmptyException(projectTargetDirectory.absolutePath)) + return false + } + + def profiles = profileRepository.getProfileAndDependencies(profileInstance) + + Map targetDirs = [:] + buildTargetFolders(profileInstance, targetDirs, projectTargetDirectory) + + for(Profile p : profiles) { + Set ymlFiles = findAllFilesByName(projectTargetDirectory, APPLICATION_YML) + Map ymlCache = [:] + + targetDirectory = targetDirs[p] + + ymlFiles.each { File applicationYmlFile -> + String previousApplicationYml = (applicationYmlFile.isFile()) ? applicationYmlFile.getText(ENCODING) : null + if (previousApplicationYml) { + ymlCache[applicationYmlFile] = previousApplicationYml + } + } + + copySkeleton(profileInstance, p) + + ymlCache.each { File applicationYmlFile, String previousApplicationYml -> + if(applicationYmlFile.exists()) { + appendToYmlSubDocument(applicationYmlFile, previousApplicationYml) + } + } + } + def ant = new GrailsConsoleAntBuilder() + + for(Feature f in features) { + def location = f.location + + File skeletonDir + File tmpDir + if(location instanceof FileSystemResource) { + skeletonDir = location.createRelative("skeleton").file + } + else { + tmpDir = unzipProfile(ant, location) + skeletonDir = new File(tmpDir, "META-INF/grails-profile/features/$f.name/skeleton") + } + + targetDirectory = targetDirs[f.profile] + + appendFeatureFiles(skeletonDir) + + if(skeletonDir.exists()) { + copySrcToTarget(ant, skeletonDir, ['**/' + APPLICATION_YML], profileInstance.binaryExtensions) + } + + // Cleanup temporal directories + deleteDirectory(tmpDir) + deleteDirectory(skeletonDir) + } + + replaceBuildTokens(profileName, profileInstance, features, projectTargetDirectory) + cmd.console.addStatus( + "${name == 'create-plugin' ? 'Plugin' : 'Application'} created at ${projectTargetDirectory.absolutePath}" + ) + if (profileInstance.instructions) { + cmd.console.addStatus(profileInstance.instructions) + } + GrailsCli.tiggerAppLoad() + return true + } + else { + System.err.println "Cannot find profile $profileName" + return false + } + } + + private boolean isDirectoryEmpty(File target) { + if (target.isDirectory()) { + try (Stream entries = Files.list(Paths.get(target.toURI()))) { + return !entries.findFirst().isPresent() + } + } + return false + } + + @Override + boolean handle(ExecutionContext executionContext) { + CommandLine commandLine = executionContext.commandLine + + String profileName = evaluateProfileName(commandLine) + + List validFlags = [INPLACE_FLAG, PROFILE_FLAG, FEATURES_FLAG] + commandLine.undeclaredOptions.each { String key, Object value -> + if (!validFlags.contains(key)) { + List possibleSolutions = validFlags.findAll { it.substring(0, 2) == key.substring(0, 2) } + StringBuilder warning = new StringBuilder("Unrecognized flag: ${key}.") + if (possibleSolutions) { + warning.append(" Possible solutions: ") + warning.append(possibleSolutions.join(", ")) + } + executionContext.console.warn(warning.toString()) + } + } + + boolean inPlace = commandLine.hasOption('inplace') || GrailsCli.isInteractiveModeActive() + String appName = commandLine.remainingArgs ? commandLine.remainingArgs[0] : "" + + List features = commandLine.optionValue("features")?.toString()?.split(',')?.toList() + + CreateAppCommandObject cmd = new CreateAppCommandObject( + appName: appName, + baseDir: executionContext.baseDir, + profileName: profileName, + grailsVersion: Environment.getPackage().getImplementationVersion() ?: GRAILS_VERSION_FALLBACK_IN_IDE_ENVIRONMENTS_FOR_RUNNING_TESTS, + features: features, + inplace: inPlace, + console: executionContext.console + ) + + return this.handle(cmd) + } + + protected boolean validateProfile(Profile profileInstance, String profileName) { + if (profileInstance == null) { + GrailsConsole.instance.error("Profile not found for name [$profileName]") + return false + } + return true + } + + private Map unzippedDirectories = new LinkedHashMap() + + @CompileDynamic + protected File unzipProfile(AntBuilder ant, Resource location) { + + def url = location.URL + def tmpDir = unzippedDirectories.get(url) + + if(tmpDir == null) { + def jarFile = IOUtils.findJarFile(url) + tmpDir = Files.createTempDirectory(UNZIP_PROFILE_TEMP_DIR).toFile() + tmpDir.deleteOnExit() + ant.unzip(src: jarFile, dest: tmpDir) + unzippedDirectories.put(url, tmpDir) + } + return tmpDir + } + + @CompileDynamic + protected void replaceBuildTokens(String profileCoords, Profile profile, List features, File targetDirectory) { + AntBuilder ant = new GrailsConsoleAntBuilder() + def ln = System.getProperty("line.separator") + + Closure repositoryUrl = { int spaces, String repo -> + repo.startsWith('http') ? "${' ' * spaces}maven { url \"${repo}\" }" : "${' ' * spaces}${repo}" + } + + def repositories = profile.repositories.collect(repositoryUrl.curry(4)).unique().join(ln) + + List profileDependencies = profile.dependencies + def dependencies = profileDependencies.findAll() { Dependency dep -> + dep.scope != 'build' + } + def buildDependencies = profileDependencies.findAll() { Dependency dep -> + dep.scope == 'build' + } + + for(Feature f in features) { + dependencies.addAll f.dependencies.findAll(){ Dependency dep -> dep.scope != 'build'} + buildDependencies.addAll f.dependencies.findAll(){ Dependency dep -> dep.scope == 'build'} + } + + dependencies.add(new Dependency(profileRepository.getProfileArtifact(profileCoords), "profile")) + + dependencies = dependencies.unique() + + List gradleDependencies = convertToGradleDependencies(dependencies) + + String dependencyString = gradleDependencies + .sort({ GradleDependency dep-> dep.scope}) + .collect( {GradleDependency dep-> dep.toString(4)}) + .unique() + .join(ln) + + def buildRepositories = profile.buildRepositories + for (Feature f in features) { + buildRepositories.addAll(f.getBuildRepositories()) + } + buildRepositories = buildRepositories.collect(repositoryUrl.curry(8)).unique().join(ln) + + buildDependencies = buildDependencies.collect() { Dependency dep -> + String artifactStr = resolveArtifactString(dep) + " classpath \"${artifactStr}\"".toString() + }.unique().join(ln) + + def buildPlugins = profile.buildPlugins.collect() { String name -> + "apply plugin:\"$name\"" + } + + for(Feature f in features) { + buildPlugins.addAll f.buildPlugins.collect() { String name -> + "apply plugin:\"$name\"" + } + } + + buildPlugins = buildPlugins.unique().join(ln) + + ant.replace(dir: targetDirectory) { + replacefilter { + replacetoken("@buildPlugins@") + replacevalue(buildPlugins) + } + replacefilter { + replacetoken("@dependencies@") + replacevalue(dependencyString) + } + replacefilter { + replacetoken("@buildDependencies@") + replacevalue(buildDependencies) + } + replacefilter { + replacetoken("@buildRepositories@") + replacevalue(buildRepositories) + } + replacefilter { + replacetoken("@repositories@") + replacevalue(repositories) + } + variables.each { k, v -> + replacefilter { + replacetoken("@${k}@".toString()) + replacevalue(v) + } + } + } + } + + protected String evaluateProfileName(CommandLine mainCommandLine) { + mainCommandLine.optionValue('profile')?.toString() ?: getDefaultProfile() + } + + protected Iterable evaluateFeatures(Profile profile, List requestedFeatures) { + if (requestedFeatures) { + List allFeatureNames = profile.features*.name + Collection validFeatureNames = requestedFeatures.intersect(allFeatureNames) + requestedFeatures.removeAll(allFeatureNames) + requestedFeatures.each { String invalidFeature -> + List possibleSolutions = allFeatureNames.findAll { + it.substring(0, 2) == invalidFeature.substring(0, 2) + } + StringBuilder warning = new StringBuilder("Feature ${invalidFeature} does not exist in the profile ${profile.name}!") + if (possibleSolutions) { + warning.append(" Possible solutions: ") + warning.append(possibleSolutions.join(", ")) + } + GrailsConsole.getInstance().warn(warning.toString()) + } + return (profile.features.findAll() { Feature f -> validFeatureNames.contains(f.name) } + profile.requiredFeatures).unique() + } + else { + return (profile.defaultFeatures + profile.requiredFeatures).unique() + } + } + + protected String getDefaultProfile() { + ProfileRepository.DEFAULT_PROFILE_NAME + } + + protected String createNewApplicationYml(String previousYml, String newYml) { + def ln = System.getProperty("line.separator") + if (newYml != previousYml) { + StringBuilder appended = new StringBuilder(previousYml.length() + newYml.length() + 30) + if(!previousYml.startsWith("---")) { + appended.append('---' + ln) + } + appended.append(previousYml).append(ln + "---" + ln) + appended.append(newYml) + appended.toString() + } else { + newYml + } + } + + private void appendToYmlSubDocument(File applicationYmlFile, String previousApplicationYml) { + appendToYmlSubDocument(applicationYmlFile, previousApplicationYml, applicationYmlFile) + } + + private void appendToYmlSubDocument(File applicationYmlFile, String previousApplicationYml, File setTo) { + String newApplicationYml = applicationYmlFile.text + if(previousApplicationYml && newApplicationYml != previousApplicationYml) { + setTo.text = createNewApplicationYml(previousApplicationYml, newApplicationYml) + } + } + + protected boolean initializeGroupAndName(String appName, boolean inplace) { + if (!appName && !inplace) { + GrailsConsole.getInstance().error("Specify an application name or use --inplace to create an application in the current directory") + return false + } + String groupAndAppName = appName + if(inplace) { + appname = new File(".").canonicalFile.name + if(!groupAndAppName) { + groupAndAppName = appname + } + } + + if(!groupAndAppName) { + GrailsConsole.getInstance().error("Specify an application name or use --inplace to create an application in the current directory") + return false + } + + try { + defaultpackagename = establishGroupAndAppName(groupAndAppName) + } catch (IllegalArgumentException e ) { + GrailsConsole.instance.error(e.message) + return false + } + } + + private void initializeVariables(String profileName, String grailsVersion) { + variables.APPNAME = appname + + variables['grails.codegen.defaultPackage'] = defaultpackagename + variables['grails.codegen.defaultPackage.path'] = defaultpackagename.replace('.', '/') + + def projectClassName = GrailsNameUtils.getNameFromScript(appname) + + variables['grails.codegen.projectClassName'] = projectClassName + variables['grails.codegen.projectNaturalName'] = GrailsNameUtils.getNaturalName(projectClassName) + variables['grails.codegen.projectName'] = GrailsNameUtils.getScriptName(projectClassName) + variables['grails.profile'] = profileName + variables['grails.version'] = grailsVersion + variables['grails.app.name'] = appname + variables['grails.app.group'] = groupname + } + + private String establishGroupAndAppName(String groupAndAppName) { + String defaultPackage + List parts = groupAndAppName.split(/\./) as List + if (parts.size() == 1) { + appname = parts[0] + defaultPackage = createValidPackageName() + groupname = defaultPackage + } else { + appname = parts[-1] + groupname = parts[0..-2].join('.') + defaultPackage = groupname + } + return defaultPackage + } + + private String createValidPackageName() { + String defaultPackage = appname.split(/[-]+/).collect { String token -> (token.toLowerCase().toCharArray().findAll { char ch -> Character.isJavaIdentifierPart(ch) } as char[]) as String }.join('.') + if(!GrailsNameUtils.isValidJavaPackage(defaultPackage)) { + throw new IllegalArgumentException("Cannot create a valid package name for [$appname]. Please specify a name that is also a valid Java package.") + } + return defaultPackage + } + + @CompileStatic(TypeCheckingMode.SKIP) + private void copySkeleton(Profile profile, Profile participatingProfile) { + def buildMergeProfileNames = profile.buildMergeProfileNames + def excludes = profile.skeletonExcludes + if (profile == participatingProfile) { + excludes = [] + } + + AntBuilder ant = new GrailsConsoleAntBuilder() + + def skeletonResource = participatingProfile.profileDir.createRelative("skeleton") + File skeletonDir + File tmpDir + if(skeletonResource instanceof FileSystemResource) { + skeletonDir = skeletonResource.file + } + else { + // establish the JAR file name and extract + tmpDir = unzipProfile(ant, skeletonResource) + skeletonDir = new File(tmpDir, "META-INF/grails-profile/skeleton") + } + copySrcToTarget(ant, skeletonDir, excludes, profile.binaryExtensions) + + Set sourceBuildGradles = findAllFilesByName(skeletonDir, BUILD_GRADLE) + + sourceBuildGradles.each { File srcFile -> + final File srcDir = srcFile.parentFile + final File destDir = getDestinationDirectory(srcFile) + final File destFile = new File(destDir, BUILD_GRADLE) + + ant.copy(file:"${srcDir}/.gitignore", todir: destDir, failonerror:false) + + if (!destFile.exists()) { + ant.copy file:srcFile, tofile:destFile + } else if (buildMergeProfileNames.contains(participatingProfile.name)) { + def concatFile = "${destDir}/concat-build.gradle" + ant.move(file:destFile, tofile: concatFile) + ant.concat([destfile: destFile, fixlastline: true], { + path { + pathelement location: concatFile + pathelement location: srcFile + } + }) + ant.delete(file: concatFile, failonerror: false) + } + } + + Set sourceGradleProperties = findAllFilesByName(skeletonDir, GRADLE_PROPERTIES) + + sourceGradleProperties.each { File srcFile -> + File destDir = getDestinationDirectory(srcFile) + File destFile = new File(destDir, GRADLE_PROPERTIES) + + if (!destFile.exists()) { + ant.copy file: srcFile, tofile: destFile + } else { + def concatGradlePropertiesFile = "${destDir}/concat-gradle.properties" + ant.move(file: destFile, tofile: concatGradlePropertiesFile) + ant.concat([destfile: destFile, fixlastline: true], { + path { + pathelement location: concatGradlePropertiesFile + pathelement location: srcFile + } + }) + ant.delete(file: concatGradlePropertiesFile, failonerror: false) + } + } + + ant.chmod(dir: targetDirectory, includes: profile.executablePatterns.join(' '), perm: 'u+x') + + // Cleanup temporal directories + deleteDirectory(tmpDir) + deleteDirectory(skeletonDir) + } + + @CompileDynamic + protected void copySrcToTarget(GrailsConsoleAntBuilder ant, File srcDir, List excludes, Set binaryFileExtensions) { + ant.copy(todir: targetDirectory, overwrite: true, encoding: 'UTF-8') { + fileSet(dir: srcDir, casesensitive: false) { + exclude(name: '**/.gitkeep') + for (exc in excludes) { + exclude name: exc + } + exclude name: "**/"+BUILD_GRADLE + exclude name: "**/"+GRADLE_PROPERTIES + binaryFileExtensions.each { ext -> + exclude(name: "**/*.${ext}") + } + } + filterset { + variables.each { k, v -> + filter(token: k, value: v) + } + } + mapper { + filtermapper { + variables.each { k, v -> + replacestring(from: "@${k}@".toString(), to: v) + } + } + } + } + ant.copy(todir: targetDirectory, overwrite: true) { + fileSet(dir: srcDir, casesensitive: false) { + binaryFileExtensions.each { ext -> + include(name: "**/*.${ext}") + } + for (exc in excludes) { + exclude name: exc + } + exclude name: "**/"+BUILD_GRADLE + } + mapper { + filtermapper { + variables.each { k, v -> + replacestring(from: "@${k}@".toString(), to: v) + } + } + } + } + } + + protected String resolveArtifactString(Dependency dep) { + def artifact = dep.artifact + def v = artifact.version.replace('BOM', '') + + return v ? "${artifact.groupId}:${artifact.artifactId}:${v}" : "${artifact.groupId}:${artifact.artifactId}" + } + + private void deleteDirectory(File directory) { + try { + directory?.deleteDir() + } catch (Throwable t) { + // Ignore error deleting temporal directory + } + } + + protected List convertToGradleDependencies(List dependencies) { + List gradleDependencies = [] + gradleDependencies.addAll(dependencies.collect { new GradleDependency(it) }) + gradleDependencies + } + + static class CreateAppCommandObject { + String appName + File baseDir + String profileName + String grailsVersion + List features + boolean inplace = false + GrailsConsole console + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CreatePluginCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CreatePluginCommand.groovy new file mode 100644 index 00000000000..9ea662a99df --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CreatePluginCommand.groovy @@ -0,0 +1,60 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands + +import groovy.transform.CompileStatic +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.Profile +/** + * A command for creating a plugin + * + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class CreatePluginCommand extends CreateAppCommand { + + public static final String NAME = "create-plugin" + + CreatePluginCommand() { + description.description = "Creates a plugin" + description.usage = "create-plugin [NAME]" + } + + @Override + protected void populateDescription() { + description.argument(name: "Plugin Name", description: "The name of the plugin to create.", required: false) + } + + @Override + String getName() { NAME } + + @Override + protected String getDefaultProfile() { "web-plugin" } + + protected boolean validateProfile(Profile profileInstance, String profileName, ExecutionContext executionContext) { + def pluginProfile = profileInstance.extends.find() { Profile parent -> parent.name == 'plugin' } + if(profileName != 'plugin' && pluginProfile == null) { + executionContext.console.error("No valid plugin profile found for name [$profileName]") + return false + } + else { + return super.validateProfile(profileInstance, profileName) + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CreateProfileCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CreateProfileCommand.groovy new file mode 100644 index 00000000000..a2d72abdc92 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/CreateProfileCommand.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile.commands + + + +/** + * Creates a profile + * + * @author Graeme Rocher + * @since 3.1 + */ +class CreateProfileCommand extends CreateAppCommand { + public static final String NAME = "create-profile" + + CreateProfileCommand() { + description.description = "Creates a profile" + description.usage = "create-profile [NAME]" + } + + @Override + protected void populateDescription() { + description.argument(name: "Profile Name", description: "The name of the plugin to create.", required: false) + } + + @Override + String getName() { NAME } + + @Override + protected String getDefaultProfile() { "profile" } + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy new file mode 100644 index 00000000000..bea967a0343 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy @@ -0,0 +1,111 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands + +import grails.build.logging.GrailsConsole +import groovy.transform.CompileDynamic +import jline.console.completer.Completer +import org.grails.cli.profile.* +import org.grails.cli.profile.steps.StepRegistry +/** + * Simple implementation of the {@link MultiStepCommand} abstract class that parses commands defined in YAML or JSON + * + * @author Lari Hotari + * @author Graeme Rocher + * @since 3.0 + */ +class DefaultMultiStepCommand extends MultiStepCommand { + private Map data + private List steps + + final CommandDescription description + + DefaultMultiStepCommand(String name, Profile profile, Map data) { + super(name, profile) + this.data = data + + def description = data?.description + if(description instanceof List) { + List descList = (List)description + if(descList) { + + this.description = new CommandDescription(name: name, description: descList.get(0).toString(), usage: data?.usage) + + if(descList.size()>1) { + for(arg in descList[1..-1]) { + if(arg instanceof Map) { + Map map = (Map)arg + if(map.containsKey('usage')) { + this.description.usage = map.get('usage')?.toString() + } + else if(map.containsKey('completer')) { + def completerClass = map.get('completer') + if(completerClass) { + try { + this.description.completer = (Completer)Thread.currentThread().contextClassLoader.loadClass(completerClass.toString()).newInstance() + } catch (e) { + // ignore + } + } + } + else { + handleArgumentOrFlag(map, 'argument') + handleArgumentOrFlag(map, 'flag') + } + } + } + } + } + } + else { + this.description = new CommandDescription(name: name, description: description.toString(), usage: data?.usage) + } + } + + @CompileDynamic + boolean handleArgumentOrFlag(Map map, String name) { + try { + if(map.containsKey(name)) { + def argName = map.remove(name) + map.put('name', argName) + this.description."$name"(map) + return true + } + } catch (Throwable e) { + GrailsConsole.getInstance().error("Invalid $name found in [$profile.name] profile ${map}: ${e.message}", e) + } + return false + } + + List getSteps() { + if(steps==null) { + steps = [] + data.steps?.each { + Map stepParameters = it.collectEntries { k,v -> [k as String, v] } + AbstractStep step = createStep(stepParameters) + if (step != null) { + steps.add(step) + } + } + } + steps + } + + protected AbstractStep createStep(Map stepParameters) { + StepRegistry.getStep(stepParameters.command?.toString(), this, stepParameters) + } + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy new file mode 100644 index 00000000000..7608fbbbc3a --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy @@ -0,0 +1,151 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile.commands + +import grails.build.logging.GrailsConsole +import jline.console.completer.Completer +import org.grails.build.parsing.CommandLine +import org.grails.build.parsing.CommandLineParser +import org.grails.cli.profile.Command +import org.grails.cli.profile.CommandDescription +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileCommand +import org.grails.cli.profile.ProfileRepository +import org.grails.cli.profile.ProfileRepositoryAware +import org.grails.cli.profile.ProjectCommand +import org.grails.cli.profile.ProjectContext +import org.grails.cli.profile.ProjectContextAware + + +/** + * @author Graeme Rocher + */ +class HelpCommand implements ProfileCommand, Completer, ProjectContextAware, ProfileRepositoryAware{ + + public static final String NAME = "help" + + final CommandDescription description = new CommandDescription(NAME, "Prints help information for a specific command", "help [COMMAND NAME]") + + Profile profile + ProfileRepository profileRepository + ProjectContext projectContext + + CommandLineParser cliParser = new CommandLineParser() + + @Override + String getName() { + return NAME + } + + + @Override + boolean handle(ExecutionContext executionContext) { + def console = executionContext.console + def commandLine = executionContext.commandLine + Collection allCommands=findAllCommands() + String remainingArgs = commandLine.getRemainingArgsString() + if(remainingArgs?.trim()) { + CommandLine remainingArgsCommand = cliParser.parseString(remainingArgs) + String helpCommandName = remainingArgsCommand.getCommandName() + for (CommandDescription desc : allCommands) { + if(desc.name == helpCommandName) { + console.addStatus("Command: $desc.name") + console.addStatus("Description:") + console.println "${desc.description?:''}" + if(desc.usage) { + console.println() + console.addStatus("Usage:") + console.println "${desc.usage}" + } + if(desc.arguments) { + console.println() + console.addStatus("Arguments:") + for(arg in desc.arguments) { + console.println "* ${arg.name} - ${arg.description?:''} (${arg.required ? 'REQUIRED' : 'OPTIONAL'})" + } + } + if(desc.flags) { + console.println() + console.addStatus("Flags:") + for(arg in desc.flags) { + console.println "* ${arg.name} - ${arg.description ?: ''}" + } + } + return true + } + } + console.error "Help for command $helpCommandName not found" + return false + } else { + console.log ''' +Usage (optionals marked with *):' +grails [environment]* [target] [arguments]*' + +''' + console.addStatus("Examples:") + console.log('$ grails dev run-app') + console.log('$ grails create-app books') + console.log '' + console.addStatus('Available Commands (type grails help \'command-name\' for more info):') + console.addStatus("${'Command Name'.padRight(37)} Command Description") + console.println('-' * 100) + for (CommandDescription desc : allCommands) { + console.println "${desc.name.padRight(40)}${desc.description}" + } + console.println() + console.addStatus("Detailed usage with help [command]") + return true + } + + } + + @Override + int complete(String buffer, int cursor, List candidates) { + def allCommands = findAllCommands().collect() { CommandDescription desc -> desc.name } + + for(cmd in allCommands) { + if(buffer) { + if(cmd.startsWith(buffer)) { + candidates << cmd.substring(buffer.size()) + } + } + else { + candidates << cmd + } + } + return cursor + } + + + protected Collection findAllCommands() { + Iterable commands + if(profile) { + commands = profile.getCommands(projectContext) + } + else { + commands = CommandRegistry.findCommands(profileRepository).findAll() { Command cmd -> + !(cmd instanceof ProjectCommand) + } + } + return commands + .collect() { Command cmd -> cmd.description } + .unique() { CommandDescription cmd -> cmd.name } + .sort(false) { CommandDescription itDesc -> itDesc.name } + } + + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ListProfilesCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ListProfilesCommand.groovy new file mode 100644 index 00000000000..98be06c5f62 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ListProfilesCommand.groovy @@ -0,0 +1,53 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands + +import grails.build.logging.GrailsConsole +import groovy.transform.CompileStatic +import org.grails.cli.profile.Command +import org.grails.cli.profile.CommandDescription +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileRepository +import org.grails.cli.profile.ProfileRepositoryAware + +/** + * Lists the available {@link org.grails.cli.profile.Profile} instances  + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class ListProfilesCommand implements Command, ProfileRepositoryAware { + + final String name = "list-profiles" + final CommandDescription description = new CommandDescription(name, "Lists the available profiles", "grails list-profiles") + + ProfileRepository profileRepository + + @Override + boolean handle(ExecutionContext executionContext) { + def allProfiles = profileRepository.allProfiles + def console = executionContext.console + console.addStatus("Available Profiles") + console.log('--------------------') + for(Profile p in allProfiles) { + console.log("* $p.name - ${p.description}") + } + + return true + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy new file mode 100644 index 00000000000..78f6d492f31 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy @@ -0,0 +1,72 @@ +package org.grails.cli.profile.commands + +import groovy.transform.CompileStatic +import jline.console.completer.Completer +import jline.console.completer.FileNameCompleter +import org.grails.cli.profile.CommandDescription +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.ProjectCommand + +import java.awt.Desktop + +/* + * Copyright 2014 original 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. + */ + +/** + * @author graemerocher + */ +@CompileStatic +class OpenCommand implements ProjectCommand, Completer { + + public static final String NAME = "open" + + @Override + String getName() { + NAME + } + + CommandDescription description = new CommandDescription(NAME, "Opens a file in the project", "open [FILE PATH]") + + @Override + boolean handle(ExecutionContext executionContext) { + def filePath = executionContext.commandLine.remainingArgsString + if(filePath) { + if(filePath == 'test-report') { + filePath = 'build/reports/tests/index.html' + } + if(Desktop.isDesktopSupported()) { + try { + Desktop.desktop.open(new File(filePath)) + return true + } catch (e) { + executionContext.console.error("Error opening file $filePath: $e.message", e) + } + } + else { + executionContext.console.error("File opening not supported by JVM, use native OS command") + } + } + else { + return true + } + return false + } + + @Override + int complete(String buffer, int cursor, List candidates) { + return new FileNameCompleter().complete(buffer, cursor, candidates) + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ProfileInfoCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ProfileInfoCommand.groovy new file mode 100644 index 00000000000..b97a7cc8ca5 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/ProfileInfoCommand.groovy @@ -0,0 +1,128 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile.commands + +import grails.build.logging.GrailsConsole +import grails.config.ConfigMap +import grails.util.BuildSettings +import groovy.transform.CompileStatic +import org.grails.cli.profile.Command +import org.grails.cli.profile.CommandDescription +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.Feature +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileRepository +import org.grails.cli.profile.ProfileRepositoryAware +import org.grails.cli.profile.ProjectContext +import org.grails.config.CodeGenConfig + + +/** + * A command to find out information about the given profile + * + * @author Graeme Rocher + * @since 3.1 + */ +@CompileStatic +class ProfileInfoCommand extends ArgumentCompletingCommand implements ProfileRepositoryAware { + + public static final String NAME = 'profile-info' + + final String name = NAME + final CommandDescription description = new CommandDescription(name, "Display information about a given profile") + + ProfileRepository profileRepository + + ProfileInfoCommand() { + description.argument(name:"Profile Name", description: "The name or coordinates of the profile", required:true) + } + + void setProfileRepository(ProfileRepository profileRepository) { + this.profileRepository = profileRepository + } + + @Override + boolean handle(ExecutionContext executionContext) { + def console = executionContext.console + if(profileRepository == null) { + console.error("No profile repository provided") + return false + } + else { + + def profileName = executionContext.commandLine.remainingArgs[0] + + def profile = profileRepository.getProfile(profileName) + if(profile == null) { + console.error("Profile not found for name [$profileName]") + } + else { + console.log("Profile: ${profile.name}") + console.log('--------------------') + console.log(profile.description) + console.log('') + console.log('Provided Commands:') + console.log('--------------------') + Iterable commands = findCommands(profile, console).toUnique { Command c -> c.name} + + for(cmd in commands) { + def description = cmd.description + console.log("* ${description.name} - ${description.description}") + } + console.log('') + console.log('Provided Features:') + console.log('--------------------') + def features = profile.features + + for(feature in features) { + console.log("* ${feature.name} - ${feature.description}") + } + } + } + return true + } + + protected Iterable findCommands(Profile profile, GrailsConsole console) { + def commands = profile.getCommands(new ProjectContext() { + @Override + GrailsConsole getConsole() { + console + } + + @Override + File getBaseDir() { + return new File(".") + } + + @Override + ConfigMap getConfig() { + return new CodeGenConfig() + } + + @Override + String navigateConfig(String... path) { + return config.navigate(path) + } + + @Override + def T navigateConfigForType(Class requiredType, String... path) { + return (T) config.navigate(path) + } + }) + commands + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/events/CommandEvents.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/events/CommandEvents.groovy new file mode 100644 index 00000000000..de503b5e3f6 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/events/CommandEvents.groovy @@ -0,0 +1,78 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.events + +import groovy.transform.CompileStatic +import groovy.transform.Generated +import org.grails.cli.profile.commands.script.GroovyScriptCommand + + +/** + * Allows for listening and reacting to events triggered by other commands + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +trait CommandEvents { + + + /** + * Register to listen for an event + * + * @param eventName The name of the event + * @param callable The closure that is executed when the event is fired + */ + @Generated + void on(String eventName, @DelegatesTo(GroovyScriptCommand) Closure callable) { + EventStorage.registerEvent(eventName, callable) + } + + + /** + * Register to listen for an event that runs before the given command + * + * @param eventName The name of the event + * @param callable The closure that is executed when the event is fired + */ + @Generated + void before(String commandName, @DelegatesTo(GroovyScriptCommand) Closure callable) { + EventStorage.registerEvent("${commandName}Start", callable) + } + + /** + * Register to listen for an event that runs before the given command + * + * @param eventName The name of the event + * @param callable The closure that is executed when the event is fired + */ + @Generated + void after(String commandName, @DelegatesTo(GroovyScriptCommand) Closure callable) { + EventStorage.registerEvent("${commandName}End", callable) + } + + /** + * Notify of an event + * + * @param eventName The name of the event + * @param args The arguments to the event + */ + @Generated + void notify(String eventName, Object...args) { + EventStorage.fireEvent(this, eventName, args) + } + +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/events/EventStorage.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/events/EventStorage.groovy new file mode 100644 index 00000000000..a71146f5c29 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/events/EventStorage.groovy @@ -0,0 +1,46 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.events + +import groovy.transform.CompileStatic + + +/** + * Stores command line events + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class EventStorage { + + private static Map> eventListeners = [:].withDefault { [] } + + static void registerEvent(String eventName, Closure callable) { + if(!eventListeners[eventName].contains(callable)) { + eventListeners[eventName] << callable + } + } + + static void fireEvent(Object caller, String eventName, Object...args) { + def listeners = eventListeners[eventName] + for(listener in listeners) { + listener.delegate = caller + listener.call args + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ApplicationContextCommandFactory.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ApplicationContextCommandFactory.groovy new file mode 100644 index 00000000000..4f886d519b9 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ApplicationContextCommandFactory.groovy @@ -0,0 +1,52 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.factory + +import grails.build.logging.GrailsConsole +import grails.util.Named +import org.grails.cli.gradle.commands.GradleTaskCommandAdapter +import org.grails.cli.profile.Command +import org.grails.cli.profile.Profile + + + +/** + * Automatically populates ApplicationContext command instances and adapts the interface to the shell + * + * @author Graeme Rocher + * @since 3.0 + */ +class ApplicationContextCommandFactory implements CommandFactory { + @Override + Collection findCommands(Profile profile, boolean inherited) { + if(inherited) return Collections.emptyList() + + try { + def classLoader = Thread.currentThread().contextClassLoader + Class registry + try { + registry = classLoader.loadClass("grails.dev.commands.ApplicationContextCommandRegistry") + } catch (ClassNotFoundException cnf) { + return [] + } + def commands = registry.findCommands() + return commands.collect() { Named named -> new GradleTaskCommandAdapter(profile, named) } + } catch (Throwable e) { + GrailsConsole.instance.error("Error occurred loading commands: $e.message", e) + return [] + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ClasspathCommandResourceResolver.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ClasspathCommandResourceResolver.groovy new file mode 100644 index 00000000000..4dfe4ed96ca --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ClasspathCommandResourceResolver.groovy @@ -0,0 +1,57 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.factory + +import groovy.transform.CompileStatic +import org.grails.cli.profile.Profile +import org.grails.io.support.PathMatchingResourcePatternResolver +import org.grails.io.support.Resource + + +/** + * A {@link CommandResourceResolver} that resolves commands from the classpath under the directory META-INF/commands + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class ClasspathCommandResourceResolver implements CommandResourceResolver { + final Collection matchingFileExtensions + ClassLoader classLoader + + private Collection resources = null + + ClasspathCommandResourceResolver(Collection matchingFileExtensions) { + this.matchingFileExtensions = matchingFileExtensions + } + + @Override + Collection findCommandResources(Profile profile) { + if(resources != null) return resources + def classLoader = classLoader ?: Thread.currentThread().contextClassLoader + PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(classLoader) + + try { + resources = [] + for(String ext in matchingFileExtensions) { + resources.addAll resourcePatternResolver.getResources("classpath*:META-INF/commands/*.$ext").toList() + } + return resources + } catch (Throwable e) { + return [] + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/CommandFactory.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/CommandFactory.groovy new file mode 100644 index 00000000000..2ad7d8e5570 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/CommandFactory.groovy @@ -0,0 +1,40 @@ +package org.grails.cli.profile.commands.factory + +import org.grails.cli.profile.Command +import org.grails.cli.profile.Profile + +/* + * Copyright 2014 original 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. + */ + +/** + * Factory for the creation of {@link Command} instances + * + * @author Graeme Rocher + * @since 3.0 + */ +interface CommandFactory { + + /** + * Creates a command for the given name + * + * @param name The name of the command + * @param profile The {@link Profile} + * @param inherited Whether the profile passed is inherited (ie a parent profile) + * @return A command or null if it wasn't possible to create one + */ + Collection findCommands( Profile profile, boolean inherited ) + +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/CommandResourceResolver.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/CommandResourceResolver.groovy new file mode 100644 index 00000000000..3c55bbd72b0 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/CommandResourceResolver.groovy @@ -0,0 +1,42 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.factory + +import org.grails.cli.profile.Profile +import org.grails.io.support.Resource + +/** + * @since 3.0 + * @author Graeme Rocher + */ +interface CommandResourceResolver { + + /** + * Finds {@link org.grails.cli.profile.Command} resources for the given profile + * + * @param profile The {@link Profile} instance + * @return A collection of {@link Resource} instances + */ + Collection findCommandResources(Profile profile) + + /** + * The pattern to match file names with + * + * @return A regex pattern + */ + Collection getMatchingFileExtensions() + +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/FileSystemCommandResourceResolver.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/FileSystemCommandResourceResolver.groovy new file mode 100644 index 00000000000..904df2967a2 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/FileSystemCommandResourceResolver.groovy @@ -0,0 +1,59 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.factory + +import groovy.transform.CompileStatic +import org.grails.cli.profile.Profile +import org.grails.io.support.PathMatchingResourcePatternResolver +import org.grails.io.support.Resource +import org.grails.io.support.StaticResourceLoader + + +/** + * A {@link CommandResourceResolver} that resolves from the file system + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class FileSystemCommandResourceResolver implements CommandResourceResolver { + + final Collection matchingFileExtensions + + FileSystemCommandResourceResolver(Collection matchingFileExtensions) { + this.matchingFileExtensions = matchingFileExtensions + } + + @Override + Collection findCommandResources(Profile profile) { + Resource commandsDir = getCommandsDirectory(profile) + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(new StaticResourceLoader(commandsDir)) + if(commandsDir.exists()) { + Collection commandFiles = [] + for(ext in matchingFileExtensions) { + commandFiles.addAll resolver.getResources("*.$ext") + } + commandFiles = commandFiles.sort(false) { Resource file -> file.filename } + return commandFiles + } + return [] + } + + protected Resource getCommandsDirectory(Profile profile) { + profile.profileDir.createRelative("commands/") + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/GroovyScriptCommandFactory.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/GroovyScriptCommandFactory.groovy new file mode 100644 index 00000000000..1b9aba68469 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/GroovyScriptCommandFactory.groovy @@ -0,0 +1,88 @@ +package org.grails.cli.profile.commands.factory + +import grails.build.logging.GrailsConsole +import grails.util.GrailsNameUtils +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer +import org.codehaus.groovy.control.customizers.ImportCustomizer +import org.grails.cli.profile.Command +import org.grails.cli.profile.Profile +import org.grails.cli.profile.commands.script.GroovyScriptCommand +import org.grails.cli.profile.commands.script.GroovyScriptCommandTransform +import org.grails.io.support.Resource + +import java.nio.charset.StandardCharsets + +/* + * Copyright 2014 original 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. + */ + +/** + * A {@link CommandFactory} that creates {@link Command} instances from Groovy scripts + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class GroovyScriptCommandFactory extends ResourceResolvingCommandFactory { + + final Collection matchingFileExtensions = ["groovy"] + final String fileNamePattern = /^.*\.(groovy)$/ + + @Override + protected GroovyScriptCommand readCommandFile(Resource resource) { + GroovyClassLoader classLoader = createGroovyScriptCommandClassLoader() + try { + return (GroovyScriptCommand) classLoader.parseClass(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8 ), resource.filename).newInstance() + } catch (Throwable e) { + GrailsConsole.getInstance().error("Failed to compile ${resource.filename}: " + e.getMessage(), e) + } + } + + @CompileDynamic + public static GroovyClassLoader createGroovyScriptCommandClassLoader() { + def configuration = new CompilerConfiguration() + // TODO: Report bug, this fails with @CompileStatic with a ClassCastException + String baseClassName = GroovyScriptCommand.class.getName() + return createClassLoaderForBaseClass(configuration, baseClassName) + } + + private static GroovyClassLoader createClassLoaderForBaseClass(CompilerConfiguration configuration, String baseClassName) { + configuration.setScriptBaseClass(baseClassName) + + + def importCustomizer = new ImportCustomizer() + importCustomizer.addStarImports("org.grails.cli.interactive.completers") + importCustomizer.addStarImports("grails.util") + importCustomizer.addStarImports("grails.codegen.model") + configuration.addCompilationCustomizers(importCustomizer,new ASTTransformationCustomizer(new GroovyScriptCommandTransform())) + def classLoader = new GroovyClassLoader(Thread.currentThread().contextClassLoader, configuration) + return classLoader + } + + @Override + protected String evaluateFileName(String fileName) { + def fn = super.evaluateFileName(fileName) + return fn.contains('-') ? fn.toLowerCase() : GrailsNameUtils.getScriptName(fn) + } + + @Override + protected Command createCommand(Profile profile, String commandName, Resource resource, GroovyScriptCommand data) { + data.setProfile(profile) + return data + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ResourceResolvingCommandFactory.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ResourceResolvingCommandFactory.groovy new file mode 100644 index 00000000000..71932e0958a --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ResourceResolvingCommandFactory.groovy @@ -0,0 +1,96 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.factory + +import grails.util.BuildSettings +import groovy.transform.CompileStatic +import org.grails.cli.profile.Command +import org.grails.cli.profile.Profile +import org.grails.io.support.FileSystemResource +import org.grails.io.support.Resource + +import java.util.regex.Pattern + + +/** + * A abstract {@link CommandFactory} that reads from the file system + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +abstract class ResourceResolvingCommandFactory implements CommandFactory { + + @Override + Collection findCommands(Profile profile, boolean inherited) { + def resources = findCommandResources(profile, inherited) + Collection commands = [] + for(Resource resource in resources) { + String commandName = evaluateFileName(resource.filename) + def data = readCommandFile(resource) + + def command = createCommand(profile, commandName, resource, data) + if(command) + commands << command + } + return commands + } + + protected String evaluateFileName(String fileName) { + fileName - Pattern.compile(/\.(${getMatchingFileExtensions().join('|')})$/) + } + + protected Collection findCommandResources(Profile profile, boolean inherited) { + Collection allResources = [] + for(CommandResourceResolver resolver in getCommandResolvers(inherited)) { + allResources.addAll resolver.findCommandResources(profile) + } + return allResources + } + + protected Collection getCommandResolvers(boolean inherited) { + def profileCommandsResolver = new FileSystemCommandResourceResolver(matchingFileExtensions) + Collection commandResolvers = [] + if(inherited) { + commandResolvers.add(profileCommandsResolver) + return commandResolvers + } + else { + def localCommandsResolver1 = new FileSystemCommandResourceResolver(matchingFileExtensions) { + @Override + protected Resource getCommandsDirectory(Profile profile) { + return new FileSystemResource("${BuildSettings.BASE_DIR}/src/main/scripts/" ) + } + } + def localCommandsResolver2 = new FileSystemCommandResourceResolver(matchingFileExtensions) { + @Override + protected Resource getCommandsDirectory(Profile profile) { + return new FileSystemResource("${BuildSettings.BASE_DIR}/commands/" ) + } + } + commandResolvers.addAll([profileCommandsResolver, localCommandsResolver1, localCommandsResolver2, new ClasspathCommandResourceResolver(matchingFileExtensions) ]) + return commandResolvers + } + } + + protected abstract T readCommandFile(Resource resource) + + protected abstract Command createCommand(Profile profile, String commandName, Resource resource, T data) + + protected abstract Collection getMatchingFileExtensions() + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ServiceCommandFactory.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ServiceCommandFactory.groovy new file mode 100644 index 00000000000..a1a4441696b --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/ServiceCommandFactory.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.factory + +import groovy.transform.CompileStatic +import org.grails.cli.profile.Command +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileCommand + + + +/** + * Uses the service registry pattern to locate commands + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class ServiceCommandFactory implements CommandFactory { + @Override + Collection findCommands(Profile profile, boolean inherited) { + if(inherited) return Collections.emptyList() + ServiceLoader.load(Command).findAll() { Command cmd -> + cmd instanceof ProfileCommand + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/YamlCommandFactory.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/YamlCommandFactory.groovy new file mode 100644 index 00000000000..02a71a73e20 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/factory/YamlCommandFactory.groovy @@ -0,0 +1,75 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.factory + +import groovy.json.JsonParserType +import groovy.json.JsonSlurper +import groovy.transform.CompileStatic +import org.grails.cli.profile.Command +import org.grails.cli.profile.Profile +import org.grails.cli.profile.commands.DefaultMultiStepCommand +import org.grails.io.support.Resource +import org.yaml.snakeyaml.LoaderOptions +import org.yaml.snakeyaml.Yaml +import org.yaml.snakeyaml.constructor.SafeConstructor + +import java.util.regex.Pattern + + +/** + * A {@link CommandFactory} that can discover commands defined in YAML or JSON + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class YamlCommandFactory extends ResourceResolvingCommandFactory { + protected Yaml yamlParser=new Yaml(new SafeConstructor(new LoaderOptions())) + // LAX parser for JSON: http://mrhaki.blogspot.ie/2014/08/groovy-goodness-relax-groovy-will-parse.html + protected JsonSlurper jsonSlurper = new JsonSlurper().setType(JsonParserType.LAX) + + final Collection matchingFileExtensions = ["yml", "json"] + final String fileNamePattern = /^.*\.(yml|json)$/ + + @Override + protected Map readCommandFile(Resource resource) { + Map data + InputStream is + + try { + is = resource.inputStream + if(resource.filename.endsWith('.json')) { + data = jsonSlurper.parse(is, "UTF-8") as Map + } else { + data = yamlParser.load(is) + } + } finally { + is?.close() + } + return data + } + + protected Command createCommand(Profile profile, String commandName, Resource resource, Map data) { + if(!data.profile || profile.name == data.profile?.toString()) { + Command command = new DefaultMultiStepCommand( commandName, profile, data ) + Object minArguments = data?.minArguments + command.minArguments = minArguments instanceof Integer ? (Integer)minArguments : 1 + return command + } + return null + } + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/FileSystemInteraction.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/FileSystemInteraction.groovy new file mode 100644 index 00000000000..0dca30817c9 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/FileSystemInteraction.groovy @@ -0,0 +1,142 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.io + +import org.grails.io.support.Resource + + +/** + * Utility methods exposed to scripts for interacting with resources (found on the file system or jars) and the file system + * + * @author Graeme Rocher + * @since 3.0 + */ +interface FileSystemInteraction { + + /** + * Makes a directory + * + * @param path The path to the directory + */ + FileSystemInteraction mkdir(path) + /** + * Deletes a file + * + * @param path The path to the file + */ + FileSystemInteraction delete(path) + /** + * Allows Gradle style simple copy specs + * + * @param callable The callable + * @return this + */ + FileSystemInteraction copy(@DelegatesTo(CopySpec) Closure callable) + /** + * Copies a resource to the target destination + * + * @param path The path + * @param destination The destination + */ + FileSystemInteraction copy(path, destination) + /** + * Copies resources to the target destination + * + * @param path The path + * @param destination The destination + */ + FileSystemInteraction copyAll(Iterable resources, destination) + /** + * Copy a Resource from the given location to the given directory or location + * + * @param from The resource to copy + * @param to The location to copy to + * @return The {@FileSystemInteraction} instance + */ + FileSystemInteraction copy(Resource from, File to) + /** + * Obtain a file for the given path + * + * @param path The path + * @return The file + */ + File file(Object path) + /** + * @return The target build directory + */ + File getBuildDir() + /** + * @return The directory where resources are processed to + */ + File getResourcesDir() + /** + * @return The directory where classes are compiled to + */ + File getClassesDir() + /** + * Finds a source file for the given class name + * @param className The class name + * @return The source resource + */ + Resource source(String className) + /** + * Obtain a resource for the given path + * @param path The path + * @return The resource + */ + Resource resource(Object path) + /** + * Obtain resources for the given pattern + * + * @param pattern The pattern + * @return The resources + */ + Collection resources(String pattern) + /** + * Obtain the path of the resource relative to the current project + * + * @param path The path to inspect + * @return The relative path + */ + String projectPath(Object path) + + /** + * The class name of the given resource + * + * @param resource The resource + * @return The class name + */ + String className(Resource resource) + + /** + * Get files matching the given pattern + * + * @param pattern The pattern + * @return the files + */ + Collection files(String pattern) + + static class CopySpec { + def from + def into + void from(path) { + this.from = path + } + void into(path) { + this.into = path + } + } +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImpl.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImpl.groovy new file mode 100644 index 00000000000..f1a11948e2f --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImpl.groovy @@ -0,0 +1,284 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.io + +import grails.build.logging.GrailsConsole +import grails.util.BuildSettings +import groovy.transform.CompileStatic +import org.grails.cli.profile.ExecutionContext +import org.grails.io.support.DefaultResourceLoader +import org.grails.io.support.FileSystemResource +import org.grails.io.support.GrailsResourceUtils +import org.grails.io.support.PathMatchingResourcePatternResolver +import org.grails.io.support.Resource +import org.grails.io.support.ResourceLoader +import org.grails.io.support.ResourceLocator +import org.grails.io.support.SpringIOUtils + + +/** + * Utility methods exposed to scripts for interacting with resources (found on the file system or jars) and the file system + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class FileSystemInteractionImpl implements FileSystemInteraction { + + ExecutionContext executionContext + ResourceLoader resourceLoader + PathMatchingResourcePatternResolver resourcePatternResolver + ResourceLocator resourceLocator + + FileSystemInteractionImpl(ExecutionContext executionContext, ResourceLoader resourceLoader = new DefaultResourceLoader()) { + this.executionContext = executionContext + this.resourceLoader = resourceLoader + this.resourceLocator = new ResourceLocator() + this.resourceLocator.setSearchLocation(executionContext.baseDir.absolutePath) + this.resourcePatternResolver = new PathMatchingResourcePatternResolver(resourceLoader) + } + + + /** + * Makes a directory + * + * @param path The path to the directory + */ + @Override + FileSystemInteractionImpl mkdir(path) { + file(path)?.mkdirs() + return this + } + + /** + * Deletes a file + * + * @param path The path to the file + */ + @Override + FileSystemInteractionImpl delete(path) { + file(path)?.delete() + return this + } + + /** + * Allows Gradle style simple copy specs + * + * @param callable The callable + * @return this + */ + @Override + FileSystemInteractionImpl copy(@DelegatesTo(FileSystemInteraction.CopySpec) Closure callable) { + FileSystemInteraction.CopySpec spec = new FileSystemInteraction.CopySpec() + callable.delegate = spec + callable.call() + if(spec.from && spec.into) { + if(spec.from instanceof Iterable) { + copyAll((Iterable)spec.from, spec.into) + } + else { + copy(spec.from, spec.into) + } + } + return this + } + /** + * Copies a resource to the target destination + * + * @param path The path + * @param destination The destination + */ + @Override + FileSystemInteractionImpl copy(path, destination) { + def from = resource(path) + def to = file(destination) + copy(from, to) + return this + } + + /** + * Copies resources to the target destination + * + * @param path The path + * @param destination The destination + */ + @Override + FileSystemInteractionImpl copyAll(Iterable resources, destination) { + mkdir(destination) + for(path in resources) { + def from = resource(path) + def to = file(destination) + copy(from, to) + } + return this + } + + /** + * Copy a Resource from the given location to the given directory or location + * + * @param from The resource to copy + * @param to The location to copy to + * @return The {@FileSystemInteraction} instance + */ + @Override + FileSystemInteractionImpl copy(Resource from, File to) { + if(!to?.exists()) mkdir(to) + if (from && to) { + if (to.isDirectory()) { + mkdir(to) + to = new File(to, from.filename) + } + SpringIOUtils.copy(from, to) + GrailsConsole.instance.addStatus("Copied ${from.filename} to location ${to.canonicalPath}") + } + return this + } + + /** + * Obtain a file for the given path + * + * @param path The path + * @return The file + */ + @Override + File file(Object path) { + if(path instanceof File) return (File)path + else if(path instanceof Resource) return ((Resource)path).file + else { + def baseDir = executionContext.baseDir + new File(baseDir ?: new File("."), path.toString()) + } + } + + /** + * @return The target build directory + */ + @Override + File getBuildDir() { + BuildSettings.TARGET_DIR + } + + /** + * @return The directory where resources are processed to + */ + @Override + File getResourcesDir() { + BuildSettings.RESOURCES_DIR + } + + /** + * @return The directory where classes are compiled to + */ + @Override + File getClassesDir() { + BuildSettings.CLASSES_DIR + } + + /** + * Finds a source file for the given class name + * @param className The class name + * @return The source resource + */ + @Override + Resource source(String className) { + resourceLocator.findResourceForClassName(className) + } + + /** + * The class name of the given resource + * + * @param resource The resource + * @return The class name + */ + String className(Resource resource) { + GrailsResourceUtils.getClassName(resource) + } + + /** + * Obtain a resource for the given path + * @param path The path + * @return The resource + */ + @Override + Resource resource(Object path) { + if(!path) return null + if(path instanceof Resource) return (Resource)path + def f = file(path) + if(f?.exists() && f.isFile()) { + return new FileSystemResource(f) + } + else { + def pathStr = path.toString() + def resource = resourceLoader.getResource(pathStr) + if(resource.exists()) { + return resource + } + else { + def allResources = resources(pathStr) + if(allResources) { + return allResources[0] + } + else { + return resource + } + } + } + } + + /** + * Obtain resources for the given pattern + * + * @param pattern The pattern + * @return The resources + */ + @Override + Collection resources(String pattern) { + try { + return resourcePatternResolver.getResources(pattern).toList() + } catch (e) { + return [] + } + } + + /** + * Obtain the path of the resource relative to the current project + * + * @param path The path to inspect + * @return The relative path + */ + @Override + String projectPath(Object path) { + def file = file(path) + if(file) { + def basePath = executionContext.baseDir.canonicalPath + return (file.canonicalPath - basePath).substring(1) + } + return "" + } + + /** + * Get files matching the given pattern + * + * @param pattern The pattern + * @return the files + */ + @Override + Collection files(String pattern) { + resources(pattern).collect() { Resource res -> res.file } + } + + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/GradleDependency.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/GradleDependency.groovy new file mode 100644 index 00000000000..6b5a1987035 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/GradleDependency.groovy @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2020 original 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 org.grails.cli.profile.commands.io + +import org.eclipse.aether.graph.Dependency + +class GradleDependency { + + static final Map SCOPE_MAP = [ + compile: 'implementation', + runtime: 'runtimeOnly', + testRuntime: 'testRuntimeOnly', + testCompile: 'testImplementation', +// provided: 'developmentOnly' + ] + + private String scope + private String dependency + + GradleDependency(String scope, String dependency) { + this.scope = scope + this.dependency = dependency + } + + GradleDependency(Dependency dependency) { +// this(dependency.scope, dependency) + this(SCOPE_MAP.get(dependency.scope) ?: dependency.scope, dependency) + } + + GradleDependency(String scope, Dependency dependency) { + this.scope = scope + def artifact = dependency.artifact + def v = artifact.version.replace('BOM', '') + StringBuilder artifactString = new StringBuilder() + if (dependency.exclusions != null && !dependency.exclusions.empty) { + artifactString.append('(') + } else { + artifactString.append(' ') + } + artifactString.append('"') + artifactString.append(artifact.groupId) + artifactString.append(':').append(artifact.artifactId) + if (v) { + artifactString.append(':').append(v) + } + artifactString.append('"') + + def ln = System.getProperty("line.separator") + + if (dependency.exclusions != null && !dependency.exclusions.empty) { + artifactString.append(") {").append(ln) + for (e in dependency.exclusions) { + artifactString.append(" ") + .append("exclude") + + artifactString.append(" group: ").append('"').append(e.groupId).append('",') + artifactString.append(" module: ").append('"').append(e.artifactId).append('"') + + artifactString.append(ln) + } + artifactString.append("}") + } + this.dependency = artifactString.toString() + } + + String toString(int spaces) { + (scope + dependency).replaceAll('(?m)^', ' ' * spaces) + } + + String getScope() { + scope + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/ServerInteraction.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/ServerInteraction.groovy new file mode 100644 index 00000000000..ab8a50828a6 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/io/ServerInteraction.groovy @@ -0,0 +1,61 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile.commands.io + +import groovy.transform.CompileStatic + + +/** + * Methods to aid interacting with the server from the CLI + * + * @author Graeme Rocher + * @since 3.0.3 + */ +@CompileStatic +trait ServerInteraction { + + /** + * Waits for the server to startup + * + * @param host The host + * @param port The port + */ + void waitForStartup(String host = "localhost", int port = 8080) { + while(!isServerAvailable(host, port)) { + sleep 100 + } + try { + new URL("http://${host ?: 'localhost'}:${port ?: 8080}/is-tomcat-running").text + } catch(ignored) { + // ignore + } + } + + /** + * Returns true if the server is available + * + * @param host The host + * @param port The port + */ + boolean isServerAvailable(String host = "localhost", int port = 8080) { + try { + new Socket(host, port) + return true + } catch (e) { + return false + } + } +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/script/GroovyScriptCommand.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/script/GroovyScriptCommand.groovy new file mode 100644 index 00000000000..c39b3d22030 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/script/GroovyScriptCommand.groovy @@ -0,0 +1,195 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.script + +import grails.build.logging.ConsoleLogger +import grails.build.logging.GrailsConsole +import grails.codegen.model.ModelBuilder +import grails.util.Environment +import grails.util.GrailsNameUtils +import groovy.ant.AntBuilder +import groovy.transform.CompileStatic +import org.grails.build.logging.GrailsConsoleAntBuilder +import org.grails.cli.GrailsCli +import org.grails.cli.boot.SpringInvoker +import org.grails.cli.gradle.GradleInvoker +import org.grails.cli.profile.CommandArgument +import org.grails.cli.profile.CommandDescription +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileCommand +import org.grails.cli.profile.ProfileRepository +import org.grails.cli.profile.ProfileRepositoryAware +import org.grails.cli.profile.commands.events.CommandEvents +import org.grails.cli.profile.commands.io.FileSystemInteraction +import org.grails.cli.profile.commands.io.FileSystemInteractionImpl +import org.grails.cli.profile.commands.io.ServerInteraction +import org.grails.cli.profile.commands.templates.TemplateRenderer +import org.grails.cli.profile.commands.templates.TemplateRendererImpl + +/** + * A base class for Groovy scripts that implement commands + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +abstract class GroovyScriptCommand extends Script implements ProfileCommand, ProfileRepositoryAware, ConsoleLogger, ModelBuilder, FileSystemInteraction, TemplateRenderer, CommandEvents, ServerInteraction { + + Profile profile + ProfileRepository profileRepository + String name = getClass().name.contains('-') ? getClass().name : GrailsNameUtils.getScriptName(getClass().name) + CommandDescription description = new CommandDescription(name) + @Delegate ExecutionContext executionContext + @Delegate TemplateRenderer templateRenderer + @Delegate ConsoleLogger consoleLogger = GrailsConsole.getInstance() + @Delegate FileSystemInteraction fileSystemInteraction + + /** + * Allows invoking of Gradle commands + */ + GradleInvoker gradle + /** + * Allows invoking of Spring Boot's CLI + */ + SpringInvoker spring = SpringInvoker.getInstance() + /** + * Access to Ant via AntBuilder + */ + AntBuilder ant = new GrailsConsoleAntBuilder() + + /** + * The location of the user.home directory + */ + String userHome = System.getProperty('user.home') + /** + * The version of Grails being used + */ + String grailsVersion = getClass().getPackage()?.getImplementationVersion() + + /** + * Provides a description for the command + * + * @param desc The description + * @param usage The usage information + */ + void description(String desc, String usage) { + // ignore, just a stub for documentation purposes, populated by CommandScriptTransform + } + + /** + * Provides a description for the command + * + * @param desc The description + * @param usage The usage information + */ + void description(String desc, Closure detail) { + // ignore, just a stub for documentation purposes, populated by CommandScriptTransform + } + + /** + * Obtains details of the given flag if it has been set by the user + * + * @param name The name of the flag + * @return The flag information, or null if it isn't set by the user + */ + def flag(String name) { + if(commandLine.hasOption(name)) { + return commandLine.optionValue(name) + } + else { + def value = commandLine?.undeclaredOptions?.get(name) + return value ?: null + } + } + + /** + * @return The undeclared command line arguments + */ + Map getArgsMap() { + executionContext.commandLine.undeclaredOptions + } + + /** + * @return The arguments as a list of strings + */ + List getArgs() { + executionContext.commandLine.remainingArgs + } + + /** + * @return The name of the current Grails environment + */ + String getGrailsEnv() { Environment.current.name } + + /** + * @return The {@link GrailsConsole} instance + */ + GrailsConsole getGrailsConsole() { executionContext.console } + + /** + * Implementation of the handle method that runs the script + * + * @param executionContext The ExecutionContext + * @return True if the script succeeds, false otherwise + */ + @Override + boolean handle(ExecutionContext executionContext) { + setExecutionContext(executionContext) + notify("${name}Start", executionContext) + def result = run() + notify("${name}End", executionContext) + if(result instanceof Boolean) { + return ((Boolean)result) + } + return true + } + + /** + * Method missing handler used to invoke other commands from a command script + * + * @param name The name of the command as a method name (for example 'run-app' would be runApp()) + * @param args The arguments to the command + */ + def methodMissing(String name, args) { + Object[] argsArray = (Object[])args + def commandName = GrailsNameUtils.getScriptName(name) + def context = executionContext + if(profile?.hasCommand(context, commandName )) { + def commandLine = context.commandLine + def newArgs = [commandName] + newArgs.addAll argsArray.collect() { it.toString() } + def newContext = new GrailsCli.ExecutionContextImpl(commandLine.parseNew(newArgs as String[]), context) + return profile.handleCommand(newContext) + } + else { + throw new MissingMethodException(name, getClass(), argsArray) + } + } + + public void setExecutionContext(ExecutionContext executionContext) { + this.executionContext = executionContext + this.consoleLogger = executionContext.console + this.templateRenderer = new TemplateRendererImpl(executionContext, profile, profileRepository) + this.fileSystemInteraction = new FileSystemInteractionImpl(executionContext) + this.gradle = new GradleInvoker(executionContext) + setDefaultPackage( executionContext.navigateConfig('grails', 'codegen', 'defaultPackage') ) + } + + ExecutionContext getExecutionContext() { + return executionContext + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/script/GroovyScriptCommandTransform.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/script/GroovyScriptCommandTransform.groovy new file mode 100644 index 00000000000..df7ccd300da --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/script/GroovyScriptCommandTransform.groovy @@ -0,0 +1,138 @@ +package org.grails.cli.profile.commands.script +import groovy.transform.CompileStatic +import org.codehaus.groovy.ast.* +import org.codehaus.groovy.ast.expr.* +import org.codehaus.groovy.ast.stmt.BlockStatement +import org.codehaus.groovy.ast.stmt.ExpressionStatement +import org.codehaus.groovy.ast.stmt.Statement +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.transform.ASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.grails.cli.profile.CommandDescription + +import java.lang.reflect.Modifier +/* + * Copyright 2014 original 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. + */ + +/** + * Transformation applied to command scripts + * + * @author Graeme Rocher + * @since 3.0 + */ +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +@CompileStatic +class GroovyScriptCommandTransform implements ASTTransformation { + @Override + void visit(ASTNode[] nodes, SourceUnit source) { + for(ClassNode cNode in source.AST.classes) { + if(cNode.superClass.name == "org.grails.cli.profile.commands.script.GroovyScriptCommand") + new CommandScriptTransformer(source, cNode).visitClass(cNode) + } + } + + static class CommandScriptTransformer extends ClassCodeVisitorSupport { + SourceUnit sourceUnit + ClassNode classNode + + CommandScriptTransformer(SourceUnit sourceUnit, ClassNode classNode) { + this.sourceUnit = sourceUnit + this.classNode = classNode + } + + @Override + void visitMethodCallExpression(MethodCallExpression call) { + if(call.methodAsString == 'description' && (call.arguments instanceof ArgumentListExpression)) { + def constructorBody = new BlockStatement() + def defaultConstructor = getDefaultConstructor(classNode) + if(defaultConstructor == null) { + defaultConstructor = new ConstructorNode(Modifier.PUBLIC, constructorBody) + classNode.addConstructor(defaultConstructor) + } + else { + constructorBody.addStatement(defaultConstructor.getCode()) + defaultConstructor.setCode(constructorBody) + } + + + ArgumentListExpression existing = (ArgumentListExpression) call.arguments + + def arguments = existing.expressions + if(arguments.size() == 2) { + def constructorArgs = new ArgumentListExpression() + constructorArgs.addExpression(new VariableExpression("name")) + def secondArg = arguments.get(1) + Expression constructDescription = new ConstructorCallExpression(ClassHelper.make(CommandDescription), constructorArgs) + if(secondArg instanceof ClosureExpression) { + constructorArgs.addExpression(arguments.get(0)) + ClosureExpression closureExpression = (ClosureExpression)secondArg + def body = closureExpression.code + if(body instanceof BlockStatement) { + BlockStatement bodyBlock = (BlockStatement)body + for(Statement s in bodyBlock.statements) { + if(s instanceof ExpressionStatement) { + ExpressionStatement es = (ExpressionStatement)s + + def expr = es.expression + if(expr instanceof MethodCallExpression) { + MethodCallExpression mce = (MethodCallExpression)expr + def methodCallArgs = mce.getArguments() + + switch(mce.methodAsString) { + case 'usage': + if(methodCallArgs instanceof ArgumentListExpression) { + constructorArgs.addExpression( ((ArgumentListExpression)methodCallArgs).getExpression(0)) + } + + break + default: + constructDescription = new MethodCallExpression(constructDescription, mce.methodAsString, methodCallArgs) + break + + } + } + } + } + } + + } + else { + constructorArgs.expressions.addAll(arguments) + } + + def assignDescription = new MethodCallExpression(new VariableExpression("this"),"setDescription", constructDescription) + constructorBody.addStatement(new ExpressionStatement(assignDescription)) + } + + + } + else { + super.visitMethodCallExpression(call) + } + } + } + + public static ConstructorNode getDefaultConstructor(ClassNode classNode) { + for (ConstructorNode cons in classNode.getDeclaredConstructors()) { + if (cons.getParameters().length == 0) { + return cons + } + } + return null + } + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/SimpleTemplate.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/SimpleTemplate.groovy new file mode 100644 index 00000000000..3f8b136d564 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/SimpleTemplate.groovy @@ -0,0 +1,18 @@ +package org.grails.cli.profile.commands.templates + +import groovy.transform.CompileStatic +import groovy.transform.Immutable + +@CompileStatic +@Immutable +class SimpleTemplate { + String template + + public String render(Map variables) { + String result = template?:'' + variables.each { k, v -> + result = result.replace("@${k}@".toString(), v?:'') + } + result + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/TemplateException.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/TemplateException.groovy new file mode 100644 index 00000000000..aa439817c67 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/TemplateException.groovy @@ -0,0 +1,29 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.templates + +import groovy.transform.InheritConstructors + + +/** + * Exception thrown when an error in template rendering occurs + * + * @author Graeme Rocher + * @since 3.0 + */ +@InheritConstructors +class TemplateException extends RuntimeException { +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/TemplateRenderer.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/TemplateRenderer.groovy new file mode 100644 index 00000000000..2addb6da86c --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/TemplateRenderer.groovy @@ -0,0 +1,171 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.templates + +import grails.codegen.model.Model +import groovy.transform.CompileDynamic +import org.grails.io.support.Resource + + + +/** + * API for locating and rendering templates in the code generation layer + * + * @author Graeme Rocher + * @since 3.0 + */ +interface TemplateRenderer { + + /** + * Render with the given named arguments + * + * @param namedArguments The named arguments are 'template', 'destination' and 'model' + */ + @CompileDynamic + void render(Map namedArguments) + /** + * Render the given template to the give destination for the given model + * + * @param template The contents template + * @param destination The destination + * @param model The model + */ + void render(CharSequence template, File destination, Model model) + + /** + * Render the given template to the given destination + * + * @param template The contents of the template + * @param destination The destination + * @param model The model + */ + void render(CharSequence template, File destination ) + + /** + * Render the given template to the given destination + * + * @param template The contents of the template + * @param destination The destination + * @param model The model + */ + void render(CharSequence template, File destination, Map model ) + /** + * Render the given template to the given destination + * + * @param template The contents of the template + * @param destination The destination + * @param model The model + */ + void render(CharSequence template, File destination, Map model, boolean overwrite) + + /** + * Render the given template to the give destination for the given model + * + * @param template The template + * @param destination The destination + * @param model The model + */ + void render(File template, File destination, Model model) + + /** + * Render the given template to the given destination + * + * @param template The template + * @param destination The destination + * @param model The model + */ + void render(File template, File destination) + + /** + * Render the given template to the given destination + * + * @param template The template + * @param destination The destination + * @param model The model + */ + void render(File template, File destination, Map model ) + + /** + * Render the given template to the given destination + * + * @param template The template + * @param destination The destination + * @param model The model + */ + void render(File template, File destination, Map model , boolean overwrite) + + /** + * Render the given template to the give destination for the given model + * + * @param template The contents template + * @param destination The destination + * @param model The model + */ + void render(Resource template, File destination, Model model) + + + /** + * Render the given template to the give destination for the given model + * + * @param template The contents template + * @param destination The destination + * @param model The model + */ + void render(Resource template, File destination, Model model, boolean overwrite) + + /** + * Render the given template to the given destination + * + * @param template The template + * @param destination The destination + * @param model The model + */ + void render(Resource template, File destination) + + /** + * Render the given template to the given destination + * + * @param template The template + * @param destination The destination + * @param model The model + */ + void render(Resource template, File destination, Map model) + + /** + * Render the given template to the given destination + * + * @param template The template + * @param destination The destination + * @param model The model + */ + void render(Resource template, File destination, Map model, boolean overwrite) + + /** + * Find templates matching the given pattern + * + * @param pattern The pattern + * @return The available templates + */ + Iterable templates(String pattern) + + /** + * Find a template at the given location + * + * @param location The location + * @return The resource or null if it doesn't exist + */ + Resource template(Object location) +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/TemplateRendererImpl.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/TemplateRendererImpl.groovy new file mode 100644 index 00000000000..7f74d5fabd9 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/commands/templates/TemplateRendererImpl.groovy @@ -0,0 +1,276 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands.templates + +import grails.codegen.model.Model +import groovy.text.GStringTemplateEngine +import groovy.text.Template +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.grails.cli.interactive.completers.ClassNameCompleter +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileRepository +import org.grails.cli.profile.ProfileRepositoryAware +import org.grails.cli.profile.commands.io.FileSystemInteraction +import org.grails.cli.profile.commands.io.FileSystemInteractionImpl +import org.grails.io.support.DefaultResourceLoader +import org.grails.io.support.Resource +import org.grails.io.support.ResourceLoader + + +/** + * Interface for classes that can render templates + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class TemplateRendererImpl implements TemplateRenderer, ProfileRepositoryAware { + + ExecutionContext executionContext + Profile profile + ProfileRepository profileRepository + @Delegate FileSystemInteraction fileSystemInteraction + private Map templateCache = [:] + + TemplateRendererImpl(ExecutionContext executionContext, ProfileRepository profileRepository, ResourceLoader resourceLoader = new DefaultResourceLoader()) { + this.executionContext = executionContext + this.profileRepository = profileRepository + this.profile = profile + this.fileSystemInteraction = new FileSystemInteractionImpl(executionContext, resourceLoader) + } + + TemplateRendererImpl(ExecutionContext executionContext, Profile profile, ProfileRepository profileRepository, ResourceLoader resourceLoader = new DefaultResourceLoader()) { + this.executionContext = executionContext + this.profile = profile + this.profileRepository = profileRepository + this.fileSystemInteraction = new FileSystemInteractionImpl(executionContext, resourceLoader) + } + + /** + * Render with the given named arguments + * + * @param namedArguments The named arguments are 'template', 'destination' and 'model' + */ + @Override + @CompileDynamic + void render(Map namedArguments) { + if(namedArguments?.template && namedArguments?.destination) { + def templateArg = namedArguments.template + def template = templateArg instanceof Resource ? templateArg : template(templateArg) + boolean overwrite = namedArguments.overwrite as Boolean ?: false + render template, file(namedArguments.destination), namedArguments.model ?: [:], overwrite + } + } + + /** + * Render the given template to the give destination for the given model + * + * @param template The contents template + * @param destination The destination + * @param model The model + */ + @Override + void render(CharSequence template, File destination, Model model) { + render(template, destination, model.asMap()) + } + + /** + * Render the given template to the given destination + * + * @param template The contents of the template + * @param destination The destination + * @param model The model + */ + void render(CharSequence template, File destination, Map model = Collections.emptyMap(), boolean overwrite = false) { + if(template && destination) { + if(destination.exists() && !overwrite) { + executionContext.console.warn("Destination file ${projectPath( destination )} already exists, skipping...") + } + else { + def templateEngine = new GStringTemplateEngine() + try { + def t = templateEngine.createTemplate(template.toString()) + writeTemplateToDestination(t, model, destination) + } catch (e) { + destination.delete() + throw new TemplateException("Error rendering template to destination ${projectPath( destination )}: ${e.message}", e) + } + } + } + } + + + + /** + * Render the given template to the give destination for the given model + * + * @param template The template + * @param destination The destination + * @param model The model + */ + void render(File template, File destination, Model model) { + render(template, destination, model.asMap()) + } + /** + * Render the given template to the given destination + * + * @param template The template + * @param destination The destination + * @param model The model + */ + void render(File template, File destination, Map model = Collections.emptyMap(), boolean overwrite = false) { + if(template && destination) { + if(destination.exists() && !overwrite) { + executionContext.console.warn("Destination file ${projectPath( destination )} already exists, skipping...") + } + else { + Template t = templateCache[template.absolutePath] + if(t == null) { + try { + def templateEngine = new GStringTemplateEngine() + t = templateEngine.createTemplate(template) + } catch (e) { + throw new TemplateException("Error rendering template [$template] to destination ${projectPath( destination )}: ${e.message}", e) + } + } + try { + writeTemplateToDestination(t, model, destination) + executionContext.console.addStatus("Rendered template ${template.name} to destination ${projectPath( destination )}") + } catch (Throwable e) { + destination.delete() + throw new TemplateException("Error rendering template [$template] to destination ${projectPath( destination )}: ${e.message}", e) + } + } + } + } + + /** + * Render the given template to the give destination for the given model + * + * @param template The contents template + * @param destination The destination + * @param model The model + */ + void render(Resource template, File destination, Model model, boolean overwrite = false) { + render(template, destination, model.asMap(), overwrite) + } + /** + * Render the given template to the given destination + * + * @param template The template + * @param destination The destination + * @param model The model + */ + void render(Resource template, File destination, Map model = Collections.emptyMap(), boolean overwrite = false) { + if(template && destination) { + if(destination.exists() && !overwrite) { + executionContext.console.warn("Destination file ${projectPath( destination )} already exists, skipping...") + } + else if(!template?.exists()) { + throw new TemplateException("Template [$template.filename] not found.") + } + else { + Template t = templateCache[template.filename] + if(t == null) { + + try { + def templateEngine = new GStringTemplateEngine() + def reader = new InputStreamReader(template.inputStream, "UTF-8") + try { + t = templateEngine.createTemplate(reader) + } finally { + try { + reader.close() + } catch (e) { + // ignore + } + } + } catch (e) { + throw new TemplateException("Error rendering template [$template.filename] to destination ${projectPath( destination )}: ${e.message}", e) + } + } + if(t != null) { + try { + writeTemplateToDestination(t, model, destination) + executionContext.console.addStatus("Rendered template ${template.filename} to destination ${projectPath( destination )}") + } catch (Throwable e) { + destination.delete() + throw new TemplateException("Error rendering template [$template.filename] to destination ${projectPath( destination )}: ${e.message}", e) + } + } + } + } + } + + /** + * Find templates matching the given pattern + * + * @param pattern The pattern + * @return The available templates + */ + Iterable templates(String pattern) { + Collection resList = [] + resList.addAll( resources(pattern) ) + resList.addAll( resources("classpath*:META-INF/templates/$pattern")) + return resList.unique() + } + + /** + * Find a template at the given location + * + * @param location The location + * @return The resource or null if it doesn't exist + */ + Resource template(Object location) { + Resource f = resource(file("src/main/templates/$location")) + if(!f?.exists()) { + if( file('profile.yml').exists() ) { + f = resource( file("templates/$location") ) + if(f.exists()) { + return f + } + } + if(profile) { + def path = location.toString() + f = profile.getTemplate(path) + if(!f.exists()) { + def allProfiles = profileRepository.getProfileAndDependencies(profile) + for(parent in allProfiles) { + f = parent.getTemplate(path) + if(f.exists()) break + } + } + } + if(!f?.exists()) { + return resource("classpath*:META-INF/templates/" + location) + } + } + return resource(f) + } + + + private static void writeTemplateToDestination(Template template, Map model, File destination) { + destination.parentFile.mkdirs() + destination.withWriter("UTF-8") { Writer w -> + template.make(model).writeTo(w) + w.flush() + } + + ClassNameCompleter.refreshAll() + } +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/repository/AbstractJarProfileRepository.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/repository/AbstractJarProfileRepository.groovy new file mode 100644 index 00000000000..0b644b59161 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/repository/AbstractJarProfileRepository.groovy @@ -0,0 +1,142 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile.repository + +import groovy.transform.CompileStatic +import org.eclipse.aether.artifact.Artifact +import org.eclipse.aether.artifact.DefaultArtifact +import org.grails.cli.GrailsCli +import org.grails.cli.profile.AbstractProfile +import org.grails.cli.profile.Command +import org.grails.cli.profile.Profile +import org.grails.cli.profile.ProfileRepository +import org.grails.cli.profile.ProjectContext +import org.grails.cli.profile.ProjectContextAware +import org.grails.io.support.ClassPathResource +import org.grails.io.support.Resource + +/** + * A repository that loads profiles from JAR files + * + * @author Graeme Rocher + * @since 3.1 + */ +@CompileStatic +abstract class AbstractJarProfileRepository implements ProfileRepository { + + protected final List allProfiles = [] + protected final Map profilesByName = [:] + protected static final String DEFAULT_PROFILE_GROUPID = "org.grails.profiles" + + private Set registeredUrls = [] + + @Override + Profile getProfile(String profileName) { + return profilesByName[profileName] + } + + @Override + Profile getProfile(String profileName, Boolean parentProfile) { + return getProfile(profileName) + } + + List getAllProfiles() { + return allProfiles + } + + @Override + Resource getProfileDirectory(String profile) { + return getProfile(profile)?.profileDir + } + + @Override + List getProfileAndDependencies(Profile profile) { + List sortedProfiles = [] + Set visitedProfiles = [] as Set + visitTopologicalSort(profile, sortedProfiles, visitedProfiles) + return sortedProfiles + } + + Artifact getProfileArtifact(String profileName) { + if (profileName.contains(':')) { + return new DefaultArtifact(profileName) + } + + String groupId = DEFAULT_PROFILE_GROUPID + String version = null + + Map defaultValues = GrailsCli.getSetting("grails.profiles", Map, [:]) + defaultValues.remove("repositories") + def data = defaultValues.get(profileName) + if(data instanceof Map) { + groupId = data.get("groupId") + version = data.get("version") + } + + return new DefaultArtifact(groupId, profileName, null, version) + } + + protected void registerProfile(URL url, ClassLoader parent) { + if(registeredUrls.contains(url)) return + + def classLoader = new URLClassLoader([url] as URL[], parent) + def profileYml = classLoader.getResource("META-INF/grails-profile/profile.yml") + if (profileYml != null) { + registeredUrls.add(url) + def profile = new JarProfile(this, new ClassPathResource("META-INF/grails-profile/", classLoader), classLoader) + profile.profileRepository = this + allProfiles.add profile + profilesByName[profile.name] = profile + } + } + private void visitTopologicalSort(Profile profile, List sortedProfiles, Set visitedProfiles) { + if(profile != null && !visitedProfiles.contains(profile)) { + visitedProfiles.add(profile) + profile.getExtends().each { Profile dependentProfile -> + visitTopologicalSort(dependentProfile, sortedProfiles, visitedProfiles); + } + sortedProfiles.add(profile) + } + } + + static class JarProfile extends AbstractProfile { + + JarProfile(ProfileRepository repository, Resource profileDir, ClassLoader classLoader) { + super(profileDir,classLoader) + this.profileRepository = repository + initialize() + } + + @Override + String getName() { + super.name + } + + @Override + Iterable getCommands(ProjectContext context) { + super.getCommands(context) + for(cmd in internalCommands) { + if(cmd instanceof ProjectContextAware) { + ((ProjectContextAware)cmd).setProjectContext(context) + } + commandsByName[cmd.name] = cmd + } + + return commandsByName.values() + } + } + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/repository/GrailsAetherGrapeEngineFactory.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/repository/GrailsAetherGrapeEngineFactory.groovy new file mode 100644 index 00000000000..f9c6d1f1b92 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/repository/GrailsAetherGrapeEngineFactory.groovy @@ -0,0 +1,91 @@ +package org.grails.cli.profile.repository + +import groovy.transform.CompileStatic +import org.apache.maven.repository.internal.MavenRepositorySystemUtils +import org.eclipse.aether.DefaultRepositorySystemSession +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory +import org.eclipse.aether.impl.DefaultServiceLocator +import org.eclipse.aether.internal.impl.DefaultRepositorySystem +import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.repository.RepositoryPolicy +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory +import org.eclipse.aether.spi.connector.transport.TransporterFactory +import org.eclipse.aether.spi.locator.ServiceLocator +import org.eclipse.aether.transport.file.FileTransporterFactory +import org.eclipse.aether.transport.http.HttpTransporterFactory +import org.eclipse.aether.util.repository.AuthenticationBuilder +import org.springframework.boot.cli.compiler.grape.AetherGrapeEngine +import org.springframework.boot.cli.compiler.grape.DefaultRepositorySystemSessionAutoConfiguration +import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext +import org.springframework.boot.cli.compiler.grape.RepositorySystemSessionAutoConfiguration + +/** + * Creates aether engine to resolve profiles. Mostly copied from {@link AetherGrapeEngine}. + * Created to support repositories with authentication. + * + * @author James Kleeh + * @since 3.2 + */ +@CompileStatic +class GrailsAetherGrapeEngineFactory { + + static AetherGrapeEngine create(GroovyClassLoader classLoader, + List repositoryConfigurations, + DependencyResolutionContext dependencyResolutionContext) { + + RepositorySystem repositorySystem = createServiceLocator() + .getService(RepositorySystem.class) + + DefaultRepositorySystemSession repositorySystemSession = MavenRepositorySystemUtils + .newSession() + + ServiceLoader autoConfigurations = ServiceLoader + .load(RepositorySystemSessionAutoConfiguration.class) + + for (RepositorySystemSessionAutoConfiguration autoConfiguration : autoConfigurations) { + autoConfiguration.apply(repositorySystemSession, repositorySystem) + } + + new DefaultRepositorySystemSessionAutoConfiguration() + .apply(repositorySystemSession, repositorySystem) + + return new AetherGrapeEngine(classLoader, repositorySystem, + repositorySystemSession, createRepositories(repositoryConfigurations), + dependencyResolutionContext, false) + } + + private static ServiceLocator createServiceLocator() { + DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator() + locator.addService(RepositorySystem.class, DefaultRepositorySystem.class) + locator.addService(RepositoryConnectorFactory.class, + BasicRepositoryConnectorFactory.class) + locator.addService(TransporterFactory.class, HttpTransporterFactory.class) + locator.addService(TransporterFactory.class, FileTransporterFactory.class) + return locator + } + + private static List createRepositories( + List repositoryConfigurations) { + List repositories = new ArrayList( + repositoryConfigurations.size()) + for (GrailsRepositoryConfiguration repositoryConfiguration : repositoryConfigurations) { + RemoteRepository.Builder builder = new RemoteRepository.Builder( + repositoryConfiguration.getName(), "default", + repositoryConfiguration.getUri().toASCIIString()) + if (repositoryConfiguration.hasCredentials()) { + builder.authentication = new AuthenticationBuilder() + .addUsername(repositoryConfiguration.username) + .addPassword(repositoryConfiguration.password) + .build() + } + if (!repositoryConfiguration.getSnapshotsEnabled()) { + builder.setSnapshotPolicy( + new RepositoryPolicy(false, RepositoryPolicy.UPDATE_POLICY_NEVER, + RepositoryPolicy.CHECKSUM_POLICY_IGNORE)) + } + repositories.add(builder.build()) + } + return repositories + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/repository/GrailsRepositoryConfiguration.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/repository/GrailsRepositoryConfiguration.groovy new file mode 100644 index 00000000000..1710d7ec535 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/repository/GrailsRepositoryConfiguration.groovy @@ -0,0 +1,93 @@ +package org.grails.cli.profile.repository + +import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration + +/** + * The configuration of a repository. See {@link org.springframework.boot.cli.compiler.grape.RepositoryConfiguration} + * Created to support configuration with authentication + * + * @author James Kleeh + * @since 3.2 + */ +class GrailsRepositoryConfiguration { + + private static final int INITIAL_HASH = 7 + private static final int MULTIPLIER = 31 + + final String name + final URI uri + final boolean snapshotsEnabled + final String username + final String password + + /** + * Creates a new {@code GrailsRepositoryConfiguration} instance. + * @param name The name of the repository + * @param uri The uri of the repository + * @param snapshotsEnabled {@code true} if the repository should enable access to snapshots, {@code false} otherwise + */ + public GrailsRepositoryConfiguration(String name, URI uri, boolean snapshotsEnabled) { + this.name = name + this.uri = uri + this.snapshotsEnabled = snapshotsEnabled + } + + + /** + * Creates a new {@code GrailsRepositoryConfiguration} instance. + * @param name The name of the repository + * @param uri The uri of the repository + * @param snapshotsEnabled {@code true} if the repository should enable access to snapshots, {@code false} otherwise + * @param username The username needed to authenticate with the repository + * @param password The password needed to authenticate with the repository + */ + public GrailsRepositoryConfiguration(String name, URI uri, boolean snapshotsEnabled, String username, String password) { + this.name = name + this.uri = uri + this.snapshotsEnabled = snapshotsEnabled + this.username = username + this.password = password + } + + @Override + String toString() { + "GrailsRepositoryConfiguration [name=$name, uri=$uri, snapshotsEnabled=$snapshotsEnabled]" + } + + @Override + int hashCode() { + nullSafeHashCode(name) + } + + boolean hasCredentials() { + username && password + } + + @Override + boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + String name = null + if (obj instanceof RepositoryConfiguration) { + name = obj.name + } else if (obj instanceof GrailsRepositoryConfiguration) { + name = obj.name + } + this.name == name + } + + static int nullSafeHashCode(char[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (char element : array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/repository/MavenProfileRepository.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/repository/MavenProfileRepository.groovy new file mode 100644 index 00000000000..7342dc16d24 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/repository/MavenProfileRepository.groovy @@ -0,0 +1,158 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile.repository + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.eclipse.aether.artifact.Artifact +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.graph.Dependency +import org.grails.cli.boot.GrailsDependencyVersions +import org.grails.cli.profile.Profile +import org.springframework.boot.cli.compiler.grape.AetherGrapeEngine +import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext +import org.springframework.boot.cli.compiler.grape.DependencyResolutionFailedException + +/** + * Resolves profiles from a configured list of repositories using Aether + * + * @author Graeme Rocher + * @since 3.1 + */ +@CompileStatic +class MavenProfileRepository extends AbstractJarProfileRepository { + + public static final GrailsRepositoryConfiguration DEFAULT_REPO = new GrailsRepositoryConfiguration("grailsCentral", new URI("https://repo.grails.org/grails/core"), true) + + List repositoryConfigurations + AetherGrapeEngine grapeEngine + GroovyClassLoader classLoader + DependencyResolutionContext resolutionContext + GrailsDependencyVersions profileDependencyVersions + private boolean resolved = false + + MavenProfileRepository(List repositoryConfigurations) { + this.repositoryConfigurations = repositoryConfigurations + classLoader = new GroovyClassLoader(Thread.currentThread().contextClassLoader) + resolutionContext = new DependencyResolutionContext() + this.grapeEngine = GrailsAetherGrapeEngineFactory.create(classLoader, repositoryConfigurations, resolutionContext) + profileDependencyVersions = new GrailsDependencyVersions(grapeEngine) + resolutionContext.addDependencyManagement(profileDependencyVersions) + } + + MavenProfileRepository() { + this([DEFAULT_REPO]) + } + + @Override + Profile getProfile(String profileName, Boolean parentProfile) { + String profileShortName = profileName + if(profileName.contains(':')) { + def art = new DefaultArtifact(profileName) + profileShortName = art.artifactId + } + if (!profilesByName.containsKey(profileShortName)) { + if(parentProfile && profileDependencyVersions.find(DEFAULT_PROFILE_GROUPID, profileShortName)) { + return resolveProfile(profileShortName) + } else { + return resolveProfile(profileName) + } + } + return super.getProfile(profileShortName) + } + + @Override + Profile getProfile(String profileName) { + getProfile(profileName, false) + } + + protected Profile resolveProfile(String profileName) { + Artifact art = getProfileArtifact(profileName) + + try { + grapeEngine.grab(group: art.groupId, module: art.artifactId, version: art.version ?: null) + } catch (DependencyResolutionFailedException e ) { + + def localData = new File(System.getProperty("user.home"),"/.m2/repository/${art.groupId.replace('.','/')}/$art.artifactId/maven-metadata-local.xml") + if(localData.exists()) { + def currentVersion = parseCurrentVersion(localData) + def profileFile = new File(localData.parentFile, "$currentVersion/${art.artifactId}-${currentVersion}.jar") + if(profileFile.exists()) { + classLoader.addURL(profileFile.toURI().toURL()) + } + else { + throw e + } + } + else { + throw e + } + } + + processUrls() + return super.getProfile(art.artifactId) + } + + @CompileDynamic + protected String parseCurrentVersion(File localData) { + new XmlSlurper().parse(localData).versioning.versions.version[0].text() + } + + protected void processUrls() { + def urls = classLoader.getURLs() + for (URL url in urls) { + registerProfile(url, new URLClassLoader([url] as URL[], Thread.currentThread().contextClassLoader)) + } + } + + @Override + List getAllProfiles() { + if(!resolved) { + List profiles = [] + resolutionContext.managedDependencies.each { Dependency dep -> + if (dep.artifact.groupId == "org.grails.profiles") { + profiles.add([group: dep.artifact.groupId, module: dep.artifact.artifactId]) + } + } + profiles.sort { it.module } + + for (Map profile in profiles) { + grapeEngine.grab(profile) + } + + def localData = new File(System.getProperty("user.home"),"/.m2/repository/org/grails/profiles") + if(localData.exists()) { + localData.eachDir { File dir -> + if(!dir.name.startsWith('.')) { + def profileData = new File(dir, "/maven-metadata-local.xml") + if(profileData.exists()) { + def currentVersion = parseCurrentVersion(profileData) + def profileFile = new File(dir, "$currentVersion/${dir.name}-${currentVersion}.jar") + if(profileFile.exists()) { + classLoader.addURL(profileFile.toURI().toURL()) + } + } + } + } + } + + processUrls() + resolved = true + } + return super.getAllProfiles() + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/repository/StaticJarProfileRepository.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/repository/StaticJarProfileRepository.groovy new file mode 100644 index 00000000000..a9be3702b08 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/repository/StaticJarProfileRepository.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2015 original 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 org.grails.cli.profile.repository +import groovy.transform.CompileStatic +import org.eclipse.aether.artifact.DefaultArtifact +import org.grails.cli.profile.Profile + +/** + * A JAR file repository that resolves profiles from a static array of JAR file URLs + * + * @author Graeme Rocher + * @since 3.1 + */ +@CompileStatic +class StaticJarProfileRepository extends AbstractJarProfileRepository { + + + final URL[] urls + + StaticJarProfileRepository(ClassLoader parent, URL...urls) { + this.urls = urls + for(url in urls) { + registerProfile(url, parent) + } + } + + Profile getProfile(String profileName) { + def profile = super.getProfile(profileName) + if(profile == null && profileName.contains(':')) { + def art = new DefaultArtifact(profileName) + profile = super.getProfile(art.artifactId) + } + return profile + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/steps/DefaultStepFactory.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/DefaultStepFactory.groovy new file mode 100644 index 00000000000..7de75668003 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/DefaultStepFactory.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.steps + +import groovy.transform.CompileStatic +import org.grails.cli.profile.Command +import org.grails.cli.profile.ProfileCommand +import org.grails.cli.profile.Step + +/** + * Dynamic creation of {@link Step} instances + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class DefaultStepFactory implements StepFactory { + + Map> steps = [ + render: RenderStep, + gradle: GradleStep, + execute: ExecuteStep, + mkdir: MkdirStep + ] + + @Override + Step createStep(String name, Command command, Map parameters) { + if(command instanceof ProfileCommand) { + return steps[name]?.newInstance(command, parameters) + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/steps/ExecuteStep.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/ExecuteStep.groovy new file mode 100644 index 00000000000..a217843254b --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/ExecuteStep.groovy @@ -0,0 +1,67 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.steps + +import org.grails.cli.profile.AbstractStep +import org.grails.cli.profile.Command +import org.grails.cli.profile.CommandException +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.ProfileCommand + +/** + * A {@link org.grails.cli.profile.Step} that can execute another command + * + * @author Graeme Rocher + * @since 3.0 + */ +class ExecuteStep extends AbstractStep { + + public static final String NAME = "execute" + public static final String CLASS_NAME = "class" + + + Command target + + + ExecuteStep(ProfileCommand command, Map parameters) { + super(command, parameters) + + try { + String className = parameters.get(CLASS_NAME) + def cmd = className ? Class.forName(className, true, Thread.currentThread().contextClassLoader) + .newInstance() : null + if(cmd instanceof Command) { + if(cmd instanceof ProfileCommand) { + ((ProfileCommand)cmd).profile = command.profile + } + this.target = cmd + } + else { + throw new CommandException("Invalid command class [$className] specified") + } + } catch (Throwable e) { + throw new CommandException("Unable to create step for command [${command.name}] for parameters $parameters", e) + } + } + + @Override + String getName() { NAME } + + @Override + boolean handle(ExecutionContext context) { + return target.handle(context) + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/steps/GradleStep.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/GradleStep.groovy new file mode 100644 index 00000000000..42c1410690c --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/GradleStep.groovy @@ -0,0 +1,106 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.steps + +import groovy.transform.CompileStatic +import org.gradle.tooling.BuildException +import org.gradle.tooling.BuildLauncher +import org.grails.build.parsing.CommandLine +import org.grails.cli.gradle.GradleUtil +import org.grails.cli.profile.* +import org.grails.exceptions.ExceptionUtils + +/** + * A {@link org.grails.cli.profile.Step} that invokes Gradle + * + * @author Lari Hotari + * @author Graeme Rocher + * + * @since 3.0 + */ +@CompileStatic +class GradleStep extends AbstractStep { + protected static final Map GRADLE_ARGUMENT_ADAPTER = [ + 'plain-output' : '--console plain', + 'verbose' : '-d' + ] + protected List tasks = [] + protected String baseArguments = "" + protected boolean passArguments = true + + GradleStep(ProfileCommand command, Map parameters) { + super(command, parameters) + initialize() + } + + + @Override + String getName() { "gradle" } + + @Override + public boolean handle(ExecutionContext context) { + try { + GradleUtil.runBuildWithConsoleOutput(context) { BuildLauncher buildLauncher -> + buildLauncher.forTasks(tasks as String[]) + fillArguments(context, buildLauncher) + } + } catch (BuildException e) { + def cause = ExceptionUtils.getRootCause(e) + context.console.error("Gradle build terminated with error: ${cause.message}", cause) + return false + } + return true; + } + + protected void initialize() { + tasks = (List)parameters.tasks + baseArguments = parameters.baseArguments ?: '' + passArguments = Boolean.valueOf(parameters.passArguments?.toString() ?: 'true' ) + } + + protected BuildLauncher fillArguments(ExecutionContext context, BuildLauncher buildLauncher) { + def commandLine = context.commandLine + + List argList = baseArguments ? [baseArguments] : new ArrayList() + + for(Map.Entry entry in commandLine.undeclaredOptions) { + def flagName = entry.key + if(GRADLE_ARGUMENT_ADAPTER.containsKey(flagName)) { + argList.addAll( GRADLE_ARGUMENT_ADAPTER[flagName].split(/\s/) ) + continue + } + + def flag = command.description.getFlag(flagName) + if(flag) { + flagName = flag.target ?: flagName + } + argList << "-$flagName".toString() + + } + + if(passArguments) { + argList.addAll(commandLine.remainingArgs.collect() { String arg -> "-${arg}".toString() } ) + } + + + if(argList) { + + buildLauncher.withArguments(argList as String[]) + } + buildLauncher + } + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/steps/MkdirStep.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/MkdirStep.groovy new file mode 100644 index 00000000000..b94f8d0df30 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/MkdirStep.groovy @@ -0,0 +1,62 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.steps + +import groovy.transform.CompileStatic +import org.grails.cli.profile.AbstractStep +import org.grails.cli.profile.CommandException +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.ProfileCommand +import org.grails.cli.profile.support.ArtefactVariableResolver + +/** + * A step that makes a directory + * + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class MkdirStep extends AbstractStep { + + public static final String NAME = "mkdir" + + String location + + MkdirStep(ProfileCommand command, Map parameters) { + super(command, parameters) + location = parameters.location + if(!location) { + throw new CommandException("Location not specified for mkdir step") + } + } + + @Override + String getName() { NAME } + + @Override + boolean handle(ExecutionContext context) { + def args = context.commandLine.remainingArgs + if(args) { + def name = args[0] + def variableResolver = new ArtefactVariableResolver(name) + File destination = variableResolver.resolveFile(location, context) + return destination.mkdirs() + } + else { + return new File(context.baseDir, location).mkdirs() + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/steps/RenderStep.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/RenderStep.groovy new file mode 100644 index 00000000000..4443cf28226 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/RenderStep.groovy @@ -0,0 +1,136 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.steps + +import grails.build.logging.GrailsConsole +import grails.util.GrailsNameUtils +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors +import org.grails.build.parsing.CommandLine +import org.grails.cli.interactive.completers.ClassNameCompleter +import org.grails.cli.profile.AbstractStep +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.Profile +import org.grails.cli.profile.commands.templates.SimpleTemplate +import org.grails.cli.profile.support.ArtefactVariableResolver +import org.grails.io.support.Resource + +/** + * A {@link org.grails.cli.profile.Step} that renders a template + * + * @author Lari Hotari + * @author Graeme Rocher + * + * @since 3.0 + */ +@InheritConstructors +@CompileStatic +class RenderStep extends AbstractStep { + + public static final String NAME = "render" + public static final String TEMPLATES_DIR = "templates/" + + @Override + @CompileStatic + String getName() { NAME } + + @Override + public boolean handle(ExecutionContext context) { + def commandLine = context.getCommandLine() + String nameAsArgument = commandLine.getRemainingArgs()[0] + String artifactName + String artifactPackage + def nameAndPackage = resolveNameAndPackage(context, nameAsArgument) + artifactName = nameAndPackage[0] + artifactPackage = nameAndPackage[1] + def variableResolver = new ArtefactVariableResolver(artifactName, (String) parameters.convention,artifactPackage) + File destination = variableResolver.resolveFile(parameters.destination.toString(), context) + + try { + + String relPath = relativePath(context.baseDir, destination) + if(destination.exists() && !flag(commandLine, 'force')) { + context.console.error("${relPath} already exists.") + return false + } + + renderToDestination(destination, variableResolver.variables) + context.console.addStatus("Created $relPath") + + return true + } catch (Throwable e) { + GrailsConsole.instance.error("Failed to render template to destination: ${e.message}", e) + return false + } + } + + protected Resource searchTemplateDepthFirst(Profile profile, String template) { + if(template.startsWith(TEMPLATES_DIR)) { + return searchTemplateDepthFirst(profile, template.substring(TEMPLATES_DIR.length())) + } + Resource templateFile = profile.getTemplate(template) + if(templateFile.exists()) { + return templateFile + } else { + for(parent in profile.extends) { + templateFile = searchTemplateDepthFirst(parent, template) + if(templateFile) { + return templateFile + } + } + } + null + } + + protected void renderToDestination(File destination, Map variables) { + Profile profile = command.profile + Resource templateFile = searchTemplateDepthFirst(profile, parameters.template.toString()) + if(!templateFile) { + throw new IOException("cannot find template " + parameters.template) + } + destination.setText(new SimpleTemplate(templateFile.inputStream.getText("UTF-8")).render(variables), "UTF-8") + ClassNameCompleter.refreshAll() + } + + protected List resolveNameAndPackage(ExecutionContext context, String nameAsArgument) { + List parts = nameAsArgument.split(/\./) as List + + String artifactName + String artifactPackage + + if(parts.size() == 1) { + artifactName = parts[0] + artifactPackage = context.navigateConfig('grails', 'codegen', 'defaultPackage')?:'' + } else { + artifactName = parts[-1] + artifactPackage = parts[0..-2].join('.') + } + + [GrailsNameUtils.getClassName(artifactName), artifactPackage] + } + + protected String relativePath(File relbase, File file) { + def pathParts = [] + def currentFile = file + while (currentFile != null && currentFile != relbase) { + pathParts += currentFile.name + currentFile = currentFile.parentFile + } + pathParts.reverse().join('/') + } + + +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/steps/StepFactory.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/StepFactory.groovy new file mode 100644 index 00000000000..1957cae7e38 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/StepFactory.groovy @@ -0,0 +1,40 @@ +package org.grails.cli.profile.steps + +import org.grails.cli.profile.Command +import org.grails.cli.profile.Step + +/* + * Copyright 2014 original 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. + */ + +/** + * Creates steps + * + * @author Graeme Rocher + * @since 3.0 + */ +interface StepFactory { + + /** + * Creates a step for the given name, command and parameters + * + * @param name The name of the step + * @param command The command + * @param parameters The parameters + * + * @return The step instance + */ + Step createStep(String name, Command command, Map parameters) +} \ No newline at end of file diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/steps/StepRegistry.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/StepRegistry.groovy new file mode 100644 index 00000000000..84df7cad819 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/steps/StepRegistry.groovy @@ -0,0 +1,58 @@ +package org.grails.cli.profile.steps + +import groovy.transform.CompileStatic +import org.grails.cli.profile.Command +import org.grails.cli.profile.Step + + +/* + * Copyright 2014 original 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. + */ + +/** + * Registry of steps + * + * @author Graeme Rocher + * + * @since 3.0 + */ +@CompileStatic +class StepRegistry { + + private static Collection registeredStepFactories = [] + + static { + def stepFactories = ServiceLoader.load(StepFactory).iterator() + + while(stepFactories.hasNext()) { + StepFactory stepFactory = stepFactories.next() + registeredStepFactories << stepFactory + } + } + + /** + * Looks up a {@link Step} + * + * @param name The name of the {@link Step} + * @return A step or null if it doesn't exist for the given name + */ + static Step getStep(String name, Command command, Map parameters) { + if(!name) return null + for(StepFactory sf in registeredStepFactories) { + def step = sf.createStep(name, command, parameters) + if(step) return step + } + } +} diff --git a/grails-shell/src/main/groovy/org/grails/cli/profile/support/ArtefactVariableResolver.groovy b/grails-shell/src/main/groovy/org/grails/cli/profile/support/ArtefactVariableResolver.groovy new file mode 100644 index 00000000000..ac154e9f7f8 --- /dev/null +++ b/grails-shell/src/main/groovy/org/grails/cli/profile/support/ArtefactVariableResolver.groovy @@ -0,0 +1,75 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.support + +import grails.util.GrailsNameUtils +import groovy.transform.CompileStatic +import org.grails.cli.profile.ExecutionContext +import org.grails.cli.profile.commands.templates.SimpleTemplate + + + +/** + * @author Graeme Rocher + * @since 3.0 + */ +@CompileStatic +class ArtefactVariableResolver { + /** + * The artifact name and package + */ + String artifactPackage, artifactName + /** + * The suffix used as a convention for the file + */ + String convention + Map variables = [:] + + ArtefactVariableResolver(String artifactName, String artifactPackage = null) { + this(artifactName, null, artifactPackage) + } + + ArtefactVariableResolver(String artifactName, String convention, String artifactPackage) { + this.artifactPackage = artifactPackage + this.artifactName = artifactName + this.convention = convention + createVariables() + } + + Map createVariables() { + if(artifactPackage) { + variables['artifact.package.name'] = artifactPackage + variables['artifact.package.path'] = artifactPackage?.replace('.','/') + variables['artifact.package'] = "package $artifactPackage\n".toString() + } + if(convention && artifactName.endsWith(convention)) { + artifactName = artifactName.substring(0, artifactName.length() - convention.length()) + } + variables['artifact.name'] = artifactName + variables['artifact.propertyName'] = GrailsNameUtils.getPropertyName(artifactName) + return variables + } + + File resolveFile(String pathToResolve, ExecutionContext context) { + String destinationName = new SimpleTemplate(pathToResolve).render(variables) + File destination = new File(context.baseDir, destinationName).absoluteFile + + if(!destination.getParentFile().exists()) { + destination.getParentFile().mkdirs() + } + return destination + } +} diff --git a/grails-shell/src/main/resources/META-INF/services/org.grails.cli.profile.Command b/grails-shell/src/main/resources/META-INF/services/org.grails.cli.profile.Command new file mode 100644 index 00000000000..487d8176593 --- /dev/null +++ b/grails-shell/src/main/resources/META-INF/services/org.grails.cli.profile.Command @@ -0,0 +1,10 @@ +org.grails.cli.profile.commands.CreateAppCommand +org.grails.cli.profile.commands.CreatePluginCommand +org.grails.cli.profile.commands.CreateProfileCommand +org.grails.cli.profile.commands.OpenCommand +org.grails.cli.profile.commands.HelpCommand +org.grails.cli.profile.commands.ListProfilesCommand +org.grails.cli.profile.commands.ProfileInfoCommand +org.grails.cli.gradle.commands.GradleCommand + + diff --git a/grails-shell/src/main/resources/META-INF/services/org.grails.cli.profile.commands.factory.CommandFactory b/grails-shell/src/main/resources/META-INF/services/org.grails.cli.profile.commands.factory.CommandFactory new file mode 100644 index 00000000000..1467d32bfd9 --- /dev/null +++ b/grails-shell/src/main/resources/META-INF/services/org.grails.cli.profile.commands.factory.CommandFactory @@ -0,0 +1,4 @@ +org.grails.cli.profile.commands.factory.YamlCommandFactory +org.grails.cli.profile.commands.factory.GroovyScriptCommandFactory +org.grails.cli.profile.commands.factory.ServiceCommandFactory +org.grails.cli.profile.commands.factory.ApplicationContextCommandFactory diff --git a/grails-shell/src/main/resources/META-INF/services/org.grails.cli.profile.steps.StepFactory b/grails-shell/src/main/resources/META-INF/services/org.grails.cli.profile.steps.StepFactory new file mode 100644 index 00000000000..0138a747d76 --- /dev/null +++ b/grails-shell/src/main/resources/META-INF/services/org.grails.cli.profile.steps.StepFactory @@ -0,0 +1 @@ +org.grails.cli.profile.steps.DefaultStepFactory \ No newline at end of file diff --git a/grails-shell/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration b/grails-shell/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration new file mode 100644 index 00000000000..97c345017d9 --- /dev/null +++ b/grails-shell/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration @@ -0,0 +1,2 @@ +org.grails.cli.boot.GrailsApplicationCompilerAutoConfiguration +org.grails.cli.boot.GrailsTestCompilerAutoConfiguration \ No newline at end of file diff --git a/grails-shell/src/main/resources/unixStartScript.txt b/grails-shell/src/main/resources/unixStartScript.txt new file mode 100644 index 00000000000..2f725453e97 --- /dev/null +++ b/grails-shell/src/main/resources/unixStartScript.txt @@ -0,0 +1,167 @@ +#!/usr/bin/env sh + +############################################################################## +## +## ${applicationName} start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: \$0 may be a link +PRG="\$0" +# Need this for relative symlinks. +while [ -h "\$PRG" ] ; do + ls=`ls -ld "\$PRG"` + link=`expr "\$ls" : '.*-> \\(.*\\)\$'` + if expr "\$link" : '/.*' > /dev/null; then + PRG="\$link" + else + PRG=`dirname "\$PRG"`"/\$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"\$PRG\"`/${appHomeRelativePath}" >/dev/null +APP_HOME="`pwd -P`" +cd "\$SAVED" >/dev/null + +APP_NAME="${applicationName}" +APP_BASE_NAME=`basename "\$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and ${optsEnvironmentVar} to pass JVM options to this script. +DEFAULT_JVM_OPTS=${defaultJvmOpts} + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "\$*" +} + +die () { + echo + echo "\$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$classpath + +# Determine the Java command to use to start the JVM. +if [ -n "\$JAVA_HOME" ] ; then + if [ -x "\$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="\$JAVA_HOME/jre/sh/java" + else + JAVACMD="\$JAVA_HOME/bin/java" + fi + if [ ! -x "\$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: \$JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "\$cygwin" = "false" -a "\$darwin" = "false" -a "\$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ \$? -eq 0 ] ; then + if [ "\$MAX_FD" = "maximum" -o "\$MAX_FD" = "max" ] ; then + MAX_FD="\$MAX_FD_LIMIT" + fi + ulimit -n \$MAX_FD + if [ \$? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: \$MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: \$MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if \$darwin; then + GRADLE_OPTS="\$GRADLE_OPTS \\"-Xdock:name=\$APP_NAME\\" \\"-Xdock:icon=\$APP_HOME/media/mn.icns\\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if \$cygwin ; then + APP_HOME=`cygpath --path --mixed "\$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "\$CLASSPATH"` + JAVACMD=`cygpath --unix "\$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in \$ROOTDIRSRAW ; do + ROOTDIRS="\$ROOTDIRS\$SEP\$dir" + SEP="|" + done + OURCYGPATTERN="(^(\$ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "\$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="\$OURCYGPATTERN|(\$GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "\$@" ; do + CHECK=`echo "\$arg"|egrep -c "\$OURCYGPATTERN" -` + CHECK2=`echo "\$arg"|egrep -c "^-"` ### Determine if an option + + if [ \$CHECK -ne 0 ] && [ \$CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args\$i`=`cygpath --path --ignore --mixed "\$arg"` + else + eval `echo args\$i`="\"\$arg\"" + fi + i=\$((i+1)) + done + case \$i in + (0) set -- ;; + (1) set -- "\$args0" ;; + (2) set -- "\$args0" "\$args1" ;; + (3) set -- "\$args0" "\$args1" "\$args2" ;; + (4) set -- "\$args0" "\$args1" "\$args2" "\$args3" ;; + (5) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" ;; + (6) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" ;; + (7) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" "\$args6" ;; + (8) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" "\$args6" "\$args7" ;; + (9) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" "\$args6" "\$args7" "\$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\\\n "\$i" | sed "s/'/'\\\\\\\\''/g;1s/^/'/;\\\$s/\\\$/' \\\\\\\\/" ; done + echo " " +} +APP_ARGS=\$(save "\$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- \$DEFAULT_JVM_OPTS \$JAVA_OPTS \$${optsEnvironmentVar} <% if ( appNameSystemProperty ) { %>"\"-D${appNameSystemProperty}=\$APP_BASE_NAME\"" <% } %>-classpath "\"\$CLASSPATH\"" ${mainClassName} "\$APP_ARGS" + +exec "\$JAVACMD" "\$@" \ No newline at end of file diff --git a/grails-shell/src/test/groovy/org/grails/cli/TestTerminal.java b/grails-shell/src/test/groovy/org/grails/cli/TestTerminal.java new file mode 100644 index 00000000000..538f66d794a --- /dev/null +++ b/grails-shell/src/test/groovy/org/grails/cli/TestTerminal.java @@ -0,0 +1,11 @@ +package org.grails.cli; + +import jline.TerminalSupport; + +public class TestTerminal extends TerminalSupport { + public TestTerminal() { + super(true); + setAnsiSupported(false); + setEchoEnabled(false); + } +} diff --git a/grails-shell/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy b/grails-shell/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy new file mode 100644 index 00000000000..ef1cbf69363 --- /dev/null +++ b/grails-shell/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy @@ -0,0 +1,40 @@ +package org.grails.cli.interactive.completers + +import spock.lang.* + +class RegexCompletorSpec extends Specification { + @Unroll("String '#source' is not matching") + def "Simple pattern matches"() { + given: "a regex completor and an empty candidate list" + def completor = new RegexCompletor(/!\w+/) + def candidateList = [] + + when: "the completor is invoked for a given string" + def retval = completor.complete(source, 0, candidateList) + + then: "that string is the sole candidate and the return value is 0" + candidateList.size() == 1 + candidateList[0] == source + retval == 0 + + where: + source << [ "!ls", "!test_stuff" ] + } + + @Unroll("String '#source' is incorrectly matching") + def "Non matching strings"() { + given: "a regex completor and an empty candidate list" + def completor = new RegexCompletor(/!\w+/) + def candidateList = [] + + when: "the completor is invoked for a given (non-matching) string" + def retval = completor.complete(source, 0, candidateList) + + then: "the candidate list is empty and the return value is -1" + candidateList.size() == 0 + retval == -1 + + where: + source << [ "!ls ls", "!", "test", "" ] + } +} diff --git a/grails-shell/src/test/groovy/org/grails/cli/profile/ResourceProfileSpec.groovy b/grails-shell/src/test/groovy/org/grails/cli/profile/ResourceProfileSpec.groovy new file mode 100644 index 00000000000..cd4c071e874 --- /dev/null +++ b/grails-shell/src/test/groovy/org/grails/cli/profile/ResourceProfileSpec.groovy @@ -0,0 +1,245 @@ +package org.grails.cli.profile + +import groovy.transform.CompileStatic +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.graph.Dependency +import org.grails.cli.profile.commands.factory.YamlCommandFactory +import org.grails.io.support.Resource +import spock.lang.Specification + +/* + * Copyright 2014 original 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. + */ + +/** + * @author graemerocher + */ +class ResourceProfileSpec extends Specification { + + void "Test resource version"() { + given:"A resource profile" + def mockResource = Mock(Resource) + + def mockProfileYml = Mock(Resource) + mockProfileYml.getInputStream() >> new ByteArrayInputStream(getYamlWithCommandAndFeatures()) + mockResource.createRelative("profile.yml") >> mockProfileYml + mockResource.getURL() >> new URL("file:/path/to/my-profile-1.0.1.jar!profile.yml") + + def mockCommandYml = Mock(Resource) + mockCommandYml.getInputStream() >> new ByteArrayInputStream(getCommandYaml()) + mockCommandYml.exists() >> true + mockResource.createRelative("commands/clean.yml") >> mockCommandYml + mockResource.getURL() >> new URL("file:/path/to/my-profile-1.0.1.jar!/clean.yml") + + def mockFeatureYml = Mock(Resource) + mockFeatureYml.getInputStream() >> new ByteArrayInputStream(getFeatureYaml()) + mockFeatureYml.exists() >> true + mockResource.createRelative("features/hibernate/feature.yml") >> mockFeatureYml + mockResource.createRelative("features/hibernate/") >> mockResource + mockResource.createRelative("feature.yml") >> mockFeatureYml + mockResource.getURL() >> new URL("file:/path/to/my-profile-1.0.1.jar!/feature.yml") + + def profileRepository = Mock(ProfileRepository) + def profile = new ResourceProfile(profileRepository, "web", mockResource) + profileRepository.getProfile("web" ) >> profile + + def baseProfile = Mock(Profile) + baseProfile.getDependencies() >> [ new Dependency(new DefaultArtifact("foo:bar:2.0"), "test")] + baseProfile.getBuildPlugins() >> [ "foo-plug"] + baseProfile.features >> [] + profileRepository.getProfile("base") >> baseProfile + profileRepository.getProfile("base", true) >> baseProfile + + expect: + profile.version == '1.0.1' + !profile.dependencies.isEmpty() + !profile.features.isEmpty() + + when: + Feature feature = profile.features.find { (it.name == "hibernate") } + + then: + feature + !feature.dependencies.isEmpty() + + } + + void "test YamlCommandFactory readCommands"() { + + given: "A resource and profile" + Resource mockCommandYml = Mock() + TestYamlCommandFactory yamlCommandFactory = new TestYamlCommandFactory() + + when: + mockCommandYml.getInputStream() >> new ByteArrayInputStream(getCommandYaml()) + mockCommandYml.filename >> "clean.yml" + Map data = yamlCommandFactory.testReadCommandFile(mockCommandYml) + + then: + data + data.description == "Cleans a Grails application's compiled sources" + } + + void "Test dependencies"() { + given:"A resource profile" + + def mockResource = Mock(Resource) + def mockProfileYml = Mock(Resource) + mockProfileYml.getInputStream() >> new ByteArrayInputStream(getYaml()) + mockResource.createRelative("profile.yml") >> mockProfileYml + + def profileRepository = Mock(ProfileRepository) + + def profile = new ResourceProfile(profileRepository, "web", mockResource) + profileRepository.getProfile("web", true) >> profile + + def baseProfile = Mock(Profile) + baseProfile.getDependencies() >> [ new Dependency(new DefaultArtifact("foo:bar:2.0"), "test")] + baseProfile.getBuildPlugins() >> [ "foo-plug"] + profileRepository.getProfile("base", true) >> baseProfile + + + when:"The dependencies are accessed" + def deps = profile.dependencies + def plugins = profile.buildPlugins + + then:"They are correct" + plugins.size() == 2 + deps.size() == 2 + plugins == ['foo-plug', 'bar'] + deps[1].scope == 'compile' + deps[1].artifact.groupId == 'org.grails' + deps[1].artifact.artifactId == 'grails-core' + deps[1].artifact.version == '3.1.0' + deps[0].scope == 'test' + deps[0].artifact.groupId == 'foo' + deps[0].artifact.artifactId == 'bar' + deps[0].artifact.version == '2.0' + } + + + void "Test dependency exclusions"() { + given:"A resource profile" + + def mockResource = Mock(Resource) + def mockProfileYml = Mock(Resource) + mockProfileYml.getInputStream() >> new ByteArrayInputStream(getExcludesYaml()) + mockResource.createRelative("profile.yml") >> mockProfileYml + + def profileRepository = Mock(ProfileRepository) + + def profile = new ResourceProfile(profileRepository, "web", mockResource) + profileRepository.getProfile("web", true) >> profile + + def baseProfile = Mock(Profile) + baseProfile.getDependencies() >> [ new Dependency(new DefaultArtifact("foo:bar:2.0"), "test")] + baseProfile.getBuildPlugins() >> [ "foo-plug"] + profileRepository.getProfile("base", true) >> baseProfile + + + when:"The dependencies are accessed" + def deps = profile.dependencies + def plugins = profile.buildPlugins + + then:"They are correct" + deps.size() == 1 + plugins == ['bar'] + + deps.size() == 1 + deps[0].scope == 'compile' + deps[0].artifact.groupId == 'org.grails' + deps[0].artifact.artifactId == 'grails-core' + deps[0].artifact.version == '3.1.0' + } + + byte[] getYaml() { + """ +name: web +extends: base +build: + plugins: + - bar +dependencies: + - scope: compile + coords: org.grails:grails-core:3.1.0 +""".bytes + } + + byte[] getYamlWithCommandAndFeatures() { + """ +name: web +extends: base +features: + provided: + - hibernate +commands: + clean: clean.yml +build: + plugins: + - bar +dependencies: + - scope: compile + coords: org.grails:grails-core:3.1.0 +""".bytes + } + + byte[] getExcludesYaml() { + """ +name: web +extends: base +build: + plugins: + - bar + excludes: + - foo-plug +dependencies: + - scope: excludes + coords: foo:bar:* + - scope: compile + coords: org.grails:grails-core:3.1.0 +""".bytes + } + + byte[] getCommandYaml() { + """ +description: Cleans a Grails application's compiled sources +minArguments: 0 +usage: | + clean +steps: + - command: gradle + tasks: + - clean + +""".bytes + } + + byte[] getFeatureYaml() { + """ +description: Adds GORM for Hibernate 5 to the project +dependencies: + - scope: compile + coords: "org.hibernate:hibernate-core:5.4.0.Final" +""".bytes + } + + @CompileStatic + static class TestYamlCommandFactory extends YamlCommandFactory { + + Map testReadCommandFile(Resource resource) { + readCommandFile(resource) + } + } +} diff --git a/grails-shell/src/test/groovy/org/grails/cli/profile/commands/CommandScriptTransformSpec.groovy b/grails-shell/src/test/groovy/org/grails/cli/profile/commands/CommandScriptTransformSpec.groovy new file mode 100644 index 00000000000..7d5a42486c6 --- /dev/null +++ b/grails-shell/src/test/groovy/org/grails/cli/profile/commands/CommandScriptTransformSpec.groovy @@ -0,0 +1,63 @@ +/* + * Copyright 2014 original 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 org.grails.cli.profile.commands + +import org.grails.cli.interactive.completers.DomainClassCompleter +import org.grails.cli.profile.commands.factory.GroovyScriptCommandFactory +import org.grails.cli.profile.commands.script.GroovyScriptCommand +import spock.lang.Specification + +/** + * @author graemerocher + */ +class CommandScriptTransformSpec extends Specification { + + + void "Test that the CommandScriptTransform correctly populates the description"() { + given:"A GroovyClassLoader with the CommandScriptTransform applied" + def gcl = GroovyScriptCommandFactory.createGroovyScriptCommandClassLoader() + + when:"A script is parsed" + def script = (GroovyScriptCommand)(gcl.parseClass(''' +import org.grails.cli.interactive.completers.DomainClassCompleter + +description("example script") { + usage "example usage" + completer DomainClassCompleter + argument name: 'controllerName', description:'The name of the controller' + flag name:'test', description:'Do something' + +} + + +println "Hello!" +''', "MyScript").newInstance()) + + then:"The scripts description is correctly populated" + script.description.name == 'my-script' + script.description.description == 'example script' + script.description.usage == 'example usage' + script.description.arguments.size() == 1 + script.description.arguments[0].name == 'controllerName' + script.description.arguments[0].description == 'The name of the controller' + + script.description.flags.size() == 1 + script.description.flags[0].name == 'test' + script.description.flags[0].description == 'Do something' + script.description.completer instanceof DomainClassCompleter + } +} diff --git a/grails-shell/src/test/groovy/org/grails/cli/profile/commands/CreateAppCommandSpec.groovy b/grails-shell/src/test/groovy/org/grails/cli/profile/commands/CreateAppCommandSpec.groovy new file mode 100644 index 00000000000..8d3fa498bb0 --- /dev/null +++ b/grails-shell/src/test/groovy/org/grails/cli/profile/commands/CreateAppCommandSpec.groovy @@ -0,0 +1,105 @@ +package org.grails.cli.profile.commands + +import grails.build.logging.GrailsConsole +import org.grails.cli.profile.Feature +import org.grails.cli.profile.Profile +import org.spockframework.util.StringMessagePrintStream +import spock.lang.Shared +import spock.lang.Specification +/** + * Created by Jim on 7/18/2016. + */ +class CreateAppCommandSpec extends Specification { + + @Shared + StringPrintStream sps + + PrintStream originalOut + + void setup() { + System.setProperty("org.fusesource.jansi.Ansi.disable", "true") + originalOut = GrailsConsole.instance.out + sps = new StringPrintStream() + GrailsConsole.instance.out = sps + } + + void cleanup() { + System.setProperty("org.fusesource.jansi.Ansi.disable", "false") + GrailsConsole.instance.out = originalOut + } + + void "test evaluateFeatures - multiple, some valid"() { + given: + Feature bar = Mock(Feature) { + 2 * getName() >> "bar" + } + Profile profile = Mock(Profile) { + 1 * getName() >> "web" + 2 * getFeatures() >> [bar] + 1 * getRequiredFeatures() >> [] + } + + when: + Iterable features = new CreateAppCommand().evaluateFeatures(profile, ['foo', 'bar']) + + then: + features.size() == 1 + features[0] == bar + sps.toString() == "Warning |\nFeature foo does not exist in the profile web!\n" + } + + void "test evaluateFeatures - multiple, all valid"() { + given: + Feature foo = Mock(Feature) { + 2 * getName() >> "foo" + } + Feature bar = Mock(Feature) { + 2 * getName() >> "bar" + } + Profile profile = Mock(Profile) { + 0 * getName() + 2 * getFeatures() >> [foo, bar] + 1 * getRequiredFeatures() >> [] + } + + when: + Iterable features = new CreateAppCommand().evaluateFeatures(profile, ['foo', 'bar']) + + then: + features.size() == 2 + features[0] == foo + features[1] == bar + sps.toString() == "" + } + + void "test evaluateFeatures fat finger"() { + given: + Feature bar = Mock(Feature) { + 2 * getName() >> "mongodb" + } + Profile profile = Mock(Profile) { + 1 * getName() >> "web" + 2 * getFeatures() >> [bar] + 1 * getRequiredFeatures() >> [] + } + + when: + Iterable features = new CreateAppCommand().evaluateFeatures(profile, ['mongo']) + + then: + features.size() == 0 + sps.toString() == "Warning |\nFeature mongo does not exist in the profile web! Possible solutions: mongodb\n" + } + + class StringPrintStream extends StringMessagePrintStream { + StringBuilder stringBuilder = new StringBuilder() + @Override + protected void printed(String message) { + stringBuilder.append(message) + } + + String toString() { + stringBuilder.toString() + } + } +} diff --git a/grails-shell/src/test/groovy/org/grails/cli/profile/commands/events/CommandEventsTraitGeneratedSpec.groovy b/grails-shell/src/test/groovy/org/grails/cli/profile/commands/events/CommandEventsTraitGeneratedSpec.groovy new file mode 100644 index 00000000000..326cab9f8e8 --- /dev/null +++ b/grails-shell/src/test/groovy/org/grails/cli/profile/commands/events/CommandEventsTraitGeneratedSpec.groovy @@ -0,0 +1,20 @@ +package org.grails.cli.profile.commands.events + +import groovy.transform.Generated +import spock.lang.Specification + +import java.lang.reflect.Method + +class CommandEventsTraitGeneratedSpec extends Specification { + + void "test that all CommandEvents trait methods are marked as Generated"() { + expect: "all CommandEvents methods are marked as Generated on implementation class" + CommandEvents.getMethods().each { Method traitMethod -> + assert TestCommandEvents.class.getMethod(traitMethod.name, traitMethod.parameterTypes).isAnnotationPresent(Generated) + } + } +} + +class TestCommandEvents implements CommandEvents { + +} diff --git a/grails-shell/src/test/groovy/org/grails/cli/profile/repository/MavenRepositorySpec.groovy b/grails-shell/src/test/groovy/org/grails/cli/profile/repository/MavenRepositorySpec.groovy new file mode 100644 index 00000000000..2b2586bcb8b --- /dev/null +++ b/grails-shell/src/test/groovy/org/grails/cli/profile/repository/MavenRepositorySpec.groovy @@ -0,0 +1,37 @@ +package org.grails.cli.profile.repository + +import spock.lang.Ignore +import spock.lang.Specification + + + +/** + * @author graemerocher + */ +class MavenRepositorySpec extends Specification { + + @Ignore + void "Test resolve profile"() { + given:"A maven profile repository" + def repo = new MavenProfileRepository() + + when:"We resolve the web profile" + def profile = repo.getProfile("web") + + then:"The profile is not null" + profile != null + profile.name == 'web' + } + + @Ignore + void "Test list all profiles"() { + given:"A maven profile repository" + def repo = new MavenProfileRepository() + + when:"We resolve the web profile" + def profiles = repo.allProfiles + + then:"The profiles are not null or empty" + profiles + } +} diff --git a/grails-shell/src/test/groovy/org/grails/cli/profile/steps/StepRegistrySpec.groovy b/grails-shell/src/test/groovy/org/grails/cli/profile/steps/StepRegistrySpec.groovy new file mode 100644 index 00000000000..f82b8701eca --- /dev/null +++ b/grails-shell/src/test/groovy/org/grails/cli/profile/steps/StepRegistrySpec.groovy @@ -0,0 +1,31 @@ +package org.grails.cli.profile.steps + +import org.grails.cli.profile.commands.ClosureExecutingCommand +import spock.lang.Specification + +/* + * Copyright 2014 original 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. + */ + +/** + * @author graemerocher + */ +class StepRegistrySpec extends Specification { + + void "Test the step registry finds registered steps"() { + expect:"The step registry to find steps" + StepRegistry.getStep("render", new ClosureExecutingCommand("test", {}), [foo:true]) instanceof RenderStep + } +} diff --git a/grails-shell/src/test/resources/gradle-sample/build.gradle b/grails-shell/src/test/resources/gradle-sample/build.gradle new file mode 100644 index 00000000000..d6ef8c0d4f5 --- /dev/null +++ b/grails-shell/src/test/resources/gradle-sample/build.gradle @@ -0,0 +1,5 @@ +apply plugin:'java' + +subprojects { + task hello << {println "Hello World!"} +} diff --git a/grails-shell/src/test/resources/gradle-sample/gradle/wrapper/gradle-wrapper.jar b/grails-shell/src/test/resources/gradle-sample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..3d0dee6e8ed Binary files /dev/null and b/grails-shell/src/test/resources/gradle-sample/gradle/wrapper/gradle-wrapper.jar differ diff --git a/grails-shell/src/test/resources/gradle-sample/gradle/wrapper/gradle-wrapper.properties b/grails-shell/src/test/resources/gradle-sample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..a1b81796d7e --- /dev/null +++ b/grails-shell/src/test/resources/gradle-sample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Sep 30 10:07:59 EEST 2014 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip diff --git a/grails-shell/src/test/resources/gradle-sample/gradlew b/grails-shell/src/test/resources/gradle-sample/gradlew new file mode 100755 index 00000000000..91a7e269e19 --- /dev/null +++ b/grails-shell/src/test/resources/gradle-sample/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/grails-shell/src/test/resources/gradle-sample/gradlew.bat b/grails-shell/src/test/resources/gradle-sample/gradlew.bat new file mode 100644 index 00000000000..aec99730b4e --- /dev/null +++ b/grails-shell/src/test/resources/gradle-sample/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/grails-shell/src/test/resources/gradle-sample/settings.gradle b/grails-shell/src/test/resources/gradle-sample/settings.gradle new file mode 100644 index 00000000000..295eb0ef438 --- /dev/null +++ b/grails-shell/src/test/resources/gradle-sample/settings.gradle @@ -0,0 +1 @@ +include 'subproj', 'subproj2' \ No newline at end of file diff --git a/grails-shell/src/test/resources/gradle-sample/subproj/build.gradle b/grails-shell/src/test/resources/gradle-sample/subproj/build.gradle new file mode 100644 index 00000000000..27039c509b6 --- /dev/null +++ b/grails-shell/src/test/resources/gradle-sample/subproj/build.gradle @@ -0,0 +1,3 @@ +apply plugin:'java' + +task hello2 << {println "Another hello world!"} diff --git a/grails-shell/src/test/resources/gradle-sample/subproj2/build.gradle b/grails-shell/src/test/resources/gradle-sample/subproj2/build.gradle new file mode 100644 index 00000000000..d8e1a654889 --- /dev/null +++ b/grails-shell/src/test/resources/gradle-sample/subproj2/build.gradle @@ -0,0 +1,3 @@ +apply plugin:'java' + +task hello3 << {println "Third hello world!"} diff --git a/grails-shell/src/test/resources/profiles-repository/profiles/web/commands/TestGroovy.groovy b/grails-shell/src/test/resources/profiles-repository/profiles/web/commands/TestGroovy.groovy new file mode 100644 index 00000000000..2107e6413e9 --- /dev/null +++ b/grails-shell/src/test/resources/profiles-repository/profiles/web/commands/TestGroovy.groovy @@ -0,0 +1,3 @@ +description "Tests out a Groovy script", "grails test-groovy" + +println "good" \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 4fc6d7cdfa4..2cf93ab2c24 100644 --- a/settings.gradle +++ b/settings.gradle @@ -40,6 +40,7 @@ include ( 'grails-docs', 'grails-encoder', 'grails-logging', + 'grails-shell', 'grails-spring', 'grails-test', 'grails-web',