Skip to content
Open
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
def subprojects = ['groovy-ant',
'groovy-bsf',
'groovy-cli',
'groovy-console',
'groovy-docgenerator',
'groovy-groovydoc',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,22 +129,26 @@ private void changeBaseScriptType(final AnnotatedNode parent, final ClassNode cN
MethodNode runScriptMethod = ClassHelper.findSAM(baseScriptType);

// If they want to use a name other than than "run", then make the change.
if (isSuitableAbstractMethod(runScriptMethod)) {
if (isCustomScriptBodyMethod(runScriptMethod)) {
MethodNode defaultMethod = cNode.getDeclaredMethod("run", Parameter.EMPTY_ARRAY);
cNode.removeMethod(defaultMethod);
MethodNode methodNode = new MethodNode(runScriptMethod.getName(), runScriptMethod.getModifiers() & ~ACC_ABSTRACT
, runScriptMethod.getReturnType(), runScriptMethod.getParameters(), runScriptMethod.getExceptions()
, defaultMethod.getCode());
// The AST node metadata has the flag that indicates that this method is a script body.
// It may also be carrying data for other AST transforms.
methodNode.copyNodeMetaData(defaultMethod);
cNode.addMethod(methodNode);
// GROOVY-6706: Sometimes an NPE is thrown here.
// The reason is that our transform is getting called more than once sometimes.
if (defaultMethod != null) {
cNode.removeMethod(defaultMethod);
MethodNode methodNode = new MethodNode(runScriptMethod.getName(), runScriptMethod.getModifiers() & ~ACC_ABSTRACT
, runScriptMethod.getReturnType(), runScriptMethod.getParameters(), runScriptMethod.getExceptions()
, defaultMethod.getCode());
// The AST node metadata has the flag that indicates that this method is a script body.
// It may also be carrying data for other AST transforms.
methodNode.copyNodeMetaData(defaultMethod);
cNode.addMethod(methodNode);
}
}
}

private boolean isSuitableAbstractMethod(MethodNode node) {
private boolean isCustomScriptBodyMethod(MethodNode node) {
return node != null
&& !(node.getDeclaringClass().equals(ClassHelper.SCRIPT_TYPE)
&& !(node.getDeclaringClass().equals(ClassHelper.SCRIPT_TYPE)
&& "run".equals(node.getName())
&& node.getParameters().length == 0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ class BaseScriptTransformTest extends CompilableTestSupport {
"""
}

abstract class MyCustomScript extends Script {}

void testBaseScriptFromCompiler(){
CompilerConfiguration config = new CompilerConfiguration()
config.scriptBaseClass = MyCustomScript.name
Expand Down Expand Up @@ -259,6 +261,37 @@ class BaseScriptTransformTest extends CompilableTestSupport {
assert result
}

/**
* Test GROOVY-6706. Base script in import (or package) with a SAM.
*/
void testGROOVY_6706() {
assertScript '''
@BaseScript(CustomBase)
import groovy.transform.BaseScript

assert did_before
assert !did_after

42

abstract class CustomBase extends Script {
boolean did_before = false
boolean did_after = false

def run() {
before()
def r = internalRun()
after()
assert r == 42
}

abstract internalRun()

def before() { did_before = true }
def after() { did_after = true }
}'''
}

void testBaseScriptOnPackage() {
def result = new GroovyShell().evaluate('''
@BaseScript(DeclaredBaseScript)
Expand Down Expand Up @@ -315,7 +348,4 @@ class BaseScriptTransformTest extends CompilableTestSupport {
println 'ok'
'''
}

}

abstract class MyCustomScript extends Script {}
24 changes: 24 additions & 0 deletions subprojects/groovy-cli/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2003-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.
*/

dependencies {
compile rootProject
groovy rootProject
testCompile rootProject.sourceSets.test.runtimeClasspath
testCompile project(':groovy-cli')

compile('com.beust:jcommander:1.35')
}
263 changes: 263 additions & 0 deletions subprojects/groovy-cli/src/main/java/groovy/cli/JCommanderScript.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/*
* Copyright 2014-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 groovy.cli;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterDescription;
import com.beust.jcommander.ParameterException;

import groovy.lang.MissingPropertyException;
import groovy.lang.Script;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.List;

import static org.codehaus.groovy.runtime.DefaultGroovyMethods.join;

/**
* Base script that provides JCommander declarative (annotation-based) argument processing for scripts.
*
* @author Jim White
*/

abstract public class JCommanderScript extends Script {
/**
* Name of the property that holds the JCommander for this script (i.e. 'scriptJCommander').
*/
public final static String SCRIPT_JCOMMANDER = "scriptJCommander";

/**
* The script body
* @return The result of the script evaluation.
*/
protected abstract Object runScriptBody();

@Override
public Object run() {
String[] args = getScriptArguments();
JCommander jc = getScriptJCommanderWithInit();
try {
parseScriptArguments(jc, args);
for (ParameterDescription pd : jc.getParameters()) {
if (pd.isHelp() && pd.isAssigned()) return exitCode(printHelpMessage(jc, args));
}
runScriptCommand(jc);
return exitCode(runScriptBody());
} catch (ParameterException pe) {
return exitCode(handleParameterException(jc, args, pe));
}
}

/**
* If the given code is numeric and non-zero, then return that from this process using System.exit.
* Non-numeric values (including null) are taken to be zero and returned as-is.
*
* @param code
* @return the given code
*/
public Object exitCode(Object code) {
if (code instanceof Number) {
int codeValue = ((Number) code).intValue();
if (codeValue != 0) System.exit(codeValue);
}
return code;
}

/**
* Return the script arguments as an array of strings.
* The default implementation is to get the "args" property.
*
* @return the script arguments as an array of strings.
*/
public String[] getScriptArguments() {
return (String[]) getProperty("args");
}

/**
* Return the JCommander for this script.
* If there isn't one already, then create it using createScriptJCommander.
*
* @return the JCommander for this script.
*/
protected JCommander getScriptJCommanderWithInit() {
try {
JCommander jc = (JCommander) getProperty(SCRIPT_JCOMMANDER);
if (jc == null) {
jc = createScriptJCommander();
// The script has a real property (a field or getter) but if we let Script.setProperty handle
// this then it just gets stuffed into a binding that shadows the property.
// This is somewhat related to other bugged behavior in Script wrt properties and bindings.
// See http://jira.codehaus.org/browse/GROOVY-6582 for example.
// The correct behavior for Script.setProperty would be to check whether
// the property has a setter before creating a new script binding.
this.getMetaClass().setProperty(this, SCRIPT_JCOMMANDER, jc);
}
return jc;
} catch (MissingPropertyException mpe) {
JCommander jc = createScriptJCommander();
// Since no property or binding already exists, we can use plain old setProperty here.
setProperty(SCRIPT_JCOMMANDER, jc);
return jc;
}
}

/**
* Create a new (hopefully just once for this script!) JCommander instance.
* The default name for the command name in usage is the script's class simple name.
* This is the time to load it up with command objects, which is done by initializeJCommanderCommands.
*
* @return A JCommander instance with the commands (if any) initialized.
*/
public JCommander createScriptJCommander() {
JCommander jc = new JCommander(this);
jc.setProgramName(this.getClass().getSimpleName());

initializeJCommanderCommands(jc);

return jc;
}

/**
* Add command objects to the given JCommander.
* The default behavior is to look for Subcommand annotations.
*
* @param jc The JCommander instance to add the commands (if any) to.
*/
public void initializeJCommanderCommands(JCommander jc) {
Class cls = this.getClass();
while (cls != null) {
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
Annotation annotation = field.getAnnotation(Subcommand.class);
if (annotation != null) {
try {
field.setAccessible(true);
jc.addCommand(field.get(this));
} catch (IllegalAccessException e) {
printErrorMessage("Trying to add JCommander @Subcommand but got error '" + e.getMessage()
+ "' when getting value of field " + field.getName());
}
}
}

cls = cls.getSuperclass();
}
}

/**
* Do JCommander.parse using the given arguments.
* If you want to do any special checking before the Runnable commands get run,
* this is the place to do it by overriding.
*
* @param jc The JCommander instance for this script instance.
* @param args The argument array.
*/
public void parseScriptArguments(JCommander jc, String[] args) {
jc.parse(args);
}

/**
* If there are any objects implementing Runnable that are part of this command script,
* then run them. If there is a parsed command, then run those objects after the main command objects.
* Note that this will not run the main script though, we leave that for run to do (which will happen
* normally since groovy.lang.Script doesn't implement java.lang.Runnable).
*
* @param jc
*/
public void runScriptCommand(JCommander jc) {
List<Object> objects = jc.getObjects();

String parsedCommand = jc.getParsedCommand();
if (parsedCommand != null) {
JCommander commandCommander = jc.getCommands().get(parsedCommand);
objects.addAll(commandCommander.getObjects());
}

for (Object commandObject : objects) {
if (commandObject instanceof Runnable) {
Runnable runnableCommand = (Runnable) commandObject;
if ((Object) runnableCommand != (Object) this) {
runnableCommand.run();
}
}
}
}

/**
* Error messages that arise from command line processing call this.
* The default is to use the Script's println method (which will go to the
* 'out' binding, if any, and System.out otherwise).
* If you want to use System.err, a logger, or something, this is the thing to override.
*
* @param message
*/
public void printErrorMessage(String message) {
println(message);
}

/**
* If a ParameterException occurs during parseScriptArguments, runScriptCommand, or runScriptBody
* then this gets called to report the problem.
* The default behavior is to show the exception message using printErrorMessage, then call printHelpMessage.
* The return value becomes the return value for the Script.run which will be the exit code
* if we've been called from the command line.
*
* @param jc The JCommander instance
* @param args The argument array
* @param pe The ParameterException that occurred
* @return The value that Script.run should return (2 by default).
*/
public Object handleParameterException(JCommander jc, String[] args, ParameterException pe) {
StringBuilder sb = new StringBuilder();

sb.append("args: [");
sb.append(join(args, ", "));
sb.append("]");
sb.append("\n");

sb.append(pe.getMessage());

printErrorMessage(sb.toString());

printHelpMessage(jc, args);

return 3;
}

/**
* If a @Parameter whose help attribute is annotated as true appears in the arguments.
* then the script body is not run and this printHelpMessage method is called instead.
* The default behavior is to show the arguments and the JCommander.usage using printErrorMessage.
* The return value becomes the return value for the Script.run which will be the exit code
* if we've been called from the command line.
*
* @param jc The JCommander instance
* @param args The argument array
* @return The value that Script.run should return (1 by default).
*/
public Object printHelpMessage(JCommander jc, String[] args) {
StringBuilder sb = new StringBuilder();

jc.usage(sb);

printErrorMessage(sb.toString());

return 2;
}

}
Loading