diff --git a/proposal/myrmidon/etc/testcases/org/apache/antlib/core/property.ant b/proposal/myrmidon/etc/testcases/org/apache/antlib/core/property.ant index 8dc4321a1..03cad74a0 100644 --- a/proposal/myrmidon/etc/testcases/org/apache/antlib/core/property.ant +++ b/proposal/myrmidon/etc/testcases/org/apache/antlib/core/property.ant @@ -50,4 +50,16 @@ + + + + + + + + + + + + diff --git a/proposal/myrmidon/etc/testcases/org/apache/myrmidon/components/builder/bad-project-name.ant b/proposal/myrmidon/etc/testcases/org/apache/myrmidon/components/builder/bad-project-name.ant new file mode 100644 index 000000000..7b0ed6f75 --- /dev/null +++ b/proposal/myrmidon/etc/testcases/org/apache/myrmidon/components/builder/bad-project-name.ant @@ -0,0 +1,4 @@ + + + + diff --git a/proposal/myrmidon/etc/testcases/org/apache/myrmidon/components/builder/bad-target-name.ant b/proposal/myrmidon/etc/testcases/org/apache/myrmidon/components/builder/bad-target-name.ant new file mode 100644 index 000000000..8c7186956 --- /dev/null +++ b/proposal/myrmidon/etc/testcases/org/apache/myrmidon/components/builder/bad-target-name.ant @@ -0,0 +1,5 @@ + + + + + diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/DefaultProjectBuilder.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/DefaultProjectBuilder.java index 5d0ff6419..22668c3a8 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/DefaultProjectBuilder.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/DefaultProjectBuilder.java @@ -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 Peter Donald * @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; - } - } } diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/Resources.properties b/proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/Resources.properties index 80cc7f97e..e1dc50fe1 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/Resources.properties +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/builder/Resources.properties @@ -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}. diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/DefaultTaskContext.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/DefaultTaskContext.java index 5224a34aa..4c153fa12 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/DefaultTaskContext.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/DefaultTaskContext.java @@ -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. * diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/Resources.properties b/proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/Resources.properties index c2db45564..e28947723 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/Resources.properties +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/workspace/Resources.properties @@ -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}". diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectBuilder.java b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectBuilder.java index 9fa3bf6c8..0c4a4df38 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectBuilder.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectBuilder.java @@ -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; } diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectException.java b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectException.java new file mode 100644 index 000000000..86b9dc5d9 --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/builder/ProjectException.java @@ -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 Darrell DeBoer + * @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; + } +} diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/DefaultNameValidator.java b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/DefaultNameValidator.java new file mode 100644 index 000000000..eef258c85 --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/DefaultNameValidator.java @@ -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 Darrell DeBoer + * @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 true 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 true 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; + } + +} diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/NameValidator.java b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/NameValidator.java new file mode 100644 index 000000000..c6f71af51 --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/NameValidator.java @@ -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 Darrell DeBoer + * @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; +} diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/Resources.properties b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/Resources.properties new file mode 100644 index 000000000..e0d56cdf6 --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/model/Resources.properties @@ -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}". diff --git a/proposal/myrmidon/src/test/org/apache/antlib/core/PropertyTest.java b/proposal/myrmidon/src/test/org/apache/antlib/core/PropertyTest.java index 7158acef7..b53426441 100644 --- a/proposal/myrmidon/src/test/org/apache/antlib/core/PropertyTest.java +++ b/proposal/myrmidon/src/test/org/apache/antlib/core/PropertyTest.java @@ -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 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 ); + } + } diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/builder/DefaultProjectBuilderTest.java b/proposal/myrmidon/src/test/org/apache/myrmidon/components/builder/DefaultProjectBuilderTest.java new file mode 100644 index 000000000..dfbcbcd94 --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/builder/DefaultProjectBuilderTest.java @@ -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 Darrell DeBoer + * @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 + } + } +} diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/interfaces/model/DefaultNameValidatorTest.java b/proposal/myrmidon/src/test/org/apache/myrmidon/interfaces/model/DefaultNameValidatorTest.java new file mode 100644 index 000000000..51d9710ec --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/interfaces/model/DefaultNameValidatorTest.java @@ -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 Darrell DeBoer + * @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 ) + { + } + } +} diff --git a/proposal/myrmidon/src/testcases/org/apache/antlib/core/PropertyTest.java b/proposal/myrmidon/src/testcases/org/apache/antlib/core/PropertyTest.java index 7158acef7..b53426441 100644 --- a/proposal/myrmidon/src/testcases/org/apache/antlib/core/PropertyTest.java +++ b/proposal/myrmidon/src/testcases/org/apache/antlib/core/PropertyTest.java @@ -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 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 ); + } + } diff --git a/proposal/myrmidon/src/testcases/org/apache/myrmidon/components/builder/DefaultProjectBuilderTest.java b/proposal/myrmidon/src/testcases/org/apache/myrmidon/components/builder/DefaultProjectBuilderTest.java new file mode 100644 index 000000000..dfbcbcd94 --- /dev/null +++ b/proposal/myrmidon/src/testcases/org/apache/myrmidon/components/builder/DefaultProjectBuilderTest.java @@ -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 Darrell DeBoer + * @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 + } + } +} diff --git a/proposal/myrmidon/src/testcases/org/apache/myrmidon/interfaces/model/DefaultNameValidatorTest.java b/proposal/myrmidon/src/testcases/org/apache/myrmidon/interfaces/model/DefaultNameValidatorTest.java new file mode 100644 index 000000000..51d9710ec --- /dev/null +++ b/proposal/myrmidon/src/testcases/org/apache/myrmidon/interfaces/model/DefaultNameValidatorTest.java @@ -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 Darrell DeBoer + * @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 ) + { + } + } +} diff --git a/proposal/myrmidon/src/xdocs/todo.xml b/proposal/myrmidon/src/xdocs/todo.xml index d77ea4d34..620acfb97 100644 --- a/proposal/myrmidon/src/xdocs/todo.xml +++ b/proposal/myrmidon/src/xdocs/todo.xml @@ -209,12 +209,6 @@
  • Fire ProjectListener events projectStarted() and projectFinished() events on start and finish of referenced projects, adding indicator methods to ProjectEvent.
  • -
  • 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 , : ? $ [ ] { } < >, etc for - future use.
  • -
  • Similarly, validate property names, using the same rules.
  • Detect duplicate type names.
  • 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.