diff --git a/spring-boot-cli/pom.xml b/spring-boot-cli/pom.xml index 33cf8056f86..9a1a12a95c4 100644 --- a/spring-boot-cli/pom.xml +++ b/spring-boot-cli/pom.xml @@ -233,14 +233,11 @@ true - org.springframework.boot.loader.PropertiesLauncher + org.springframework.boot.loader.JarLauncher ${start-class} - - groovy.lang.GroovyClassLoader - @@ -348,57 +345,6 @@ - - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - org.apache.maven.plugins - maven-antrun-plugin - [1.7,) - - run - - - - - - - - - - org.apache.maven.plugins - - - maven-dependency-plugin - - - [2.8,) - - - - copy-dependencies - - - - - - - - - - - - - diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java index 63f7135ba2d..ac61fe598e1 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/SpringCli.java @@ -22,10 +22,10 @@ import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.List; -import java.util.ServiceLoader; import java.util.Set; import org.springframework.boot.cli.command.AbstractCommand; +import org.springframework.boot.cli.command.InitCommand; /** * Spring Command Line Interface. This is the main entry-point for the Spring command line @@ -48,7 +48,7 @@ public class SpringCli { private static final Set NO_EXCEPTION_OPTIONS = EnumSet .noneOf(SpringCliException.Option.class); - private List commands; + private List commands = new ArrayList(); private String displayName = CLI_APP + " "; @@ -56,15 +56,11 @@ public class SpringCli { * Create a new {@link SpringCli} implementation with the default set of commands. */ public SpringCli() { - setCommands(ServiceLoader.load(CommandFactory.class, getClass().getClassLoader())); - } - - private void setCommands(Iterable iterable) { - this.commands = new ArrayList(); - for (CommandFactory factory : iterable) { - for (Command command : factory.getCommands(this)) { - this.commands.add(command); - } + try { + new InitCommand(this).run(); + } + catch (Exception e) { + throw new IllegalStateException("Cannot init with those args", e); } this.commands.add(0, new HelpCommand()); this.commands.add(new HintCommand()); diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/FileOptions.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/FileOptions.java index e6264883d9c..f38a723fb9f 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/FileOptions.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/FileOptions.java @@ -46,12 +46,20 @@ public class FileOptions { } /** - * Create a new {@link FileOptions} instance. + * Create a new {@link FileOptions} instance. If it is an error to pass options that + * specify non-existent files, but the default paths are allowed not to exist (the + * paths are tested before use). If default paths are provided and the option set + * contains no file arguments it is not an error even if none of the default paths + * exist). + * * @param optionSet the source option set * @param classLoader an optional classloader used to try and load files that are not - * found directly. + * found in the local filesystem + * @param defaultPaths the default paths to use if no files are provided in the option + * set */ - public FileOptions(OptionSet optionSet, ClassLoader classLoader) { + public FileOptions(OptionSet optionSet, ClassLoader classLoader, + String... defaultPaths) { List nonOptionArguments = optionSet.nonOptionArguments(); List files = new ArrayList(); for (Object option : nonOptionArguments) { @@ -63,18 +71,26 @@ public class FileOptions { if (filename.endsWith(".groovy") || filename.endsWith(".java")) { File file = getFile(filename, classLoader); if (file == null) { - throw new RuntimeException("Can't find " + filename); + throw new IllegalArgumentException("Can't find " + filename); } files.add(file); } } } - if (files.size() == 0) { - throw new RuntimeException("Please specify a file to run"); - } - this.files = Collections.unmodifiableList(files); this.args = Collections.unmodifiableList(nonOptionArguments.subList(files.size(), nonOptionArguments.size())); + if (files.size() == 0) { + if (defaultPaths.length == 0) { + throw new RuntimeException("Please specify at least one file to run"); + } + for (String path : defaultPaths) { + File file = getFile(path, classLoader); + if (file != null && file.exists()) { + files.add(file); + } + } + } + this.files = Collections.unmodifiableList(files); } private File getFile(String filename, ClassLoader classLoader) { diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/InitCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/InitCommand.java new file mode 100644 index 00000000000..272f7f48520 --- /dev/null +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/InitCommand.java @@ -0,0 +1,141 @@ +/* + * Copyright 2012-2013 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.springframework.boot.cli.command; + +import groovy.lang.GroovyClassLoader; +import groovy.lang.Script; + +import java.io.File; +import java.util.List; +import java.util.ServiceLoader; + +import joptsimple.OptionSet; + +import org.springframework.boot.cli.Command; +import org.springframework.boot.cli.CommandFactory; +import org.springframework.boot.cli.SpringCli; +import org.springframework.boot.cli.compiler.GroovyCompiler; +import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration; +import org.springframework.boot.cli.compiler.GroovyCompilerConfigurationAdapter; +import org.springframework.boot.cli.compiler.GroovyCompilerScope; +import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory; +import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration; + +/** + *

+ * Command to initialize the Spring CLI with commands from the classpath. If the current + * context class loader is a GroovyClassLoader then it can be enhanced by passing in + * compiler options (e.g. --classpath=...). + *

+ *

+ * If the current context class loader is not already GroovyClassLoader then one will be + * created and will replace the current context loader. In this case command arguments can + * include files to compile that have @Grab annotations to process. By + * default a script called "init.groovy" or "spring.groovy" is used if it exists in the + * current directory or the root of the classpath. + *

+ * + * @author Dave Syer + */ +public class InitCommand extends OptionParsingCommand { + + public static final String NAME = "init"; + + public InitCommand(SpringCli cli) { + super(NAME, "(Re)-initialize the Spring cli", new InitOptionHandler(cli)); + } + + private static class InitOptionHandler extends CompilerOptionHandler { + + private SpringCli cli; + private GroovyCompiler compiler; + + public InitOptionHandler(SpringCli cli) { + this.cli = cli; + } + + @Override + protected void run(OptionSet options) throws Exception { + + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + boolean enhanced = false; + + FileOptions fileOptions = new FileOptions(options, loader, "init.groovy", + "spring.groovy"); + File[] files = fileOptions.getFilesArray(); + + if (!(loader instanceof GroovyClassLoader)) { + + List repositoryConfiguration = RepositoryConfigurationFactory + .createDefaultRepositoryConfiguration(); + + GroovyCompilerConfiguration configuration = new InitGroovyCompilerConfigurationAdapter( + options, this, repositoryConfiguration); + + this.compiler = new GroovyCompiler(configuration); + loader = this.compiler.getLoader(); + Thread.currentThread().setContextClassLoader(loader); + + } + else { + String classpath = getClasspathOption().value(options); + if (classpath != null && classpath.length() > 0) { + ((GroovyClassLoader) loader).addClasspath(classpath); + enhanced = true; + } + } + + if (this.compiler != null && files.length > 0) { + Class[] classes = this.compiler.compile(files); + for (Class type : classes) { + if (Script.class.isAssignableFrom(type)) { + ((Script) type.newInstance()).run(); + } + } + enhanced = true; + } + + if (this.cli.getCommands().isEmpty() || enhanced) { + + for (CommandFactory factory : ServiceLoader.load(CommandFactory.class, + loader)) { + for (Command command : factory.getCommands(this.cli)) { + this.cli.register(command); + } + } + + } + + } + + } + + private static class InitGroovyCompilerConfigurationAdapter extends + GroovyCompilerConfigurationAdapter { + private InitGroovyCompilerConfigurationAdapter(OptionSet optionSet, + CompilerOptionHandler compilerOptionHandler, + List repositoryConfiguration) { + super(optionSet, compilerOptionHandler, repositoryConfiguration); + } + + @Override + public GroovyCompilerScope getScope() { + return GroovyCompilerScope.EXTENSION; + } + } + +} diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java index f567b74a359..31d67e38ae8 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/ShellCommand.java @@ -99,6 +99,7 @@ public class ShellCommand extends AbstractCommand { PromptCommand prompt = new PromptCommand(this); cli.register(prompt); + cli.register(new InitCommand(cli)); } private ConsoleReader createConsoleReader() throws IOException { diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/ExtendedGroovyClassLoader.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/ExtendedGroovyClassLoader.java index 11bf532d150..629196b1b05 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/ExtendedGroovyClassLoader.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/ExtendedGroovyClassLoader.java @@ -46,7 +46,7 @@ import org.springframework.util.FileCopyUtils; * @author Phillip Webb * @author Dave Syer */ -class ExtendedGroovyClassLoader extends GroovyClassLoader { +public class ExtendedGroovyClassLoader extends GroovyClassLoader { private static final String SHARED_PACKAGE = "org.springframework.boot.groovy"; diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java index c8880af12c1..086f7df51b4 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompiler.java @@ -48,7 +48,6 @@ import org.springframework.boot.cli.compiler.grape.AetherGrapeEngine; import org.springframework.boot.cli.compiler.grape.GrapeEngineInstaller; import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration; import org.springframework.boot.cli.compiler.transformation.DependencyAutoConfigurationTransformation; -import org.springframework.boot.cli.compiler.transformation.GrabResolversAutoConfigurationTransformation; import org.springframework.boot.cli.compiler.transformation.GroovyBeansTransformation; import org.springframework.boot.cli.compiler.transformation.ResolveDependencyCoordinatesTransformation; @@ -108,7 +107,7 @@ public class GroovyCompiler { } this.transformations = new ArrayList(); - this.transformations.add(new GrabResolversAutoConfigurationTransformation()); + // this.transformations.add(new GrabResolversAutoConfigurationTransformation()); this.transformations.add(new DependencyAutoConfigurationTransformation( this.loader, this.coordinatesResolver, this.compilerAutoConfigurations)); this.transformations.add(new GroovyBeansTransformation()); @@ -118,6 +117,10 @@ public class GroovyCompiler { } } + public ExtendedGroovyClassLoader getLoader() { + return this.loader; + } + private ExtendedGroovyClassLoader createLoader( GroovyCompilerConfiguration configuration) { ExtendedGroovyClassLoader loader = new ExtendedGroovyClassLoader( diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompilerScope.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompilerScope.java index 6693313295f..e5784fc3124 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompilerScope.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/GroovyCompilerScope.java @@ -24,7 +24,7 @@ package org.springframework.boot.cli.compiler; public enum GroovyCompilerScope { /** - * Default scope, exposes groovy-all.jar (loaded from the parent) and the shared cli + * Default scope, exposes groovy.jar (loaded from the parent) and the shared cli * package (loaded via groovy classloader). */ DEFAULT, diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/RepositoryConfigurationFactory.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/RepositoryConfigurationFactory.java index 413cfd2f1c6..e015ee1a72b 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/RepositoryConfigurationFactory.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/RepositoryConfigurationFactory.java @@ -50,8 +50,8 @@ public final class RepositoryConfigurationFactory { repositoryConfiguration.add(MAVEN_CENTRAL); if (!Boolean.getBoolean("disableSpringSnapshotRepos")) { - repositoryConfiguration.add(SPRING_SNAPSHOT); repositoryConfiguration.add(SPRING_MILESTONE); + repositoryConfiguration.add(SPRING_SNAPSHOT); } addDefaultCacheAsRespository(repositoryConfiguration); diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java index 96441a24590..fe24bc5e98e 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/AetherGrapeEngine.java @@ -263,6 +263,9 @@ public class AetherGrapeEngine implements GrapeEngine { } protected void addRepository(RemoteRepository repository) { + if (this.repositories.contains(repository)) { + return; + } if (repository.getProxy() == null) { RemoteRepository.Builder builder = new RemoteRepository.Builder(repository); builder.setProxy(this.proxySelector.getProxy(repository)); diff --git a/spring-boot-cli/src/test/java/cli/command/CustomCommand.java b/spring-boot-cli/src/test/java/cli/command/CustomCommand.java new file mode 100644 index 00000000000..24c9a363825 --- /dev/null +++ b/spring-boot-cli/src/test/java/cli/command/CustomCommand.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2013 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 cli.command; + +import org.springframework.boot.cli.command.AbstractCommand; + +/** + * @author Dave Syer + */ +public class CustomCommand extends AbstractCommand { + + public CustomCommand() { + super("custom", "Custom command added in tests"); + } + + @Override + public void run(String... args) throws Exception { + System.err.println("Custom Command Hello"); + } + +} diff --git a/spring-boot-cli/src/test/java/cli/command/CustomCommandFactory.java b/spring-boot-cli/src/test/java/cli/command/CustomCommandFactory.java new file mode 100644 index 00000000000..0c0231a97a0 --- /dev/null +++ b/spring-boot-cli/src/test/java/cli/command/CustomCommandFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2013 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 cli.command; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.boot.cli.Command; +import org.springframework.boot.cli.CommandFactory; +import org.springframework.boot.cli.SpringCli; + +/** + * @author Dave Syer + */ +public class CustomCommandFactory implements CommandFactory { + + @Override + public Collection getCommands(SpringCli cli) { + return Collections. singleton(new CustomCommand()); + } + +} diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/InitCommandTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/InitCommandTests.java new file mode 100644 index 00000000000..5587618e009 --- /dev/null +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/InitCommandTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2013 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.springframework.boot.cli.command; + +import groovy.lang.GroovyClassLoader; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.boot.OutputCapture; +import org.springframework.boot.cli.Command; +import org.springframework.boot.cli.SpringCli; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Dave Syer + */ +public class InitCommandTests { + + @Rule + public OutputCapture output = new OutputCapture(); + + private SpringCli cli = mock(SpringCli.class); + private InitCommand command = new InitCommand(this.cli); + private int defaultCount = new DefaultCommandFactory().getCommands(this.cli).size(); + private ClassLoader classLoader; + + @Before + public void init() { + this.classLoader = Thread.currentThread().getContextClassLoader(); + } + + @After + public void close() { + Thread.currentThread().setContextClassLoader(this.classLoader); + } + + @Test + public void explicitClasspath() throws Exception { + Thread.currentThread().setContextClassLoader(new GroovyClassLoader()); + this.command.run("--cp=src/test/plugins/custom/custom/0.0.1/custom-0.0.1.jar"); + verify(this.cli, times(this.defaultCount + 1)).register(any(Command.class)); + } + + @Test + public void initScript() throws Exception { + this.command.run("src/test/resources/grab.groovy"); + verify(this.cli, times(this.defaultCount + 1)).register(any(Command.class)); + assertTrue(this.output.toString().contains("Hello Grab")); + } + + @Test(expected = IllegalArgumentException.class) + public void initNonExistentScript() throws Exception { + this.command.run("nonexistent.groovy"); + } + + // There is an init.groovy on the test classpath so this succeeds + @Test + public void initDefault() throws Exception { + this.command.run(); + assertTrue(this.output.toString().contains("Hello World")); + } + +} diff --git a/spring-boot-cli/src/test/plugins/custom/META-INF/services/org.springframework.boot.cli.CommandFactory b/spring-boot-cli/src/test/plugins/custom/META-INF/services/org.springframework.boot.cli.CommandFactory new file mode 100644 index 00000000000..727204a63ad --- /dev/null +++ b/spring-boot-cli/src/test/plugins/custom/META-INF/services/org.springframework.boot.cli.CommandFactory @@ -0,0 +1 @@ +cli.command.CustomCommandFactory diff --git a/spring-boot-cli/src/test/plugins/custom/custom/0.0.1/custom-0.0.1.jar b/spring-boot-cli/src/test/plugins/custom/custom/0.0.1/custom-0.0.1.jar new file mode 100644 index 00000000000..6623163c931 Binary files /dev/null and b/spring-boot-cli/src/test/plugins/custom/custom/0.0.1/custom-0.0.1.jar differ diff --git a/spring-boot-cli/src/test/plugins/custom/custom/0.0.1/custom-0.0.1.pom b/spring-boot-cli/src/test/plugins/custom/custom/0.0.1/custom-0.0.1.pom new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-cli/src/test/resources/grab.groovy b/spring-boot-cli/src/test/resources/grab.groovy new file mode 100644 index 00000000000..d9394424ca7 --- /dev/null +++ b/spring-boot-cli/src/test/resources/grab.groovy @@ -0,0 +1,6 @@ +@GrabResolver(name="test", root="file:./src/test/plugins") +@Grab("custom:custom:0.0.1") +@Controller +class Foo {} + +println "Hello Grab" \ No newline at end of file diff --git a/spring-boot-cli/src/test/resources/init.groovy b/spring-boot-cli/src/test/resources/init.groovy new file mode 100644 index 00000000000..31665bffc0d --- /dev/null +++ b/spring-boot-cli/src/test/resources/init.groovy @@ -0,0 +1 @@ +println "Hello World" \ No newline at end of file