/***************************************************************************
* Copyright (C) 2008-2010 by Fabrizio Montesi <famontesi@gmail.com> *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Library General Public License as *
* published by the Free Software Foundation; either version 2 of the *
* License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
* *
* For details about the authors of this software, see the AUTHORS file. *
***************************************************************************/
package jolie;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import jolie.jap.JapURLConnection;
import jolie.lang.Constants;
import jolie.lang.parse.Scanner;
import jolie.runtime.correlation.CorrelationEngine;
/**
* A parser for JOLIE's command line arguments,
* providing methods for accessing them.
* @author Fabrizio Montesi
*/
public class CommandLineParser
{
private final static Pattern pathSeparatorPattern = Pattern.compile( jolie.lang.Constants.pathSeparator );
private final static Pattern optionSeparatorPattern = Pattern.compile( " " );
private final int connectionsLimit;
private final int connectionsCache;
private final CorrelationEngine.Type correlationAlgorithmType;
private final String[] includePaths;
private final String[] optionArgs;
private final String[] whitepages;
private final URL[] libURLs;
private final InputStream programStream;
private final String programFilepath;
private final String[] arguments;
private final Map< String, Scanner.Token > constants = new HashMap< String, Scanner.Token >();
private final boolean verbose;
private final JolieClassLoader jolieClassLoader;
private final boolean isProgramCompiled;
private final boolean typeCheck;
private File programDirectory = null;
/**
* Returns the arguments passed to the JOLIE program.
* @return the arguments passed to the JOLIE program.
*/
public String[] arguments()
{
return arguments;
}
/**
* Returns {@code true} if the program is compiled, {@code false} otherwise.
* @return {@code true} if the program is compiled, {@code false} otherwise.
*/
public boolean isProgramCompiled()
{
return isProgramCompiled;
}
/**
* Returns the file path of the JOLIE program to execute.
* @return the file path of the JOLIE program to execute
*/
public String programFilepath()
{
return programFilepath;
}
/**
* Returns an InputStream for the program code to execute.
* @return an InputStream for the program code to execute
*/
public InputStream programStream()
{
return programStream;
}
/**
* Returns the library URLs passed by command line with the -l option.
* @return the library URLs passed by command line
*/
public URL[] libURLs()
{
return libURLs;
}
/**
* Returns the include paths passed by command line with the -i option.
* @return the include paths passed by command line
*/
public String[] includePaths()
{
return includePaths;
}
/**
* Returns the connection limit parameter
* passed by command line with the -c option.
* @return the connection limit parameter passed by command line
*/
public int connectionsLimit()
{
return connectionsLimit;
}
/**
* Returns the connection cache parameter
* passed by command line with the --conncache option.
* @return the connection cache parameter passed by command line
*/
public int connectionsCache()
{
return connectionsCache;
}
private static String getOptionString( String option, String description )
{
return( '\t' + option + "\t\t" + description + '\n' );
}
private String getVersionString()
{
return( Constants.VERSION + " " + Constants.COPYRIGHT );
}
/**
* Returns a map containing the constants defined by command line.
* @return a map containing the constants defined by command line
*/
public Map< String, Scanner.Token > definedConstants()
{
return constants;
}
private String getHelpString()
{
StringBuilder helpBuilder = new StringBuilder();
helpBuilder.append( getVersionString() );
helpBuilder.append( "\n\nUsage: jolie [options] behaviour_file [options] [program arguments]\n\n" );
helpBuilder.append( "Available options:\n" );
helpBuilder.append(
getOptionString( "-h, --help", "Display this help information" ) );
//TODO include doc for -l and -i
helpBuilder.append(
getOptionString( "-C ConstantIdentifier=ConstantValue", "Sets constant ConstantIdentifier to ConstantValue before starting execution" ) );
helpBuilder.append(
getOptionString( "--connlimit [number]", "Set the maximum number of active connection threads" ) );
helpBuilder.append(
getOptionString( "--conncache [number]", "Set the maximum number of cached persistent output connections" ) );
helpBuilder.append(
getOptionString( "--correlationAlgorithm [simple|hash]", "Set the algorithm to use for message correlation" ) );
helpBuilder.append(
getOptionString( "--typecheck [true|false]", "Check for correlation and other data related typing errors (default: false)" ) );
helpBuilder.append(
getOptionString( "-R [location]", "Add the service at [location] to the registry whitelist for this Jolie program" ) );
helpBuilder.append(
getOptionString( "--verbose", "Activate verbose mode" ) );
helpBuilder.append(
getOptionString( "--version", "Display this program version information" ) );
return helpBuilder.toString();
}
private void parseCommandLineConstant( String input )
throws IOException
{
try {
Scanner scanner = new Scanner( new ByteArrayInputStream( input.getBytes() ), new URI( "urn:CommandLine" ) );
Scanner.Token token = scanner.getToken();
if ( token.is( Scanner.TokenType.ID ) ) {
String id = token.content();
token = scanner.getToken();
if ( token.isNot( Scanner.TokenType.ASSIGN ) ) {
throw new IOException( "expected = after constant identifier " + id + ", found token type " + token.type() );
}
token = scanner.getToken();
if ( token.isValidConstant() == false ) {
throw new IOException( "expected constant value for constant identifier " + id + ", found token type " + token.type() );
}
constants.put( id, token );
} else {
throw new IOException( "expected constant identifier, found token type " + token.type() );
}
} catch( URISyntaxException e ) {
throw new IOException( e );
}
}
/**
* Returns <code>true</code> if the verbose option has been specified, false otherwise.
* @return <code>true</code> if the verbose option has been specified, false otherwise
*/
public boolean verbose()
{
return verbose;
}
/**
* Returns the type of correlation algorithm that has been specified.
* @return the type of correlation algorithm that has been specified.
* @see CorrelationAlgorithm
*/
public CorrelationEngine.Type correlationAlgorithmType()
{
return correlationAlgorithmType;
}
/**
* Constructor
* @param args the command line arguments
* @param classLoader the ClassLoader to use for finding resources
* @throws jolie.CommandLineException if the command line is not valid or asks for simple information. (like --help and --version)
*/
public CommandLineParser( String[] args, ClassLoader parentClassLoader )
throws CommandLineException, IOException
{
this( args, parentClassLoader, ArgumentHandler.DEFAULT_ARGUMENT_HANDLER );
}
public CommandLineParser( String[] args, ClassLoader parentClassLoader, ArgumentHandler argHandler )
throws CommandLineException, IOException
{
List< String > argsList = new ArrayList< String >( args.length );
Collections.addAll( argsList, args );
String csetAlgorithmName = "simple";
List< String > optionsList = new ArrayList< String >();
boolean bVerbose = false;
boolean bTypeCheck = false; // Default for typecheck
List< String > programArgumentsList = new ArrayList< String >();
List< String > whitepageList = new ArrayList< String >();
LinkedList< String > includeList = new LinkedList< String >();
List< String > libList = new ArrayList< String >();
int cLimit = -1;
int cCache = 100;
String pwd = new File( "" ).getCanonicalPath();
includeList.add( pwd );
includeList.add( "include" );
libList.add( pwd );
libList.add( "ext" );
libList.add( "lib" );
String olFilepath = null;
String japUrl = null;
for( int i = 0; i < argsList.size(); i++ ) {
if ( "--help".equals( argsList.get( i ) ) || "-h".equals( argsList.get( i ) ) ) {
throw new CommandLineException( getHelpString() );
} else if ( "-C".equals( argsList.get( i ) ) ) {
optionsList.add( argsList.get( i ) );
i++;
try {
parseCommandLineConstant( argsList.get( i ) );
} catch( IOException e ) {
throw new CommandLineException( "Invalid constant definition, reason: " + e.getMessage() );
}
optionsList.add( argsList.get( i ) );
} else if ( "-R".equals( argsList.get( i ) ) ) {
optionsList.add( argsList.get( i ) );
i++;
whitepageList.add( argsList.get( i ) );
optionsList.add( argsList.get( i ) );
} else if ( "-i".equals( argsList.get( i ) ) ) {
optionsList.add( argsList.get( i ) );
i++;
if ( japUrl != null ) {
argsList.set( i, argsList.get( i ).replace( "$JAP$", japUrl ) );
}
String[] tmp = pathSeparatorPattern.split( argsList.get( i ) );
Collections.addAll( includeList, tmp );
optionsList.add( argsList.get( i ) );
} else if ( "-l".equals( argsList.get( i ) ) ) {
optionsList.add( argsList.get( i ) );
i++;
if ( japUrl != null ) {
argsList.set( i, argsList.get( i ).replace( "$JAP$", japUrl ) );
}
String[] tmp = pathSeparatorPattern.split( argsList.get( i ) );
Collections.addAll( libList, tmp );
optionsList.add( argsList.get( i ) );
} else if ( "--connlimit".equals( argsList.get( i ) ) ) {
optionsList.add( argsList.get( i ) );
i++;
cLimit = Integer.parseInt( argsList.get( i ) );
optionsList.add( argsList.get( i ) );
} else if ( "--conncache".equals( argsList.get( i ) ) ) {
optionsList.add( argsList.get( i ) );
i++;
cCache = Integer.parseInt( argsList.get( i ) );
optionsList.add( argsList.get( i ) );
} else if ( "--correlationAlgorithm".equals( argsList.get( i ) ) ) {
optionsList.add( argsList.get( i ) );
i++;
csetAlgorithmName = argsList.get( i );
optionsList.add( argsList.get( i ) );
} else if ( "--typecheck".equals( argsList.get( i ) ) ) {
optionsList.add( argsList.get( i ) );
i++;
String typeCheckStr = argsList.get( i );
optionsList.add( argsList.get( i ) );
if ( "false".equals( typeCheckStr ) ) {
bTypeCheck = false;
} else if ( "true".equals( typeCheckStr ) ) {
bTypeCheck = true;
}
} else if ( "--verbose".equals( argsList.get( i ) ) ) {
optionsList.add( argsList.get( i ) );
bVerbose = true;
} else if ( "--version".equals( argsList.get( i ) ) ) {
throw new CommandLineException( getVersionString() );
} else if (
argsList.get( i ).endsWith( ".ol" )
||
argsList.get( i ).endsWith( ".iol" )
||
argsList.get( i ).endsWith( ".olc" )
) {
if ( olFilepath == null ) {
olFilepath = argsList.get( i );
} else {
programArgumentsList.add( argsList.get( i ) );
}
} else if ( argsList.get( i ).endsWith( ".jap" ) ) {
if ( olFilepath == null ) {
String japFilename = new File( argsList.get( i ) ).getCanonicalPath();
JarFile japFile = new JarFile( japFilename );
Manifest manifest = japFile.getManifest();
olFilepath = parseJapManifestForMainProgram( manifest, japFile );
libList.add( japFilename );
Collection< String > japOptions = parseJapManifestForOptions( manifest );
argsList.addAll( i+1, japOptions );
japUrl = japFilename + "!";
programDirectory = new File( japFilename ).getParentFile();
} else {
programArgumentsList.add( argsList.get( i ) );
}
} else {
if ( olFilepath == null ) { // It's an unrecognized argument
int newIndex = argHandler.onUnrecognizedArgument( argsList, i );
if ( newIndex == i ) {
// The handler didn't change the index.
// We abort so to avoid infinite looping.
throw new CommandLineException( "Unrecognized command line option: " + argsList.get( i ) );
}
i = newIndex;
} else { // They are command line arguments for the Jolie program
for( ; i < argsList.size(); i++ ) {
programArgumentsList.add( argsList.get( i ) );
}
}
}
}
verbose = bVerbose;
typeCheck = bTypeCheck;
correlationAlgorithmType = CorrelationEngine.Type.fromString( csetAlgorithmName );
if ( correlationAlgorithmType == null ) {
throw new CommandLineException( "Unrecognized correlation algorithm: " + csetAlgorithmName );
}
optionArgs = optionsList.toArray( new String[ optionsList.size() ] );
arguments = programArgumentsList.toArray( new String[ programArgumentsList.size() ] );
whitepages = whitepageList.toArray( new String[ whitepageList.size() ] );
if ( olFilepath == null ) {
throw new CommandLineException( "Input file not specified." );
} else if ( olFilepath.endsWith( ".olc" ) ) {
isProgramCompiled = true;
} else {
isProgramCompiled = false;
}
programFilepath = olFilepath;
connectionsLimit = cLimit;
connectionsCache = cCache;
List< URL > urls = new ArrayList< URL >();
for( String path : libList ) {
if ( path.contains( "!/" ) && !path.startsWith( "jap:" ) && !path.startsWith( "jar:" ) ) {
path = "jap:file:" + path;
}
if ( path.endsWith( ".jar" ) || path.endsWith( ".jap" ) ) {
if ( path.startsWith( "jap:" ) ) {
urls.add( new URL( path + "!/" ) );
} else {
urls.add( new URL( "jap:file:" + path + "!/" ) );
}
} else if ( new File( path ).isDirectory() ) {
urls.add( new URL( "file:" + path + "/" ) );
} else if ( path.endsWith( Constants.fileSeparator + "*" ) ) {
File dir = new File( path.substring( 0, path.length() - 2 ) );
String jars[] = dir.list( new FilenameFilter() {
public boolean accept( File dir, String filename ) {
return filename.endsWith( ".jar" );
}
} );
if ( jars != null ) {
for( String jarPath : jars ) {
urls.add( new URL( "jar:file:" + dir.getCanonicalPath() + '/' + jarPath + "!/" ) );
}
}
} else {
try {
urls.add( new URL( path ) );
} catch( MalformedURLException e ) {}
}
}
libURLs = urls.toArray( new URL[]{} );
jolieClassLoader = new JolieClassLoader( libURLs, parentClassLoader );
programStream = getOLStream( olFilepath, includeList, jolieClassLoader );
if ( programStream == null ) {
throw new FileNotFoundException( olFilepath );
}
includePaths = includeList.toArray( new String[]{} );
}
public File programDirectory()
{
return programDirectory;
}
public boolean typeCheck()
{
return typeCheck;
}
public JolieClassLoader jolieClassLoader()
{
return jolieClassLoader;
}
// TODO: What do optionArgs represent?
public String[] optionArgs()
{
return optionArgs;
}
private String parseJapManifestForMainProgram( Manifest manifest, JarFile japFile )
{
String filepath = null;
if ( manifest != null ) { // See if a main program is defined through a Manifest attribute
Attributes attrs = manifest.getMainAttributes();
filepath = attrs.getValue( Constants.Manifest.MainProgram );
}
if ( filepath == null ) { // Main program not defined, we make <japName>.ol and <japName>.olc guesses
String name = japFile.getName();
filepath = new StringBuilder()
.append( name.subSequence( 0, name.lastIndexOf( ".jap" ) ) )
.append( ".ol" )
.toString();
if ( japFile.getEntry( filepath ) == null ) {
filepath = null;
filepath = filepath + 'c';
if ( japFile.getEntry( filepath ) == null ) {
filepath = null;
}
}
}
if ( filepath != null ) {
filepath = new StringBuilder()
.append( "jap:file:" )
.append( japFile.getName() )
.append( "!/" )
.append( filepath )
.toString();
}
return filepath;
}
private Collection< String > parseJapManifestForOptions( Manifest manifest )
throws IOException
{
Collection< String > optionList = new ArrayList();
if ( manifest != null ) {
Attributes attrs = manifest.getMainAttributes();
String options = attrs.getValue( Constants.Manifest.Options );
if ( options != null ) {
String[] tmp = optionSeparatorPattern.split( options );
Collections.addAll( optionList, tmp );
}
}
return optionList;
}
private InputStream getOLStream( String olFilepath, LinkedList< String > includePaths, ClassLoader classLoader )
throws FileNotFoundException, IOException
{
InputStream olStream = null;
URL olURL = null;
File f = new File( olFilepath ).getAbsoluteFile();
if ( f.exists() ) {
olStream = new FileInputStream( f );
programDirectory = f.getParentFile();
} else {
for( int i = 0; i < includePaths.size() && olStream == null; i++ ) {
f = new File(
includePaths.get( i ) +
jolie.lang.Constants.fileSeparator +
olFilepath
);
if ( f.exists() ) {
f = f.getAbsoluteFile();
olStream = new BufferedInputStream( new FileInputStream( f ) );
programDirectory = f.getParentFile();
}
}
if ( olStream == null ) {
try {
olURL = new URL( olFilepath );
olStream = olURL.openStream();
if ( olStream == null ) {
throw new MalformedURLException();
}
} catch( MalformedURLException e ) {
olURL = classLoader.getResource( olFilepath );
if ( olURL != null ) {
olStream = olURL.openStream();
}
}
if ( programDirectory == null && olURL != null && olURL.getPath() != null ) {
// Try to extract the parent directory of the JAP/JAR library file
try {
File urlFile = new File( JapURLConnection.nestingSeparatorPattern.split( new URI( olURL.getPath() ).getSchemeSpecificPart() )[0] ).getAbsoluteFile();
if ( urlFile.exists() ) {
programDirectory = urlFile.getParentFile();
}
} catch( URISyntaxException e ) {}
}
}
}
if ( olStream != null ) {
if ( f.exists() && f.getParent() != null ) {
includePaths.addFirst( f.getParent() );
} else if ( olURL != null ) {
String urlString = olURL.toString();
includePaths.addFirst( urlString.substring( 0, urlString.lastIndexOf( "/" ) + 1 ) );
}
}
return olStream;
}
/**
* A handler for unrecognized arguments, meant to be implemented
* by classes that wants to extend the behaviour of {@link jolie.CommandLineParser}.
* @author Fabrizio Montesi
*/
public interface ArgumentHandler
{
/**
* Called when {@link CommandLineParser} cannot recognize a command line argument.
* @param argumentsList the argument list.
* @param index the index at which the unrecognized argument has been found in the list.
* @return the new index at which the {@link CommandLineParser} should continue parsing the arguments.
* @throws CommandLineException if the argument is invalid or not recognized.
*/
public int onUnrecognizedArgument( List< String > argumentsList, int index )
throws CommandLineException;
public static ArgumentHandler DEFAULT_ARGUMENT_HANDLER =
new ArgumentHandler() {
public int onUnrecognizedArgument( List< String > argumentsList, int index )
throws CommandLineException
{
throw new CommandLineException( "Unrecognized command line option: " + argumentsList.get( index ) );
}
};
}
}