blob: 5749f621c2adc8f1428f812c0953b671167fb331 [file] [log] [blame]
package org.apache.maven.shared.scriptinterpreter;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import org.apache.maven.shared.utils.StringUtils;
import org.apache.maven.shared.utils.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Runs pre-/post-build hook scripts.
*
* @author Benjamin Bentmann
*/
public class ScriptRunner
{
private static final Logger LOG = LoggerFactory.getLogger( ScriptRunner.class );
/**
* The supported script interpreters, indexed by the lower-case file extension of their associated script files,
* never <code>null</code>.
*/
private Map<String, ScriptInterpreter> scriptInterpreters;
/**
* The common set of global variables to pass into the script interpreter, never <code>null</code>.
*/
private Map<String, Object> globalVariables;
/**
* The additional class path for the script interpreter, never <code>null</code>.
*/
private List<String> classPath;
/**
* The file encoding of the hook scripts or <code>null</code> to use platform encoding.
*/
private String encoding;
/**
* Creates a new script runner.
*/
public ScriptRunner()
{
scriptInterpreters = new LinkedHashMap<>();
scriptInterpreters.put( "bsh", new BeanShellScriptInterpreter() );
scriptInterpreters.put( "groovy", new GroovyScriptInterpreter() );
globalVariables = new HashMap<>();
classPath = new ArrayList<>();
}
public void addScriptInterpreter( String id, ScriptInterpreter scriptInterpreter )
{
scriptInterpreters.put( id, scriptInterpreter );
}
/**
* Sets a global variable for the script interpreter.
*
* @param name The name of the variable, must not be <code>null</code>.
* @param value The value of the variable, may be <code>null</code>.
*/
public void setGlobalVariable( String name, Object value )
{
this.globalVariables.put( name, value );
}
/**
* Sets the additional class path for the hook scripts. Note that the provided list is copied, so any later changes
* will not affect the scripts.
*
* @param classPath The additional class path for the script interpreter, may be <code>null</code> or empty if only
* the plugin realm should be used for the script evaluation. If specified, this class path will precede the
* artifacts from the plugin class path.
*/
public void setClassPath( List<String> classPath )
{
this.classPath = ( classPath != null ) ? new ArrayList<>( classPath ) : new ArrayList<String>();
}
/**
* Sets the file encoding of the hook scripts.
*
* @param encoding The file encoding of the hook scripts, may be <code>null</code> or empty to use the platform's
* default encoding.
*/
public void setScriptEncoding( String encoding )
{
this.encoding = StringUtils.isNotEmpty( encoding ) ? encoding : null;
}
/**
* Runs the specified hook script (after resolution).
*
* @param scriptDescription The description of the script to use for logging, must not be <code>null</code>.
* @param basedir The base directory of the project, must not be <code>null</code>.
* @param relativeScriptPath The path to the script relative to the project base directory, may be <code>null</code>
* to skip the script execution and may not have extensions (resolution will search).
* @param context The key-value storage used to share information between hook scripts, may be <code>null</code>.
* @param logger The logger to redirect the script output to, may be <code>null</code> to use stdout/stderr.
* @throws IOException If an I/O error occurred while reading the script file.
* @throws ScriptEvaluationException If the script did not return <code>true</code> of threw an exception.
*/
public void run( final String scriptDescription, final File basedir, final String relativeScriptPath,
final Map<String, ?> context, final ExecutionLogger logger )
throws IOException, ScriptEvaluationException
{
if ( relativeScriptPath == null )
{
LOG.debug( "{}: relativeScriptPath is null, not executing script", scriptDescription );
return;
}
final File scriptFile = resolveScript( new File( basedir, relativeScriptPath ) );
if ( !scriptFile.exists() )
{
LOG.debug( "{} : no script '{}' found in directory {}", scriptDescription, relativeScriptPath,
basedir.getAbsolutePath() );
return;
}
LOG.info( "run {} {}.{}",
scriptDescription, relativeScriptPath, FileUtils.extension( scriptFile.getAbsolutePath() ) );
executeRun( scriptDescription, scriptFile, context, logger );
}
/**
* Runs the specified hook script.
*
* @param scriptDescription The description of the script to use for logging, must not be <code>null</code>.
* @param scriptFile The path to the script, may be <code>null</code> to skip the script execution.
* @param context The key-value storage used to share information between hook scripts, may be <code>null</code>.
* @param logger The logger to redirect the script output to, may be <code>null</code> to use stdout/stderr.
* @throws IOException If an I/O error occurred while reading the script file.
* @throws ScriptEvaluationException If the script did not return <code>true</code> of threw an exception.
*/
public void run( final String scriptDescription, File scriptFile, final Map<String, ?> context,
final ExecutionLogger logger )
throws IOException, ScriptEvaluationException
{
if ( !scriptFile.exists() )
{
LOG.debug( "{} : script file not found in directory {}", scriptDescription, scriptFile.getAbsolutePath() );
return;
}
LOG.info( "run {} {}", scriptDescription, scriptFile.getAbsolutePath() );
executeRun( scriptDescription, scriptFile, context, logger );
}
private void executeRun( final String scriptDescription, File scriptFile,
final Map<String, ?> context, final ExecutionLogger logger )
throws IOException, ScriptEvaluationException
{
ScriptInterpreter interpreter = getInterpreter( scriptFile );
if ( LOG.isDebugEnabled() )
{
String name = interpreter.getClass().getName();
name = name.substring( name.lastIndexOf( '.' ) + 1 );
LOG.debug( "Running script with {} :{}", name, scriptFile );
}
String script;
try
{
script = FileUtils.fileRead( scriptFile, encoding );
}
catch ( IOException e )
{
String errorMessage =
"error reading " + scriptDescription + " " + scriptFile.getPath() + ", " + e.getMessage();
throw new IOException( errorMessage, e );
}
Object result;
try
{
if ( logger != null )
{
logger.consumeLine( "Running " + scriptDescription + ": " + scriptFile );
}
PrintStream out = ( logger != null ) ? logger.getPrintStream() : null;
Map<String, Object> scriptVariables = new HashMap<>( this.globalVariables );
scriptVariables.put( "basedir", scriptFile.getParentFile() );
scriptVariables.put( "context", context );
result = interpreter.evaluateScript( script, classPath, scriptVariables, out );
if ( logger != null )
{
logger.consumeLine( "Finished " + scriptDescription + ": " + scriptFile );
}
}
catch ( ScriptEvaluationException e )
{
Throwable t = ( e.getCause() != null ) ? e.getCause() : e;
if ( logger != null )
{
t.printStackTrace( logger.getPrintStream() );
}
throw e;
}
if ( !Boolean.parseBoolean( String.valueOf( result ) ) )
{
throw new ScriptEvaluationException( "The " + scriptDescription + " returned " + result + "." );
}
}
/**
* Gets the effective path to the specified script. For convenience, we allow to specify a script path as "verify"
* and have the plugin auto-append the file extension to search for "verify.bsh" and "verify.groovy".
*
* @param scriptFile The script file to resolve, may be <code>null</code>.
* @return The effective path to the script file or <code>null</code> if the input was <code>null</code>.
*/
private File resolveScript( File scriptFile )
{
if ( scriptFile != null && !scriptFile.exists() )
{
for ( String ext : this.scriptInterpreters.keySet() )
{
File candidateFile = new File( scriptFile.getPath() + '.' + ext );
if ( candidateFile.exists() )
{
scriptFile = candidateFile;
break;
}
}
}
return scriptFile;
}
/**
* Determines the script interpreter for the specified script file by looking at its file extension. In this
* context, file extensions are considered case-insensitive. For backward compatibility with plugin versions 1.2-,
* the BeanShell interpreter will be used for any unrecognized extension.
*
* @param scriptFile The script file for which to determine an interpreter, must not be <code>null</code>.
* @return The script interpreter for the file, never <code>null</code>.
*/
private ScriptInterpreter getInterpreter( File scriptFile )
{
String ext = FileUtils.extension( scriptFile.getName() ).toLowerCase( Locale.ENGLISH );
ScriptInterpreter interpreter = scriptInterpreters.get( ext );
if ( interpreter == null )
{
interpreter = scriptInterpreters.get( "bsh" );
}
return interpreter;
}
}