org.grails.cli.profile.tasks.Groovy Maven / Gradle / Ivy
Show all versions of grace-shell Show documentation
/*
* Copyright 2022-2024 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
*
* https://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.tasks;
import groovy.ant.AntBuilder;
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyShell;
import groovy.lang.MissingMethodException;
import groovy.lang.Script;
import groovy.util.CharsetToolkit;
import org.apache.groovy.io.StringBuilderWriter;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.filters.util.ChainReaderHelper;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.FilterChain;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.util.FileUtils;
import org.codehaus.groovy.ant.AntProjectPropertiesDelegate;
import org.codehaus.groovy.ant.LoggingHelper;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.codehaus.groovy.reflection.ReflectionUtils;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.ResourceGroovyMethods;
import org.codehaus.groovy.tools.ErrorReporter;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.security.PrivilegedAction;
import java.util.List;
import java.util.Vector;
import org.grails.build.logging.GrailsConsoleAntProject;
/**
* Groovy Ant task to execute a series of Groovy statements.
*
*
Statements can either be read in from a text file using
* the src attribute or from between the enclosing groovy tags.
*
* This class is copied from {@link org.codehaus.groovy.ant.Groovy},
* allow to customize {@link groovy.lang.Binding} and {@link groovy.ant.AntBuilder}.
*
* @since 2023.0
* @see org.codehaus.groovy.ant.Groovy
*/
public class Groovy extends Java {
private static final String PREFIX = "embedded_script_in_";
private static final String SUFFIX = "groovy_Ant_task";
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
/**
* encoding; set to null or empty means 'default'
*/
private String encoding = null;
/**
* output encoding; set to null or empty means 'default'
*/
private String outputEncoding = null;
private final LoggingHelper log = new LoggingHelper(this);
/**
* files to load
*/
private final Vector filesets = new Vector<>();
/**
* The input resource
*/
private Resource src = null;
/**
* input command
*/
private String command = "";
/**
* Results Output file
*/
private File output = null;
/**
* Append to an existing file or overwrite it?
*/
private boolean append = false;
private Path classpath;
private boolean fork = false;
private boolean includeAntRuntime = true;
private boolean useGroovyShell = false;
private String scriptBaseClass;
private String configscript;
private final List filterChains = new Vector<>();
/**
* Compiler configuration.
*
* Used to specify the debug output to print stacktraces in case something fails.
*/
private final CompilerConfiguration configuration = new CompilerConfiguration();
private final Commandline cmdline = new Commandline();
private boolean contextClassLoader;
private AntBuilder antBuilder;
private Binding binding = new Binding();
/**
* Should the script be executed using a forked process. Defaults to false.
*
* @param fork true if the script should be executed in a forked process
*/
@Override
public void setFork(boolean fork) {
this.fork = fork;
}
/**
* Declare the encoding to use when outputting to a file;
* Leave unspecified or use "" for the platform's default encoding.
*
* @param encoding the character encoding to use.
* @since 3.0.3
*/
public void setOutputEncoding(String encoding) {
this.outputEncoding = encoding;
}
/**
* Declare the encoding to use when inputting from a resource;
* If not supplied or the empty encoding is supplied, a guess will be made for file resources,
* otherwise the platform's default encoding will be used.
*
* @param encoding the character encoding to use.
* @since 3.0.3
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* Should a new GroovyShell be used when forking. Special variables won't be available
* but you don't need Ant in the classpath.
*
* @param useGroovyShell true if GroovyShell should be used to run the script directly
*/
public void setUseGroovyShell(boolean useGroovyShell) {
this.useGroovyShell = useGroovyShell;
}
/**
* Should the system classpath be included on the classpath when forking. Defaults to true.
*
* @param includeAntRuntime true if the system classpath should be on the classpath
*/
public void setIncludeAntRuntime(boolean includeAntRuntime) {
this.includeAntRuntime = includeAntRuntime;
}
/**
* Enable compiler to report stack trace information if a problem occurs
* during compilation.
*
* @param stacktrace set to true to enable stacktrace reporting
*/
public void setStacktrace(boolean stacktrace) {
configuration.setDebug(stacktrace);
}
/**
* Set the name of the file to be run. The folder of the file is automatically added to the classpath.
* Required unless statements are enclosed in the build file or a nested resource is supplied.
*
* @param srcFile the file containing the groovy script to execute
*/
public void setSrc(final File srcFile) {
addConfigured(new FileResource(srcFile));
}
/**
* Set an inline command to execute.
* NB: Properties are not expanded in this text.
*
* @param txt the inline groovy commands to execute
*/
public void addText(String txt) {
log.verbose("addText('" + txt + "')");
this.command += txt;
}
/**
* Adds a fileset (nested fileset attribute) which should represent a single source file.
*
* @param set the fileset representing a source file
*/
public void addFileset(FileSet set) {
filesets.addElement(set);
}
/**
* Set the output file;
* optional, defaults to the Ant log.
*
* @param output the output file
*/
@Override
public void setOutput(File output) {
this.output = output;
}
/**
* Whether output should be appended to or overwrite
* an existing file. Defaults to false.
*
* @param append set to true to append
*/
@Override
public void setAppend(boolean append) {
this.append = append;
}
/**
* Sets the classpath for loading.
*
* @param classpath The classpath to set
*/
@Override
public void setClasspath(final Path classpath) {
this.classpath = classpath;
}
public void setAntBuilder(AntBuilder antBuilder) {
this.antBuilder = antBuilder;
}
public void setBinding(Binding binding) {
this.binding = binding;
}
/**
* Returns a new path element that can be configured.
* Gets called for instance by Ant when it encounters a nested <classpath> element.
*
* @return the resulting created path
*/
@Override
public Path createClasspath() {
if (this.classpath == null) {
this.classpath = new Path(getProject());
}
return this.classpath.createPath();
}
/**
* Set the classpath for loading
* using the classpath reference.
*
* @param ref the refid to use
*/
@Override
public void setClasspathRef(final Reference ref) {
createClasspath().setRefid(ref);
}
/**
* Gets the classpath.
*
* @return Returns a Path
*/
public Path getClasspath() {
return classpath;
}
/**
* Sets the configuration script for the groovy compiler configuration.
*
* @param configscript path to the configuration script
*/
public void setConfigscript(final String configscript) {
this.configscript = configscript;
}
/**
* Legacy method to set the indy flag (only true is allowed)
*
* @param indy true means invokedynamic support is active
*/
@Deprecated
public void setIndy(final boolean indy) {
if (!indy) {
throw new BuildException("Disabling indy is no longer supported!", getLocation());
}
}
/**
* Set the script base class name
*
* @param scriptBaseClass the name of the base class for scripts
*/
public void setScriptBaseClass(final String scriptBaseClass) {
this.scriptBaseClass = scriptBaseClass;
}
/**
* If true, generates metadata for reflection on method parameter names (jdk8+ only). Defaults to false.
*
* @param parameters set to true to generate metadata.
*/
public void setParameters(boolean parameters) {
configuration.setParameters(parameters);
}
/**
* Returns true if parameter metadata generation has been enabled.
*/
public boolean getParameters() {
return configuration.getParameters();
}
/**
* Load the file and then execute it
*/
@Override
public void execute() throws BuildException {
log.info("execute script: " + this.src);
command = command.trim();
// process filesets
for (FileSet next : filesets) {
for (Resource res : next) {
if (src == null) {
src = res;
} else {
throw new BuildException("A single source resource must be provided!", getLocation());
}
}
}
if (src == null && command.length() == 0) {
throw new BuildException("Source does not exist!", getLocation());
}
if (src != null && !src.isExists()) {
throw new BuildException("Source resource does not exist!", getLocation());
}
if (outputEncoding == null || outputEncoding.isEmpty()) {
outputEncoding = Charset.defaultCharset().name();
}
try {
PrintStream out = System.out;
try {
if (output != null) {
log.verbose("Opening PrintStream to output file " + output);
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(output.getAbsolutePath(), append));
out = new PrintStream(bos, false, outputEncoding);
}
// if there are no groovy statements between the enclosing Groovy tags
// then read groovy statements in from a resource using the src attribute
if (command == null || command.trim().length() == 0) {
Reader reader;
if (src instanceof FileResource) {
File file = ((FileResource) src).getFile();
createClasspath().add(new Path(getProject(), file.getParentFile().getCanonicalPath()));
if (encoding != null && !encoding.isEmpty()) {
reader = new LineNumberReader(new InputStreamReader(new FileInputStream(file), encoding));
} else {
reader = new CharsetToolkit(file).getReader();
}
} else {
if (encoding != null && !encoding.isEmpty()) {
reader = new InputStreamReader(new BufferedInputStream(src.getInputStream()), encoding);
} else {
reader = new InputStreamReader(new BufferedInputStream(src.getInputStream()), Charset.defaultCharset());
}
}
readCommandFromReader(reader);
} else {
if (src != null) {
log.info("Ignoring supplied resource as direct script text found");
}
}
if (command != null) {
execGroovy(command, out);
}
} finally {
if (out != null && out != System.out) {
out.close();
}
}
} catch (IOException e) {
throw new BuildException(e, getLocation());
}
log.verbose("Statements executed successfully");
}
private void readCommandFromReader(Reader reader) {
try {
final long len = src.getSize();
log.debug("resource size = " + (len != Resource.UNKNOWN_SIZE ? String.valueOf(len) : "unknown"));
if (len == 0) {
log.info("Ignoring empty resource");
command = null;
} else {
try (ChainReaderHelper.ChainReader chainReader = new ChainReaderHelper(getProject(), reader, filterChains).with(crh -> {
if (len != Resource.UNKNOWN_SIZE && len <= Integer.MAX_VALUE) {
crh.setBufferSize((int) len);
}
}).getAssembledReader()) {
command = chainReader.readFully();
}
}
} catch (final IOException ioe) {
throw new BuildException("Unable to load resource: ", ioe, getLocation());
}
}
@Override
public Commandline.Argument createArg() {
return cmdline.createArgument();
}
/**
* Add the FilterChain element.
* @param filter the filter to add
*/
public final void addFilterChain(FilterChain filter) {
filterChains.add(filter);
}
/**
* Set the source resource.
* @param a the resource to load as a single element Resource collection.
*/
public void addConfigured(ResourceCollection a) {
if (a.size() != 1) {
throw new BuildException("Only single argument resource collections are supported");
}
src = a.iterator().next();
}
/**
* Read in lines and execute them.
*
* @param reader the reader from which to get the groovy source to exec
* @param out the outputstream to use
* @throws java.io.IOException if something goes wrong
*/
protected void runStatements(Reader reader, PrintStream out)
throws IOException {
log.debug("runStatements()");
StringBuilder txt = new StringBuilder();
String line = "";
BufferedReader in = new BufferedReader(reader);
while ((line = in.readLine()) != null) {
line = getProject().replaceProperties(line);
if (line.contains("--")) {
txt.append("\n");
}
}
// Catch any statements not followed by ;
if (!txt.toString().isEmpty()) {
execGroovy(txt.toString(), out);
}
}
/**
* Exec the statement.
*
* @param txt the groovy source to exec
* @param out not used?
*/
protected void execGroovy(final String txt, final PrintStream out) {
log.debug("execGroovy()");
// Check and ignore empty statements
if (txt.trim().isEmpty()) {
return;
}
log.verbose("Script: " + txt);
if (classpath != null) {
log.debug("Explicit Classpath: " + classpath.toString());
}
if (fork) {
log.debug("Using fork mode");
try {
createClasspathParts();
createNewArgs(txt);
super.setFork(fork);
super.setClassname(useGroovyShell ? "groovy.lang.GroovyShell" : "org.codehaus.groovy.ant.Groovy");
configureCompiler();
super.execute();
} catch (Exception e) {
Writer writer = new StringBuilderWriter();
new ErrorReporter(e, false).write(new PrintWriter(writer));
String message = writer.toString();
throw new BuildException("Script Failed: " + message, e, getLocation());
}
return;
}
Object mavenPom = null;
final Project project = getProject();
final ClassLoader baseClassLoader;
ClassLoader savedLoader = null;
final Thread thread = Thread.currentThread();
boolean maven = "org.apache.commons.grant.GrantProject".equals(project.getClass().getName());
// treat the case Ant is run through Maven, and
if (maven) {
if (contextClassLoader) {
throw new BuildException("Using setContextClassLoader not permitted when using Maven.", getLocation());
}
try {
final Object propsHandler = project.getClass().getMethod("getPropsHandler").invoke(project);
final Field contextField = propsHandler.getClass().getDeclaredField("context");
ReflectionUtils.trySetAccessible(contextField);
final Object context = contextField.get(propsHandler);
mavenPom = InvokerHelper.invokeMethod(context, "getProject", EMPTY_OBJECT_ARRAY);
} catch (Exception e) {
throw new BuildException("Impossible to retrieve Maven's Ant project: " + e.getMessage(), getLocation());
}
// load groovy into "root.maven" classloader instead of "root" so that
// groovy script can access Maven classes
baseClassLoader = mavenPom.getClass().getClassLoader();
} else {
baseClassLoader = GroovyShell.class.getClassLoader();
}
if (contextClassLoader || maven) {
savedLoader = thread.getContextClassLoader();
thread.setContextClassLoader(GroovyShell.class.getClassLoader());
}
final String scriptName = computeScriptName();
@SuppressWarnings("removal") // TODO a future Groovy version should perform the operation not as a privileged action
final GroovyClassLoader classLoader = java.security.AccessController.doPrivileged((PrivilegedAction) () ->
new GroovyClassLoader(baseClassLoader));
addClassPathes(classLoader);
configureCompiler();
final GroovyShell groovy = new GroovyShell(classLoader, binding, configuration);
try {
if (this.antBuilder == null) {
this.antBuilder = new AntBuilder(this);
}
parseAndRunScript(groovy, txt, mavenPom, scriptName, null, antBuilder);
} finally {
groovy.resetLoadedClasses();
groovy.getClassLoader().clearCache();
if (contextClassLoader || maven) thread.setContextClassLoader(savedLoader);
}
}
private void configureCompiler() {
if (scriptBaseClass != null) {
configuration.setScriptBaseClass(scriptBaseClass);
}
if (configscript != null) {
Binding binding = new Binding();
binding.setVariable("configuration", configuration);
CompilerConfiguration configuratorConfig = new CompilerConfiguration();
ImportCustomizer customizer = new ImportCustomizer();
customizer.addStaticStars("org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder");
configuratorConfig.addCompilationCustomizers(customizer);
GroovyShell shell = new GroovyShell(binding, configuratorConfig);
File confSrc = new File(configscript);
try {
shell.evaluate(confSrc);
} catch (IOException e) {
throw new BuildException("Unable to configure compiler using configuration file: " + confSrc, e);
}
}
}
private void parseAndRunScript(GroovyShell shell, String txt, Object mavenPom, String scriptName, File scriptFile, AntBuilder builder) {
try {
final Script script;
if (scriptFile != null) {
script = shell.parse(scriptFile);
} else {
script = shell.parse(txt, scriptName);
}
final Project project = getProject();
script.setProperty("ant", builder);
script.setProperty("project", project);
script.setProperty("properties", new AntProjectPropertiesDelegate(project));
script.setProperty("target", getOwningTarget());
script.setProperty("task", this);
if (project instanceof GrailsConsoleAntProject) {
script.setProperty("options", ((GrailsConsoleAntProject) project).getOptions());
}
script.setProperty("args", cmdline.getCommandline());
if (mavenPom != null) {
script.setProperty("pom", mavenPom);
}
script.run();
} catch (final MissingMethodException mme) {
// not a script, try running through run method but properties will not be available
if (scriptFile != null) {
try {
shell.run(scriptFile, cmdline.getCommandline());
} catch (IOException e) {
processError(e);
}
} else {
shell.run(txt, scriptName, cmdline.getCommandline());
}
} catch (final CompilationFailedException | IOException e) {
processError(e);
}
}
private void processError(Exception e) {
Writer writer = new StringBuilderWriter();
new ErrorReporter(e, false).write(new PrintWriter(writer));
String message = writer.toString();
throw new BuildException("Script Failed: " + message, e, getLocation());
}
public static void main(String[] args) {
final GroovyShell shell = new GroovyShell(new Binding());
final Groovy groovy = new Groovy();
for (int i = 1; i < args.length; i++) {
final Commandline.Argument argument = groovy.createArg();
argument.setValue(args[i]);
}
final AntBuilder builder = new AntBuilder();
groovy.setProject(builder.getProject());
groovy.parseAndRunScript(shell, null, null, null, new File(args[0]), builder);
}
private void createClasspathParts() {
Path path;
if (classpath != null) {
path = super.createClasspath();
path.setPath(classpath.toString());
}
if (includeAntRuntime) {
path = super.createClasspath();
path.setPath(System.getProperty("java.class.path"));
}
String groovyHome = null;
final String[] strings = getSysProperties().getVariables();
if (strings != null) {
for (String prop : strings) {
if (prop.startsWith("-Dgroovy.home=")) {
groovyHome = prop.substring("-Dgroovy.home=".length());
}
}
}
if (groovyHome == null) {
groovyHome = System.getProperty("groovy.home");
}
if (groovyHome == null) {
groovyHome = System.getenv("GROOVY_HOME");
}
if (groovyHome == null) {
throw new IllegalStateException("Neither ${groovy.home} nor GROOVY_HOME defined.");
}
File jarDir = new File(groovyHome, "lib");
if (!jarDir.exists()) {
throw new IllegalStateException("GROOVY_HOME incorrectly defined. No lib directory found in: " + groovyHome);
}
final File[] files = jarDir.listFiles();
if (files != null) {
for (File file : files) {
try {
log.debug("Adding jar to classpath: " + file.getCanonicalPath());
} catch (IOException e) {
// ignore
}
path = super.createClasspath();
path.setLocation(file);
}
}
}
private void createNewArgs(String txt) throws IOException {
final String[] args = cmdline.getCommandline();
// Temporary file - delete on exit, create (assured unique name).
final File tempFile = FileUtils.getFileUtils().createTempFile(null, PREFIX, SUFFIX, null, true, true);
final String[] commandline = new String[args.length + 1];
ResourceGroovyMethods.write(tempFile, txt);
commandline[0] = tempFile.getCanonicalPath();
System.arraycopy(args, 0, commandline, 1, args.length);
super.clearArgs();
for (String arg : commandline) {
final Commandline.Argument argument = super.createArg();
argument.setValue(arg);
}
}
/**
* Try to build a script name for the script of the groovy task to have a helpful value in stack traces in case of exception
*
* @return the name to use when compiling the script
*/
private String computeScriptName() {
if (src instanceof FileResource) {
FileResource fr = (FileResource) src;
return fr.getFile().getAbsolutePath();
} else {
String name = PREFIX;
if (getLocation().getFileName().length() > 0)
name += getLocation().getFileName().replaceAll("[^\\w_\\.]", "_").replaceAll("[\\.]", "_dot_");
else
name += SUFFIX;
return name;
}
}
/**
* Adds the class paths (if any)
*
* @param classLoader the classloader to configure
*/
protected void addClassPathes(final GroovyClassLoader classLoader) {
if (classpath != null) {
for (int i = 0; i < classpath.list().length; i++) {
classLoader.addClasspath(classpath.list()[i]);
}
}
}
/**
* print any results in the statement.
*
* @param out the output PrintStream to print to
*/
protected void printResults(PrintStream out) {
log.debug("printResults()");
out.println();
}
/**
* Setting to true will cause the contextClassLoader to be set with
* the classLoader of the shell used to run the script. Not used if
* fork is true. Not allowed when running from Maven but in that
* case the context classLoader is set appropriately for Maven.
*
* @param contextClassLoader set to true to set the context classloader
*/
public void setContextClassLoader(boolean contextClassLoader) {
this.contextClassLoader = contextClassLoader;
}
}