Browse Source

* Added NameValidator, to provide a reasonably flexible mechanism for

specifying/testing name validity.

* NameValidator is used by DefaultProjectBuilder for project and target
  names, and by DefaultTaskContext for Property names.

* Added ProjectException, thrown by ProjectBuilder.build().

* Tidy-up error messages for project building errors.

* Added a bunch of tests

Submitted by Darrell DeBoer [darrell@apache.org]


git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@271810 13f79535-47bb-0310-9956-ffa450edef68
master
adammurdoch 23 years ago
parent
commit
80793a20ad
19 changed files with 1057 additions and 78 deletions
  1. +12
    -0
      proposal/myrmidon/etc/testcases/org/apache/antlib/core/property.ant
  2. +4
    -0
      proposal/myrmidon/etc/testcases/org/apache/myrmidon/components/builder/bad-project-name.ant
  3. +5
    -0
      proposal/myrmidon/etc/testcases/org/apache/myrmidon/components/builder/bad-target-name.ant
  4. +133
    -69
      proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/DefaultProjectBuilder.java
  5. +5
    -1
      proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/Resources.properties
  6. +24
    -0
      proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/DefaultTaskContext.java
  7. +1
    -0
      proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/Resources.properties
  8. +2
    -2
      proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectBuilder.java
  9. +56
    -0
      proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectException.java
  10. +356
    -0
      proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/DefaultNameValidator.java
  11. +23
    -0
      proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/NameValidator.java
  12. +6
    -0
      proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/Resources.properties
  13. +23
    -0
      proposal/myrmidon/src/test/org/apache/antlib/core/PropertyTest.java
  14. +70
    -0
      proposal/myrmidon/src/test/org/apache/myrmidon/components/builder/DefaultProjectBuilderTest.java
  15. +122
    -0
      proposal/myrmidon/src/test/org/apache/myrmidon/interfaces/model/DefaultNameValidatorTest.java
  16. +23
    -0
      proposal/myrmidon/src/testcases/org/apache/antlib/core/PropertyTest.java
  17. +70
    -0
      proposal/myrmidon/src/testcases/org/apache/myrmidon/components/builder/DefaultProjectBuilderTest.java
  18. +122
    -0
      proposal/myrmidon/src/testcases/org/apache/myrmidon/interfaces/model/DefaultNameValidatorTest.java
  19. +0
    -6
      proposal/myrmidon/src/xdocs/todo.xml

+ 12
- 0
proposal/myrmidon/etc/testcases/org/apache/antlib/core/property.ant View File

@@ -50,4 +50,16 @@
<property-test-type value="value 3"/>
</property>
</target>

<!-- Test properties with invalid names -->
<target name="bad-prop-name1">
<property name="badname!" value="value"/>
</target>
<target name="bad-prop-name2">
<property name="bad name" value="value"/>
</target>
<target name="bad-prop-name3">
<property name="" value="value"/>
</target>

</project>

+ 4
- 0
proposal/myrmidon/etc/testcases/org/apache/myrmidon/components/builder/bad-project-name.ant View File

@@ -0,0 +1,4 @@
<!-- Project with an invalid name -->
<project version="2.0" name="!badname">
<target name="main"/>
</project>

+ 5
- 0
proposal/myrmidon/etc/testcases/org/apache/myrmidon/components/builder/bad-target-name.ant View File

@@ -0,0 +1,5 @@
<!-- Target with an invalid name -->
<project version="2.0" name="ok name">
<target name="main"/>
<target name="bad ^ name"/>
</project>

+ 133
- 69
proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/DefaultProjectBuilder.java View File

@@ -8,6 +8,7 @@
package org.apache.myrmidon.components.builder;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
@@ -22,11 +23,13 @@ import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.configuration.SAXConfigurationHandler;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.myrmidon.framework.conditions.Condition;
import org.apache.myrmidon.framework.conditions.AndCondition;
import org.apache.myrmidon.framework.conditions.Condition;
import org.apache.myrmidon.framework.conditions.IsSetCondition;
import org.apache.myrmidon.framework.conditions.NotCondition;
import org.apache.myrmidon.interfaces.builder.ProjectBuilder;
import org.apache.myrmidon.interfaces.builder.ProjectException;
import org.apache.myrmidon.interfaces.model.DefaultNameValidator;
import org.apache.myrmidon.interfaces.model.Project;
import org.apache.myrmidon.interfaces.model.Target;
import org.apache.myrmidon.interfaces.model.TypeLib;
@@ -37,6 +40,7 @@ import org.xml.sax.XMLReader;
*
* @author <a href="mailto:peter@apache.org">Peter Donald</a>
* @version $Revision$ $Date$
*
* @ant:type type="project-builder" name="xml"
* @ant:type type="project-builder" name="ant"
*/
@@ -54,36 +58,37 @@ public class DefaultProjectBuilder
private final static int IMPLICIT_TASKS = 2;
private final static int TARGETS = 3;

// Use a name validator with the default rules.
private DefaultNameValidator m_nameValidator = new DefaultNameValidator();

/**
* build a project from file.
*
* @param source the source
* @return the constructed Project
* @exception Exception if an error occurs
* @exception ProjectException if an error occurs
*/
public Project build( final String source )
throws Exception
throws ProjectException
{
final File file = new File( source );
return build( file, new HashMap() );
}

private Project build( final File file, final HashMap projects )
throws Exception
throws ProjectException
{
final URL systemID = file.toURL();
final URL systemID = extractURL( file );
final Project result = (Project)projects.get( systemID.toString() );
if( null != result )
{
return result;
}

final SAXConfigurationHandler handler = new SAXConfigurationHandler();

process( systemID, handler );

final Configuration configuration = handler.getConfiguration();
// Parse the project file
final Configuration configuration = parseProject( systemID );

// Build the project model
final DefaultProject project = buildProject( file, configuration );

projects.put( systemID.toString(), project );
@@ -94,20 +99,33 @@ public class DefaultProjectBuilder
return project;
}

protected void process( final URL systemID,
final SAXConfigurationHandler handler )
throws Exception
/**
* Parses the project.
*/
private Configuration parseProject( final URL systemID )
throws ProjectException
{
final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
final SAXParser saxParser = saxParserFactory.newSAXParser();
final XMLReader parser = saxParser.getXMLReader();
parser.setFeature( "http://xml.org/sax/features/namespace-prefixes", false );
parser.setFeature( "http://xml.org/sax/features/namespaces", false );
//parser.setFeature( "http://xml.org/sax/features/validation", false );

parser.setContentHandler( handler );
parser.setErrorHandler( handler );
parser.parse( systemID.toString() );
try
{
final SAXConfigurationHandler handler = new SAXConfigurationHandler();
final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
final SAXParser saxParser = saxParserFactory.newSAXParser();
final XMLReader parser = saxParser.getXMLReader();
parser.setFeature( "http://xml.org/sax/features/namespace-prefixes", false );
parser.setFeature( "http://xml.org/sax/features/namespaces", false );
//parser.setFeature( "http://xml.org/sax/features/validation", false );

parser.setContentHandler( handler );
parser.setErrorHandler( handler );
parser.parse( systemID.toString() );

return handler.getConfiguration();
}
catch( Exception e )
{
String message = REZ.getString( "ant.project-parse.error" );
throw new ProjectException( message, e );
}
}

/**
@@ -116,23 +134,20 @@ public class DefaultProjectBuilder
* @param file the file from which configuration was loaded
* @param configuration the configuration loaded
* @return the created Project
* @exception Exception if an error occurs
* @exception Exception if an error occurs
* @exception ConfigurationException if an error occurs
* @exception ProjectException if an error occurs building the project
*/
private DefaultProject buildProject( final File file,
final Configuration configuration )
throws Exception
throws ProjectException
{
if( !configuration.getName().equals( "project" ) )
{
final String message = REZ.getString( "ant.no-project-element.error" );
throw new Exception( message );
throw new ProjectException( message );
}

//get project-level attributes
final String projectName = configuration.getAttribute( "name",
FileUtil.removeExtension( file.getName() ) );
final String projectName = getProjectName( configuration, file );
final String baseDirectoryName = configuration.getAttribute( "basedir", null );
final String defaultTarget = configuration.getAttribute( "default", "main" );
final Version version = getVersion( configuration );
@@ -141,7 +156,7 @@ public class DefaultProjectBuilder
{
final String message =
REZ.getString( "ant.bad-version.error", VERSION, version );
throw new Exception( message );
throw new ProjectException( message );
}

//determine base directory for project. Use the directory containing
@@ -168,12 +183,52 @@ public class DefaultProjectBuilder
return project;
}

/**
* Get the project name from the configuration, or create a default name if none
* was supplied.
*/
private String getProjectName( final Configuration configuration, final File file )
throws ProjectException
{
String projectName = configuration.getAttribute( "name", null );

if( projectName == null )
{
// Create a name based on the file name.
String fileNameBase = FileUtil.removeExtension( file.getName() );
try
{
projectName = m_nameValidator.makeValidName( fileNameBase );
}
catch( Exception e )
{
String message = REZ.getString( "ant.project-create-name.error" );
throw new ProjectException( message, e );
}
}
else
{
// Make sure the supplied name is valid.
try
{
m_nameValidator.validate( projectName );
}
catch( Exception e )
{
String message = REZ.getString( "ant.project-bad-name.error" );
throw new ProjectException( message, e );
}
}
return projectName;

}

/**
* Retrieve the version attribute from the specified configuration element.
* Throw exceptions with meaningful errors if malformed or missing.
*/
private Version getVersion( final Configuration configuration )
throws Exception
throws ProjectException
{
try
{
@@ -183,7 +238,7 @@ public class DefaultProjectBuilder
catch( final ConfigurationException ce )
{
final String message = REZ.getString( "ant.version-missing.error" );
throw new ConfigurationException( message, ce );
throw new ProjectException( message, ce );
}
}

@@ -191,7 +246,7 @@ public class DefaultProjectBuilder
* Utility function to extract version
*/
private Version parseVersion( final String versionString )
throws Exception
throws ProjectException
{

try
@@ -202,8 +257,7 @@ public class DefaultProjectBuilder
{
final String message =
REZ.getString( "ant.malformed.version", versionString );
getLogger().warn( message );
throw new ConfigurationException( message, e );
throw new ProjectException( message, e );
}
}

@@ -212,12 +266,12 @@ public class DefaultProjectBuilder
*
* @param project the project
* @param configuration the Configuration
* @exception Exception if an error occurs
* @exception ProjectException if an error occurs
*/
private void buildTopLevelProject( final DefaultProject project,
final Configuration configuration,
final HashMap projects )
throws Exception
throws ProjectException
{
final ArrayList implicitTaskList = new ArrayList();
final Configuration[] children = configuration.getChildren();
@@ -277,7 +331,7 @@ public class DefaultProjectBuilder
{
final String message =
REZ.getString( "ant.unknown-toplevel-element.error", name, element.getLocation() );
throw new Exception( message );
throw new ProjectException( message );
}
}

@@ -291,7 +345,7 @@ public class DefaultProjectBuilder
private void buildProjectRef( final DefaultProject project,
final Configuration element,
final HashMap projects )
throws Exception
throws ProjectException
{
final String name = element.getAttribute( "name", null );
final String location = element.getAttribute( "location", null );
@@ -300,27 +354,31 @@ public class DefaultProjectBuilder
{
final String message =
REZ.getString( "ant.projectref-no-name.error", element.getLocation() );
throw new Exception( message );
throw new ProjectException( message );
}

if( !validName( name ) )
try
{
m_nameValidator.validate( name );
}
catch( Exception e )
{
final String message =
REZ.getString( "ant.projectref-bad-name.error", element.getLocation() );
throw new Exception( message );
throw new ProjectException( message, e );
}

if( null == location )
{
final String message =
REZ.getString( "ant.projectref-no-location.error", element.getLocation() );
throw new Exception( message );
throw new ProjectException( message );
}

// Build the URL of the referenced projects
final File baseDirectory = project.getBaseDirectory();
final File file = FileUtil.resolveFile( baseDirectory, location );
final String systemID = file.toURL().toString();
final String systemID = extractURL( file ).toString();

// Locate the referenced project, building it if necessary
Project other = (Project)projects.get( systemID );
@@ -333,9 +391,22 @@ public class DefaultProjectBuilder
project.addProject( name, other );
}

private URL extractURL( final File file ) throws ProjectException
{
try
{
return file.toURL();
}
catch( MalformedURLException e )
{
final String message = REZ.getString( "ant.project-unexpected.error" );
throw new ProjectException( message, e );
}
}

private void buildTypeLib( final DefaultProject project,
final Configuration element )
throws Exception
throws ProjectException
{
final String library = element.getAttribute( "library", null );
final String name = element.getAttribute( "name", null );
@@ -345,7 +416,7 @@ public class DefaultProjectBuilder
{
final String message =
REZ.getString( "ant.import-no-library.error", element.getLocation() );
throw new Exception( message );
throw new ProjectException( message );
}

if( null == name || null == type )
@@ -354,7 +425,7 @@ public class DefaultProjectBuilder
{
final String message =
REZ.getString( "ant.import-malformed.error", element.getLocation() );
throw new Exception( message );
throw new ProjectException( message );
}
}

@@ -368,14 +439,14 @@ public class DefaultProjectBuilder
* @param target the Configuration
*/
private void buildTarget( final DefaultProject project, final Configuration target )
throws Exception
throws ProjectException
{
final String name = target.getAttribute( "name", null );
final String depends = target.getAttribute( "depends", null );
final String ifCondition = target.getAttribute( "if", null );
final String unlessCondition = target.getAttribute( "unless", null );

verifyName( name, target );
verifyTargetName( name, target );

if( getLogger().isDebugEnabled() )
{
@@ -392,24 +463,30 @@ public class DefaultProjectBuilder
project.addTarget( name, defaultTarget );
}

private void verifyName( final String name, final Configuration target ) throws Exception
private void verifyTargetName( final String name, final Configuration target )
throws ProjectException
{
if( null == name )
{
final String message =
REZ.getString( "ant.target-noname.error", target.getLocation() );
throw new Exception( message );
throw new ProjectException( message );
}

if( !validName( name ) )
try
{
m_nameValidator.validate( name );
}
catch( Exception e )
{
final String message =
REZ.getString( "ant.target-bad-name.error", target.getLocation() );
throw new Exception( message );
throw new ProjectException( message, e );
}
}

private String[] buildDependsList( final String depends, final Configuration target ) throws Exception
private String[] buildDependsList( final String depends, final Configuration target )
throws ProjectException
{
String[] dependencies = null;

@@ -428,7 +505,7 @@ public class DefaultProjectBuilder
final String message = REZ.getString( "ant.target-bad-dependency.error",
target.getName(),
target.getLocation() );
throw new Exception( message );
throw new ProjectException( message );
}

if( getLogger().isDebugEnabled() )
@@ -447,7 +524,6 @@ public class DefaultProjectBuilder

private Condition buildCondition( final String ifCondition,
final String unlessCondition )
throws Exception
{
final AndCondition condition = new AndCondition();

@@ -475,16 +551,4 @@ public class DefaultProjectBuilder

return condition;
}

protected boolean validName( final String name )
{
if( -1 != name.indexOf( "->" ) )
{
return false;
}
else
{
return true;
}
}
}

+ 5
- 1
proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/Resources.properties View File

@@ -10,10 +10,14 @@ ati.attribue-unquoted.error=Expecting the value of attribute {0} to be enclosed
ant.project-banner.notice=Project {0} base directory: {1}.
ant.target-parse.notice=Parsing target: {0}.
ant.target-if.notice=Target if condition: {0}
ant.target- unless.notice=Target unless condition: {0}
ant.target-unless.notice=Target unless condition: {0}
ant.target-dependency.notice=Target dependency: {0}
ant.project-unexpected.error=Unexpected error building project.
ant.project-parse.error=Error parsing project.
ant.no-project-element.error=Project file must be enclosed in project element.
ant.unknown-toplevel-element.error=Unknown top-level element {0} at {1}.
ant.project-bad-name.error=Invalid project name.
ant.project-create-name.error=Could not create a name for this project.
ant.projectref-no-name.error=Malformed projectref without a name attribute at {0}.
ant.projectref-bad-name.error=Projectref with an invalid name attribute at {0}.
ant.projectref-no-location.error=Malformed projectref without a location attribute at {0}.


+ 24
- 0
proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/DefaultTaskContext.java View File

@@ -20,6 +20,7 @@ import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.myrmidon.api.TaskContext;
import org.apache.myrmidon.api.TaskException;
import org.apache.myrmidon.interfaces.model.DefaultNameValidator;
import org.apache.myrmidon.interfaces.workspace.PropertyResolver;

/**
@@ -34,6 +35,12 @@ public class DefaultTaskContext
private final static Resources REZ =
ResourceManager.getPackageResources( DefaultTaskContext.class );

// Property name validator allows digits, but no internal whitespace.
private static DefaultNameValidator m_propertyNameValidator = new DefaultNameValidator();
static {
m_propertyNameValidator.setAllowInternalWhitespace( false );
}

private final Map m_contextData = new Hashtable();
private final TaskContext m_parent;
private ServiceManager m_serviceManager;
@@ -199,6 +206,7 @@ public class DefaultTaskContext
public void setProperty( final String name, final Object value )
throws TaskException
{
checkPropertyName( name );
checkPropertyValid( name, value );
m_contextData.put( name, value );
}
@@ -362,6 +370,22 @@ public class DefaultTaskContext
return value;
}

/**
* Checks that the supplied property name is valid.
*/
private void checkPropertyName( final String name ) throws TaskException
{
try
{
m_propertyNameValidator.validate( name );
}
catch( Exception e )
{
String message = REZ.getString( "bad-property-name.error" );
throw new TaskException( message, e );
}
}

/**
* Make sure property is valid if it is one of the "magic" properties.
*


+ 1
- 0
proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/Resources.properties View File

@@ -13,6 +13,7 @@ exec-task.notice=Executing task {0}.
#DefaultTaskContext
unknown-prop.error=Unknown property {0}.
bad-property.error=Property {0} must have a value of type {1}.
bad-property-name.error=Invalid property name.
null-resolved-value.error=Value "{0}" resolved to null.
bad-resolve.error=Unable to resolve value "{0}".
bad-find-service.error=Could not find service "{0}".


+ 2
- 2
proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectBuilder.java View File

@@ -25,8 +25,8 @@ public interface ProjectBuilder
*
* @param source the source
* @return the constructed Project
* @exception Exception if an error occurs
* @exception ProjectException if an error occurs
*/
Project build( String source )
throws Exception;
throws ProjectException;
}

+ 56
- 0
proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectException.java View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) The Apache Software Foundation. All rights reserved.
*
* This software is published under the terms of the Apache Software License
* version 1.1, a copy of which has been included with this distribution in
* the LICENSE.txt file.
*/
package org.apache.myrmidon.interfaces.builder;

/**
* A cascading exception thrown on a problem constructing a Project model.
*
* @author <a href="mailto:darrell@apache.org">Darrell DeBoer</a>
* @version $Revision$ $Date$
*/
public class ProjectException
extends Exception
{
/**
* If this exception is cascaded, the cause of this exception.
*/
private final Throwable m_throwable;

/**
* Constructs an non-cascaded exception with a message
*
* @param message the message
*/
public ProjectException( final String message )
{
this( message, null );
}

/**
* Constructs a cascaded exception with the supplied message, which links the
* Throwable provided.
*
* @param message the message
* @param throwable the throwable that caused this exception
*/
public ProjectException( final String message, final Throwable throwable )
{
super( message );
m_throwable = throwable;
}

/**
* Retrieve root cause of the exception.
*
* @return the root cause
*/
public final Throwable getCause()
{
return m_throwable;
}
}

+ 356
- 0
proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/DefaultNameValidator.java View File

@@ -0,0 +1,356 @@
/*
* Copyright (C) The Apache Software Foundation. All rights reserved.
*
* This software is published under the terms of the Apache Software License
* version 1.1, a copy of which has been included with this distribution in
* the LICENSE.txt file.
*/
package org.apache.myrmidon.interfaces.model;

import org.apache.avalon.excalibur.i18n.ResourceManager;
import org.apache.avalon.excalibur.i18n.Resources;

/**
* Simple helper class which determines the validity of names used
* in ant projects.
*
* @author <a href="mailto:darrell@apache.org">Darrell DeBoer</a>
* @version $Revision$ $Date$
*/
public class DefaultNameValidator
implements NameValidator
{
private final static Resources REZ =
ResourceManager.getPackageResources( DefaultNameValidator.class );

/**
* Determines whether the supplied name may include surrounding whitespace.
*/
private boolean m_allowSurroundingWhitespace;

// Settings for initial characters.
private boolean m_allowInitialDigit;
private String m_additionalInitialCharacters;

// Settings for internal characters.
private boolean m_allowInternalDigits;
private boolean m_allowInternalWhitespace;
private String m_additionalInternalCharacters;

/**
* Construct a default name validator.
* Letters, digits and "_" are permitted as initial character.
* Letters, digits, whitespace and "_-." are permitted as internal characters.
* Surrounding whitespace is not permitted.
*/
public DefaultNameValidator()
{
this( false, true, "_", true, true, "_-." );
}

/**
* Contstruct a NameValidator with the specified rules.
* @param allowSurroundingWhitespace
* specified if names are trimmed before checking
* @param allowInitialDigit
* specifies if digits are permitted as intial characters
* @param additionalInitialCharacters
* extra characters to allow as initial characters.
* @param allowInternalDigits
* specifies if digits are permitted as internal characters
* @param allowInternalWhitespace
* specifies if whitespace is permitted internally in names
* @param additionalInternalCharacters
* extra characters permitted in names
*/
public DefaultNameValidator( final boolean allowSurroundingWhitespace,
final boolean allowInitialDigit,
final String additionalInitialCharacters,
final boolean allowInternalDigits,
final boolean allowInternalWhitespace,
final String additionalInternalCharacters )
{
setAllowSurroundingWhitespace( allowSurroundingWhitespace );
setAllowInitialDigit( allowInitialDigit );
setAdditionalInitialCharacters( additionalInitialCharacters );
setAllowInternalDigits( allowInternalDigits );
setAllowInternalWhitespace( allowInternalWhitespace );
setAdditionalInternalCharacters( additionalInternalCharacters );
}

/**
* Creates a valid name based on the supplied string value, removing invalid
* characters. If no valid characters are present, an exception is thrown.
* @param baseName the name used to construct the valid name
* @throws Exception if no valid name could be constructed.
*/
public String makeValidName( final String baseName ) throws Exception
{
final StringBuffer buffer = new StringBuffer( baseName );
while( buffer.length() > 0 && !isValidInitialChar( buffer.charAt( 0 ) ) )
{
buffer.delete( 0, 1 );
}
if( buffer.length() == 0 )
{
final String message = REZ.getString( "name.could-not-create.error", baseName );
throw new Exception( message );
}

for( int i = 1; i < buffer.length(); )
{
if( !isValidInternalChar( buffer.charAt( i ) ) )
{
buffer.delete( i, i + 1 );
}
else
{
i++;
}
}

return buffer.toString();
}

/**
* Validates the supplied name, failing if it is not.
* @throws Exception is the supplied name is not valid.
*/
public void validate( final String name ) throws Exception
{
String testName = name;

// If surrounding whitespace is allowed, trim it. Otherwise, check.
if( m_allowSurroundingWhitespace )
{
testName = testName.trim();
}
else
{
checkSurroundingWhitespace( testName );
}

// Zero-length name is invalid.
if( testName.length() == 0 )
{
final String message = REZ.getString( "name.zero-char-name.error" );
throw new Exception( message );
}

// Check first character.
final char initial = testName.charAt( 0 );
checkInitialCharacter( initial, testName );

// Check the rest of the characters.
for( int i = 1; i < testName.length(); i++ )
{
final char internal = testName.charAt( i );
checkInternalCharacter( internal, testName );
}
}

/**
* Checks if the supplied character is permitted as an internal character.
* @throws Exception if the character is not permitted
*/
private void checkInternalCharacter( final char internal, final String name )
throws Exception
{
if( !isValidInternalChar( internal ) )
{
final String message = REZ.getString( "name.invalid-internal-char.error",
name,
describeValidInternalChars() );
throw new Exception( message );
}
}

/**
* Checks if the supplied character is permitted as an internal character.
* @throws Exception if the character is not permitted
*/
private void checkInitialCharacter( final char initial, final String name )
throws Exception
{
if( !isValidInitialChar( initial ) )
{
final String message = REZ.getString( "name.invalid-initial-char.error",
name,
describeValidInitialChars() );
throw new Exception( message );
}
}

/**
* Checks the name for surrounding whitespace
* @throws Exception if surrounding whitespace is found
*/
private void checkSurroundingWhitespace( final String testName )
throws Exception
{
if( testName.length() == 0 )
{
return;
}

if( Character.isWhitespace( testName.charAt( 0 ) ) ||
Character.isWhitespace( testName.charAt( testName.length() - 1 ) ) )
{
final String message =
REZ.getString( "name.enclosing-whitespace.error", testName );
throw new Exception( message );
}
}

/**
* Determines if a character is allowed as the first character in a name.
* Valid characters are Letters, Digits, and defined initial characters ("_").
* @param chr the character to be assessed
* @return <code>true</code> if the character can be the first character of a name
*/
protected boolean isValidInitialChar( final char chr )
{
if( Character.isLetter( chr ) )
{
return true;
}

if( m_allowInitialDigit
&& Character.isDigit( chr ) )
{
return true;
}

if( m_additionalInitialCharacters.indexOf( chr ) != -1 )
{
return true;
}

return false;
}

/**
* Determines if a character is allowed as a non-initial character in a name.
* Valid characters are Letters, Digits, whitespace, and defined
* internal characters ("_-.").
* @param chr the character to be assessed
* @return <code>true</code> if the character can be included in a name
*/
protected boolean isValidInternalChar( final char chr )
{
if( Character.isLetter( chr ) )
{
return true;
}

if( m_allowInternalDigits
&& Character.isDigit( chr ) )
{
return true;
}

if( m_allowInternalWhitespace
&& Character.isWhitespace( chr ) )
{
return true;
}

if( m_additionalInternalCharacters.indexOf( chr ) != -1 )
{
return true;
}

return false;
}

/**
* Builds a message detailing the valid initial characters.
*/
protected String describeValidInitialChars()
{
StringBuffer validChars = new StringBuffer( "letters" );
if( m_allowInitialDigit )
{
validChars.append( ", digits" );
}
validChars.append( ", and \"" );
validChars.append( m_additionalInitialCharacters );
validChars.append( "\"" );
return validChars.toString();
}

/**
* Builds a message detailing the valid internal characters.
*/
protected String describeValidInternalChars()
{
StringBuffer validChars = new StringBuffer( "letters" );
if( m_allowInternalDigits )
{
validChars.append( ", digits" );
}
if( m_allowInternalWhitespace )
{
validChars.append( ", whitespace" );
}
validChars.append( ", and \"" );
validChars.append( m_additionalInternalCharacters );
validChars.append( "\"" );
return validChars.toString();
}

/**
* @param allowSurroundingWhitespace
* specified if names are trimmed before checking
*/
public void setAllowSurroundingWhitespace( boolean allowSurroundingWhitespace )
{
m_allowSurroundingWhitespace = allowSurroundingWhitespace;
}

/**
* @param allowInitialDigit
* specifies if digits are permitted as intial characters
*/
public void setAllowInitialDigit( boolean allowInitialDigit )
{
m_allowInitialDigit = allowInitialDigit;
}

/**
* @param additionalInitialCharacters
* extra characters to allow as initial characters.
*/
public void setAdditionalInitialCharacters( String additionalInitialCharacters )
{
m_additionalInitialCharacters = additionalInitialCharacters;
}

/**
* @param allowInternalDigits
* specifies if digits are permitted as internal characters
*/
public void setAllowInternalDigits( boolean allowInternalDigits )
{
m_allowInternalDigits = allowInternalDigits;
}

/**
* @param allowInternalWhitespace
* specifies if whitespace is permitted internally in names
*/
public void setAllowInternalWhitespace( boolean allowInternalWhitespace )
{
m_allowInternalWhitespace = allowInternalWhitespace;
}

/**
* @param additionalInternalCharacters
* extra characters permitted in names
*/
public void setAdditionalInternalCharacters( String additionalInternalCharacters )
{
m_additionalInternalCharacters = additionalInternalCharacters;
}

}

+ 23
- 0
proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/NameValidator.java View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) The Apache Software Foundation. All rights reserved.
*
* This software is published under the terms of the Apache Software License
* version 1.1, a copy of which has been included with this distribution in
* the LICENSE.txt file.
*/
package org.apache.myrmidon.interfaces.model;

/**
* Determines the validity of names used in projects.
*
* @author <a href="mailto:darrell@apache.org">Darrell DeBoer</a>
* @version $Revision$ $Date$
*/
public interface NameValidator
{
/**
* Validates the supplied name, failing if it is not.
* @throws Exception is the supplied name is not valid.
*/
void validate( String name ) throws Exception;
}

+ 6
- 0
proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/Resources.properties View File

@@ -0,0 +1,6 @@
# Name validation
name.zero-char-name.error=Name "" is invalid, as it contains no characters.
name.enclosing-whitespace.error=Name "{0}" is invalid, as it contains enclosing whitespace.
name.invalid-initial-char.error=Name "{0}" is invalid, as it begins with an illegal character. Names can start with {1}.
name.invalid-internal-char.error=Name "{0}" is invalid, as it contains an illegal character. Permitted name characters are {1}.
name.could-not-create.error=Could not valid name from "{0}".

+ 23
- 0
proposal/myrmidon/src/test/org/apache/antlib/core/PropertyTest.java View File

@@ -12,6 +12,7 @@ import org.apache.avalon.excalibur.i18n.ResourceManager;
import org.apache.avalon.excalibur.i18n.Resources;
import org.apache.myrmidon.AbstractProjectTest;
import org.apache.myrmidon.LogMessageTracker;
import org.apache.myrmidon.components.workspace.DefaultTaskContext;

/**
* Test cases for <property> task.
@@ -91,4 +92,26 @@ public class PropertyTest
executeTargetExpectError( projectFile, "too-many-values3", messages );
}

/**
* Tests basic validation of property names.
*/
public void testNameValidation() throws Exception
{
final File projectFile = getTestResource( "property.ant" );

final Resources contextResources
= ResourceManager.getPackageResources( DefaultTaskContext.class );

// Invalid names
String[] messages = new String[]
{
null,
contextResources.getString( "bad-property-name.error" ),
null
};
executeTargetExpectError( projectFile, "bad-prop-name1", messages );
executeTargetExpectError( projectFile, "bad-prop-name2", messages );
executeTargetExpectError( projectFile, "bad-prop-name3", messages );
}

}

+ 70
- 0
proposal/myrmidon/src/test/org/apache/myrmidon/components/builder/DefaultProjectBuilderTest.java View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) The Apache Software Foundation. All rights reserved.
*
* This software is published under the terms of the Apache Software License
* version 1.1, a copy of which has been included with this distribution in
* the LICENSE.txt file.
*/
package org.apache.myrmidon.components.builder;

import java.io.File;
import org.apache.avalon.excalibur.i18n.ResourceManager;
import org.apache.avalon.excalibur.i18n.Resources;
import org.apache.myrmidon.AbstractMyrmidonTest;

/**
* Test cases for {@link DefaultProjectBuilder}.
*
* @author <a href="mailto:darrell@apache.org">Darrell DeBoer</a>
* @version $Revision$ $Date$
*/
public class DefaultProjectBuilderTest
extends AbstractMyrmidonTest
{
private final static Resources REZ
= ResourceManager.getPackageResources( DefaultProjectBuilder.class );

private DefaultProjectBuilder m_builder;

public DefaultProjectBuilderTest( String name )
{
super( name );
}

protected void setUp() throws Exception
{
super.setUp();
m_builder = new DefaultProjectBuilder();
m_builder.enableLogging( createLogger() );
}

/**
* Test validation of project and target names.
*/
public void testNameValidation() throws Exception
{
// Check bad project name
final File badProjectFile = getTestResource( "bad-project-name.ant" );
try
{
m_builder.build( badProjectFile.getAbsolutePath() );
fail();
}
catch( Exception e )
{
assertSameMessage( REZ.getString( "ant.project-bad-name.error" ), e );
}

// Check bad target name
final File badTargetFile = getTestResource( "bad-target-name.ant" );
try
{
m_builder.build( badTargetFile.getAbsolutePath() );
fail();
}
catch( Exception e )
{
// TODO - check error message
}
}
}

+ 122
- 0
proposal/myrmidon/src/test/org/apache/myrmidon/interfaces/model/DefaultNameValidatorTest.java View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) The Apache Software Foundation. All rights reserved.
*
* This software is published under the terms of the Apache Software License
* version 1.1, a copy of which has been included with this distribution in
* the LICENSE.txt file.
*/
package org.apache.myrmidon.interfaces.model;

import org.apache.myrmidon.AbstractMyrmidonTest;

/**
* TestCases for {@link DefaultNameValidator}.
*
* @author <a href="mailto:darrell@apache.org">Darrell DeBoer</a>
* @version $Revision$ $Date$
*/
public class DefaultNameValidatorTest
extends AbstractMyrmidonTest
{
private DefaultNameValidator m_validator = new DefaultNameValidator();

public DefaultNameValidatorTest( String name )
{
super( name );
}

/**
* Test valid names for the default validator.
*/
public void testValidNames() throws Exception
{
testValid( "aName" );
testValid( "123456" );
testValid( "s p a ce s" );
testValid( "d-a-s-h-e-s-" );
testValid( "d.o.t.s." );
testValid( "_u_n_d_e_r_s_c_o_r_e_s" );
testValid( "a" );
testValid( "1" );
testValid( "_" );

}

/**
* Test invalid names for the default validator.
*/
public void testInvalidNames() throws Exception
{
testInvalid( "" );
testInvalid( " " );
testInvalid( " " );
testInvalid( " bad" );
testInvalid( "bad " );
testInvalid( " bad " );
testInvalid( "-dashfirst" );
testInvalid( ".dotfirst" );
testInvalid( "question?" );
}

/**
* Test that certain characters are disallowed in the default validator.
*/
public void testReservedChars() throws Exception
{
String reserved = "!@#$%^&*()+=~`{}[]|\\/?<>,:;";

for( int pos = 0; pos < reserved.length(); pos++ )
{
char chr = reserved.charAt( pos );
testReservedChar( chr );
}
}

private void testReservedChar( char chr ) throws Exception
{
String test = "a" + String.valueOf( chr );
testInvalid( test );
}

/**
* Test validation using a restrictive set of validation rules.
*/
public void testStrictNames() throws Exception
{
m_validator = new DefaultNameValidator( false, false, "", false, false, "." );

testValid( "name" );
testValid( "a" );
testValid( "yep.ok" );

testInvalid( "_nogood" );
testInvalid( "no_good" );
testInvalid( "nope1" );
testInvalid( "123" );
testInvalid( "not ok" );
}

private void testValid( String name )
{
try
{
m_validator.validate( name );
}
catch( Exception e )
{
fail( e.getMessage() );
}
}

private void testInvalid( String name )
{
try
{
m_validator.validate( name );
fail( "Name \"" + name + "\" should be invalid." );
}
catch( Exception e )
{
}
}
}

+ 23
- 0
proposal/myrmidon/src/testcases/org/apache/antlib/core/PropertyTest.java View File

@@ -12,6 +12,7 @@ import org.apache.avalon.excalibur.i18n.ResourceManager;
import org.apache.avalon.excalibur.i18n.Resources;
import org.apache.myrmidon.AbstractProjectTest;
import org.apache.myrmidon.LogMessageTracker;
import org.apache.myrmidon.components.workspace.DefaultTaskContext;

/**
* Test cases for <property> task.
@@ -91,4 +92,26 @@ public class PropertyTest
executeTargetExpectError( projectFile, "too-many-values3", messages );
}

/**
* Tests basic validation of property names.
*/
public void testNameValidation() throws Exception
{
final File projectFile = getTestResource( "property.ant" );

final Resources contextResources
= ResourceManager.getPackageResources( DefaultTaskContext.class );

// Invalid names
String[] messages = new String[]
{
null,
contextResources.getString( "bad-property-name.error" ),
null
};
executeTargetExpectError( projectFile, "bad-prop-name1", messages );
executeTargetExpectError( projectFile, "bad-prop-name2", messages );
executeTargetExpectError( projectFile, "bad-prop-name3", messages );
}

}

+ 70
- 0
proposal/myrmidon/src/testcases/org/apache/myrmidon/components/builder/DefaultProjectBuilderTest.java View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) The Apache Software Foundation. All rights reserved.
*
* This software is published under the terms of the Apache Software License
* version 1.1, a copy of which has been included with this distribution in
* the LICENSE.txt file.
*/
package org.apache.myrmidon.components.builder;

import java.io.File;
import org.apache.avalon.excalibur.i18n.ResourceManager;
import org.apache.avalon.excalibur.i18n.Resources;
import org.apache.myrmidon.AbstractMyrmidonTest;

/**
* Test cases for {@link DefaultProjectBuilder}.
*
* @author <a href="mailto:darrell@apache.org">Darrell DeBoer</a>
* @version $Revision$ $Date$
*/
public class DefaultProjectBuilderTest
extends AbstractMyrmidonTest
{
private final static Resources REZ
= ResourceManager.getPackageResources( DefaultProjectBuilder.class );

private DefaultProjectBuilder m_builder;

public DefaultProjectBuilderTest( String name )
{
super( name );
}

protected void setUp() throws Exception
{
super.setUp();
m_builder = new DefaultProjectBuilder();
m_builder.enableLogging( createLogger() );
}

/**
* Test validation of project and target names.
*/
public void testNameValidation() throws Exception
{
// Check bad project name
final File badProjectFile = getTestResource( "bad-project-name.ant" );
try
{
m_builder.build( badProjectFile.getAbsolutePath() );
fail();
}
catch( Exception e )
{
assertSameMessage( REZ.getString( "ant.project-bad-name.error" ), e );
}

// Check bad target name
final File badTargetFile = getTestResource( "bad-target-name.ant" );
try
{
m_builder.build( badTargetFile.getAbsolutePath() );
fail();
}
catch( Exception e )
{
// TODO - check error message
}
}
}

+ 122
- 0
proposal/myrmidon/src/testcases/org/apache/myrmidon/interfaces/model/DefaultNameValidatorTest.java View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) The Apache Software Foundation. All rights reserved.
*
* This software is published under the terms of the Apache Software License
* version 1.1, a copy of which has been included with this distribution in
* the LICENSE.txt file.
*/
package org.apache.myrmidon.interfaces.model;

import org.apache.myrmidon.AbstractMyrmidonTest;

/**
* TestCases for {@link DefaultNameValidator}.
*
* @author <a href="mailto:darrell@apache.org">Darrell DeBoer</a>
* @version $Revision$ $Date$
*/
public class DefaultNameValidatorTest
extends AbstractMyrmidonTest
{
private DefaultNameValidator m_validator = new DefaultNameValidator();

public DefaultNameValidatorTest( String name )
{
super( name );
}

/**
* Test valid names for the default validator.
*/
public void testValidNames() throws Exception
{
testValid( "aName" );
testValid( "123456" );
testValid( "s p a ce s" );
testValid( "d-a-s-h-e-s-" );
testValid( "d.o.t.s." );
testValid( "_u_n_d_e_r_s_c_o_r_e_s" );
testValid( "a" );
testValid( "1" );
testValid( "_" );

}

/**
* Test invalid names for the default validator.
*/
public void testInvalidNames() throws Exception
{
testInvalid( "" );
testInvalid( " " );
testInvalid( " " );
testInvalid( " bad" );
testInvalid( "bad " );
testInvalid( " bad " );
testInvalid( "-dashfirst" );
testInvalid( ".dotfirst" );
testInvalid( "question?" );
}

/**
* Test that certain characters are disallowed in the default validator.
*/
public void testReservedChars() throws Exception
{
String reserved = "!@#$%^&*()+=~`{}[]|\\/?<>,:;";

for( int pos = 0; pos < reserved.length(); pos++ )
{
char chr = reserved.charAt( pos );
testReservedChar( chr );
}
}

private void testReservedChar( char chr ) throws Exception
{
String test = "a" + String.valueOf( chr );
testInvalid( test );
}

/**
* Test validation using a restrictive set of validation rules.
*/
public void testStrictNames() throws Exception
{
m_validator = new DefaultNameValidator( false, false, "", false, false, "." );

testValid( "name" );
testValid( "a" );
testValid( "yep.ok" );

testInvalid( "_nogood" );
testInvalid( "no_good" );
testInvalid( "nope1" );
testInvalid( "123" );
testInvalid( "not ok" );
}

private void testValid( String name )
{
try
{
m_validator.validate( name );
}
catch( Exception e )
{
fail( e.getMessage() );
}
}

private void testInvalid( String name )
{
try
{
m_validator.validate( name );
fail( "Name \"" + name + "\" should be invalid." );
}
catch( Exception e )
{
}
}
}

+ 0
- 6
proposal/myrmidon/src/xdocs/todo.xml View File

@@ -209,12 +209,6 @@
<li>Fire ProjectListener events projectStarted() and projectFinished()
events on start and finish of referenced projects, adding indicator methods
to ProjectEvent.</li>
<li>Validate project and target names in DefaultProjectBuilder - reject dodgy
names like "," or "", or " ". Probably want to reject names that start or
end with white-space (though internal whitespace is probably fine). We also
want to reserve certain punctuation characters like , : ? $ [ ] { } &lt; &gt;, etc for
future use.</li>
<li>Similarly, validate property names, using the same rules.</li>
<li>Detect duplicate type names.</li>
<li>Add fully qualified type names, based on antlib name and type shorthand name.
Allow these to be used in build files in addition to the shorthand names.</li>


Loading…
Cancel
Save