To aid discussion, here is a patch to make property expansion recursive.

Notes:

   * I rejustified PropertyHelper.java in NetBeans so the diff looks more 
monstrous than it really is.
   * PropertyHelper.replacePropertiesRecursively() is substituted 
replaceProperties
        o For BC this should be done differently
   * Put the file RecursivePropertyParser in ..ant/util
   * The ant-test-expansion.xml file is a simple example with three test cases. 
The last one should fail.

--
Jack J. Woehr      # We have gone from the horse and buggy
Senior Consultant  # to the moon rocket in one lifetime, but
Purematrix, Inc.   # there has not been a corresponding moral
www.purematrix.com # growth in mankind. - Dwight D. Eisenhower
? recursive-properties-patch.txt
? .nbintdb
? src/main/.nbintdb
? src/main/org/apache/tools/ant/.nbattrs
? src/main/org/apache/tools/ant/util/RecursivePropertyParser.java
Index: src/main/org/apache/tools/ant/PropertyHelper.java
===================================================================
RCS file: 
/home/cvspublic/ant/src/main/org/apache/tools/ant/PropertyHelper.java,v
retrieving revision 1.15
diff -c -r1.15 PropertyHelper.java
*** src/main/org/apache/tools/ant/PropertyHelper.java   14 Apr 2004 15:42:06 
-0000      1.15
--- src/main/org/apache/tools/ant/PropertyHelper.java   3 Jun 2004 17:40:32 
-0000
***************
*** 20,25 ****
--- 20,26 ----
  import java.util.Hashtable;
  import java.util.Vector;
  import java.util.Enumeration;
+ import org.apache.tools.ant.util.RecursivePropertyParser;
  
  
  /* ISSUES:
***************
*** 45,57 ****
   * @since Ant 1.6
   */
  public class PropertyHelper {
! 
      private Project project;
      private PropertyHelper next;
! 
      /** Project properties map (usually String to String). */
      private Hashtable properties = new Hashtable();
! 
      /**
       * Map of "user" properties (as created in the Ant task, for example).
       * Note that these key/value pairs are also always put into the
--- 46,58 ----
   * @since Ant 1.6
   */
  public class PropertyHelper {
!     
      private Project project;
      private PropertyHelper next;
!     
      /** Project properties map (usually String to String). */
      private Hashtable properties = new Hashtable();
!     
      /**
       * Map of "user" properties (as created in the Ant task, for example).
       * Note that these key/value pairs are also always put into the
***************
*** 59,65 ****
       * Mapping is String to String.
       */
      private Hashtable userProperties = new Hashtable();
! 
      /**
       * Map of inherited "user" properties - that are those "user"
       * properties that have been created by tasks and not been set
--- 60,66 ----
       * Mapping is String to String.
       */
      private Hashtable userProperties = new Hashtable();
!     
      /**
       * Map of inherited "user" properties - that are those "user"
       * properties that have been created by tasks and not been set
***************
*** 67,81 ****
       * Mapping is String to String.
       */
      private Hashtable inheritedProperties = new Hashtable();
! 
      /**
       * Default constructor.
       */
      protected PropertyHelper() {
      }
! 
      // --------------------  Hook management  --------------------
! 
      /**
       * Set the project for which this helper is performing property resolution
       *
--- 68,82 ----
       * Mapping is String to String.
       */
      private Hashtable inheritedProperties = new Hashtable();
!     
      /**
       * Default constructor.
       */
      protected PropertyHelper() {
      }
!     
      // --------------------  Hook management  --------------------
!     
      /**
       * Set the project for which this helper is performing property resolution
       *
***************
*** 84,90 ****
      public void setProject(Project p) {
          this.project = p;
      }
! 
      /** There are 2 ways to hook into property handling:
       *  - you can replace the main PropertyHelper. The replacement is required
       * to support the same semantics (of course :-)
--- 85,91 ----
      public void setProject(Project p) {
          this.project = p;
      }
!     
      /** There are 2 ways to hook into property handling:
       *  - you can replace the main PropertyHelper. The replacement is required
       * to support the same semantics (of course :-)
***************
*** 98,104 ****
      public void setNext(PropertyHelper next) {
          this.next = next;
      }
! 
      /**
       * Get the next property helper in the chain.
       *
--- 99,105 ----
      public void setNext(PropertyHelper next) {
          this.next = next;
      }
!     
      /**
       * Get the next property helper in the chain.
       *
***************
*** 107,113 ****
      public PropertyHelper getNext() {
          return next;
      }
! 
      /**
       * Factory method to create a property processor.
       * Users can provide their own or replace it using "ant.PropertyHelper"
--- 108,114 ----
      public PropertyHelper getNext() {
          return next;
      }
!     
      /**
       * Factory method to create a property processor.
       * Users can provide their own or replace it using "ant.PropertyHelper"
***************
*** 119,139 ****
       * @return the project's property helper.
       */
      public static synchronized
!         PropertyHelper getPropertyHelper(Project project) {
          PropertyHelper helper
!             = (PropertyHelper) project.getReference("ant.PropertyHelper");
          if (helper != null) {
              return helper;
          }
          helper = new PropertyHelper();
          helper.setProject(project);
! 
          project.addReference("ant.PropertyHelper", helper);
          return helper;
      }
! 
      // --------------------  Methods to override  --------------------
! 
      /**
       * Sets a property. Any existing property of the same name
       * is overwritten, unless it is a user property. Will be called
--- 120,140 ----
       * @return the project's property helper.
       */
      public static synchronized
!     PropertyHelper getPropertyHelper(Project project) {
          PropertyHelper helper
!         = (PropertyHelper) project.getReference("ant.PropertyHelper");
          if (helper != null) {
              return helper;
          }
          helper = new PropertyHelper();
          helper.setProject(project);
!         
          project.addReference("ant.PropertyHelper", helper);
          return helper;
      }
!     
      // --------------------  Methods to override  --------------------
!     
      /**
       * Sets a property. Any existing property of the same name
       * is overwritten, unless it is a user property. Will be called
***************
*** 151,171 ****
       *    has a good reason not to).
       */
      public boolean setPropertyHook(String ns, String name,
!                                    Object value,
!                                    boolean inherited, boolean user,
!                                    boolean isNew) {
          if (getNext() != null) {
              boolean subst = getNext().setPropertyHook(ns, name, value,
!                     inherited, user, isNew);
              // If next has handled the property
              if (subst) {
                  return true;
              }
          }
! 
          return false;
      }
! 
      /** Get a property. If all hooks return null, the default
       * tables will be used.
       *
--- 152,172 ----
       *    has a good reason not to).
       */
      public boolean setPropertyHook(String ns, String name,
!     Object value,
!     boolean inherited, boolean user,
!     boolean isNew) {
          if (getNext() != null) {
              boolean subst = getNext().setPropertyHook(ns, name, value,
!             inherited, user, isNew);
              // If next has handled the property
              if (subst) {
                  return true;
              }
          }
!         
          return false;
      }
!     
      /** Get a property. If all hooks return null, the default
       * tables will be used.
       *
***************
*** 189,205 ****
              }
              return v.toString();
          }
! 
! 
          return null;
      }
! 
      // -------------------- Optional methods   --------------------
      // You can override those methods if you want to optimize or
      // do advanced things (like support a special syntax).
      // The methods do not chain - you should use them when embedding ant
      // (by replacing the main helper)
! 
      /**
       * Parses a string containing <code>${xxx}</code> style property
       * references into two lists. The first list is a collection
--- 190,206 ----
              }
              return v.toString();
          }
!         
!         
          return null;
      }
!     
      // -------------------- Optional methods   --------------------
      // You can override those methods if you want to optimize or
      // do advanced things (like support a special syntax).
      // The methods do not chain - you should use them when embedding ant
      // (by replacing the main helper)
!     
      /**
       * Parses a string containing <code>${xxx}</code> style property
       * references into two lists. The first list is a collection
***************
*** 220,230 ****
       *                           <code>}</code>
       */
      public void parsePropertyString(String value, Vector fragments,
!                                     Vector propertyRefs)
!         throws BuildException {
          parsePropertyStringDefault(value, fragments, propertyRefs);
      }
! 
      /**
       * Replaces <code>${xxx}</code> style constructions in the given value
       * with the string value of the corresponding data types.
--- 221,237 ----
       *                           <code>}</code>
       */
      public void parsePropertyString(String value, Vector fragments,
!     Vector propertyRefs)
!     throws BuildException {
          parsePropertyStringDefault(value, fragments, propertyRefs);
      }
!     
!     public String replaceProperties(String ns, String value,
!     Hashtable keys)
!     throws BuildException {
!         return replacePropertiesRecursively(ns, value, keys);
!     }
!     
      /**
       * Replaces <code>${xxx}</code> style constructions in the given value
       * with the string value of the corresponding data types.
***************
*** 242,268 ****
       * @return the original string with the properties replaced, or
       *         <code>null</code> if the original string is <code>null</code>.
       */
!     public String replaceProperties(String ns, String value,
!                                     Hashtable keys)
!             throws BuildException {
          if (value == null) {
              return null;
          }
! 
          Vector fragments = new Vector();
          Vector propertyRefs = new Vector();
          parsePropertyString(value, fragments, propertyRefs);
! 
          StringBuffer sb = new StringBuffer();
          Enumeration i = fragments.elements();
          Enumeration j = propertyRefs.elements();
! 
          while (i.hasMoreElements()) {
              String fragment = (String) i.nextElement();
              if (fragment == null) {
                  String propertyName = (String) j.nextElement();
                  Object replacement = null;
! 
                  // try to get it from the project or keys
                  // Backward compatibility
                  if (keys != null) {
--- 249,275 ----
       * @return the original string with the properties replaced, or
       *         <code>null</code> if the original string is <code>null</code>.
       */
!     public String replacePropertiesBC(String ns, String value,
!     Hashtable keys)
!     throws BuildException {
          if (value == null) {
              return null;
          }
!         
          Vector fragments = new Vector();
          Vector propertyRefs = new Vector();
          parsePropertyString(value, fragments, propertyRefs);
!         
          StringBuffer sb = new StringBuffer();
          Enumeration i = fragments.elements();
          Enumeration j = propertyRefs.elements();
!         
          while (i.hasMoreElements()) {
              String fragment = (String) i.nextElement();
              if (fragment == null) {
                  String propertyName = (String) j.nextElement();
                  Object replacement = null;
!                 
                  // try to get it from the project or keys
                  // Backward compatibility
                  if (keys != null) {
***************
*** 271,329 ****
                  if (replacement == null) {
                      replacement = getProperty(ns, propertyName);
                  }
! 
                  if (replacement == null) {
                      project.log("Property ${" + propertyName
!                             + "} has not been set", Project.MSG_VERBOSE);
                  }
                  fragment = (replacement != null)
!                         ? replacement.toString()
!                         : "${" + propertyName + "}";
              }
              sb.append(fragment);
          }
! 
          return sb.toString();
      }
! 
      // -------------------- Default implementation  --------------------
      // Methods used to support the default behavior and provide backward
      // compatibility. Some will be deprecated, you should avoid calling them.
! 
! 
      /** Default implementation of setProperty. Will be called from Project.
       *  This is the original 1.5 implementation, with calls to the hook
       *  added.
       */
      public synchronized boolean setProperty(String ns, String name,
!                                             Object value, boolean verbose) {
          // user (CLI) properties take precedence
          if (null != userProperties.get(name)) {
              if (verbose) {
                  project.log("Override ignored for user property " + name,
!                         Project.MSG_VERBOSE);
              }
              return false;
          }
! 
          boolean done = setPropertyHook(ns, name, value, false, false, false);
          if (done) {
              return true;
          }
! 
          if (null != properties.get(name) && verbose) {
              project.log("Overriding previous definition of property " + name,
!                     Project.MSG_VERBOSE);
          }
! 
          if (verbose) {
              project.log("Setting project property: " + name + " -> "
!                 + value, Project.MSG_DEBUG);
          }
          properties.put(name, value);
          return true;
      }
! 
      /**
       * Sets a property if no value currently exists. If the property
       * exists already, a message is logged and the method returns with
--- 278,403 ----
                  if (replacement == null) {
                      replacement = getProperty(ns, propertyName);
                  }
!                 
                  if (replacement == null) {
                      project.log("Property ${" + propertyName
!                     + "} has not been set", Project.MSG_VERBOSE);
                  }
                  fragment = (replacement != null)
!                 ? replacement.toString()
!                 : "${" + propertyName + "}";
              }
              sb.append(fragment);
          }
!         
          return sb.toString();
      }
!     
!     /**
!      * Replaces <code>${xxx}</code> style constructions RECURSIVELY in the 
given value
!      * with the string value of the corresponding data types. That is, this 
function
!      * keeps finding the next leftmost outermost expression and expands it 
recursively until no
!      * instantiable expressions remain.
!      *
!      *
!      * @param value The string to be scanned for property references.
!      *              May be <code>null</code>, in which case this
!      *              method returns immediately with no effect.
!      * @param keys  Mapping (String to String) of property names to their
!      *              values. If <code>null</code>, only project properties will
!      *              be used.
!      *
!      * @exception BuildException if the string contains an opening
!      *                           <code>${</code> without a closing
!      *                           <code>}</code>
!      * @return the original string with the properties replaced, or
!      *         <code>null</code> if the original string is <code>null</code>.
!      */
!     
!     public String replacePropertiesRecursively(String ns, String value,
!     Hashtable keys)
!     throws BuildException {
!         if (value == null) {
!             return null;
!         }
!         StringBuffer sb = new StringBuffer(value);
!         
!         int nextOpenerIndex = RecursivePropertyParser.nextOpener(value, 0);
!         
!         while (nextOpenerIndex != RecursivePropertyParser.NO_MARKER) { // 
More inner expressions, recurse.
!           //  /* debug */ System.out.println("Head of 
replacePropertiesRecursively while loop, nextOpenerIndex = " + nextOpenerIndex 
+ " sb=" + sb.toString());
!             int balancingCloserIndex = 
RecursivePropertyParser.balancingCloser(sb.toString(), nextOpenerIndex);
!             if (balancingCloserIndex == RecursivePropertyParser.NO_MARKER) {
!                 // ??? Should this be logged first?
!                 throw new BuildException("Unbalanced property expression: " + 
sb.substring(nextOpenerIndex).toString());
!             }
!             final String innerExpression = sb.substring(nextOpenerIndex + 
RecursivePropertyParser.OPENER_LENGTH , balancingCloserIndex);
!             
!             final String expansion = replacePropertiesRecursively(ns, 
innerExpression, keys);
!             // /* debug */ System.out.println("replacePropertiesRecursively() 
innerExpression=" + innerExpression);
!             Object replacement = null;
!             
!             // try to get it from the project or keys
!             // Backward compatibility
!             if (keys != null) {
!                 replacement = keys.get(expansion);
!             }
!             if (replacement == null) {
!                 replacement = getProperty(ns, expansion);
!             }
!             
!             if (replacement == null) {
!                 project.log("Property ${" + expansion
!                 + "} has not been set", Project.MSG_VERBOSE);
!             }
!             final String fragment = (replacement != null)
!             ? replacement.toString()
!             : "${" + expansion + "}";
!             
!             sb.replace(nextOpenerIndex, balancingCloserIndex + 
RecursivePropertyParser.CLOSER_LENGTH, fragment);
!             nextOpenerIndex = 
RecursivePropertyParser.nextOpener(sb.toString(), balancingCloserIndex+1);
!         }
!         return sb.toString();
!     }
!     
      // -------------------- Default implementation  --------------------
      // Methods used to support the default behavior and provide backward
      // compatibility. Some will be deprecated, you should avoid calling them.
!     
!     
      /** Default implementation of setProperty. Will be called from Project.
       *  This is the original 1.5 implementation, with calls to the hook
       *  added.
       */
      public synchronized boolean setProperty(String ns, String name,
!     Object value, boolean verbose) {
          // user (CLI) properties take precedence
          if (null != userProperties.get(name)) {
              if (verbose) {
                  project.log("Override ignored for user property " + name,
!                 Project.MSG_VERBOSE);
              }
              return false;
          }
!         
          boolean done = setPropertyHook(ns, name, value, false, false, false);
          if (done) {
              return true;
          }
!         
          if (null != properties.get(name) && verbose) {
              project.log("Overriding previous definition of property " + name,
!             Project.MSG_VERBOSE);
          }
!         
          if (verbose) {
              project.log("Setting project property: " + name + " -> "
!             + value, Project.MSG_DEBUG);
          }
          properties.put(name, value);
          return true;
      }
!     
      /**
       * Sets a property if no value currently exists. If the property
       * exists already, a message is logged and the method returns with
***************
*** 336,360 ****
       * @since Ant 1.6
       */
      public synchronized void setNewProperty(String ns, String name,
!                                             Object value) {
          if (null != properties.get(name)) {
              project.log("Override ignored for property " + name,
!                     Project.MSG_VERBOSE);
              return;
          }
! 
          boolean done = setPropertyHook(ns, name, value, false, false, true);
          if (done) {
              return;
          }
! 
          project.log("Setting project property: " + name + " -> "
!             + value, Project.MSG_DEBUG);
          if (name != null && value != null) {
              properties.put(name, value);
          }
      }
! 
      /**
       * Sets a user property, which cannot be overwritten by
       * set/unset property calls. Any previous value is overwritten.
--- 410,434 ----
       * @since Ant 1.6
       */
      public synchronized void setNewProperty(String ns, String name,
!     Object value) {
          if (null != properties.get(name)) {
              project.log("Override ignored for property " + name,
!             Project.MSG_VERBOSE);
              return;
          }
!         
          boolean done = setPropertyHook(ns, name, value, false, false, true);
          if (done) {
              return;
          }
!         
          project.log("Setting project property: " + name + " -> "
!         + value, Project.MSG_DEBUG);
          if (name != null && value != null) {
              properties.put(name, value);
          }
      }
!     
      /**
       * Sets a user property, which cannot be overwritten by
       * set/unset property calls. Any previous value is overwritten.
***************
*** 364,381 ****
       *              Must not be <code>null</code>.
       */
      public synchronized void setUserProperty(String ns, String name,
!                                              Object value) {
          project.log("Setting ro project property: " + name + " -> "
!             + value, Project.MSG_DEBUG);
          userProperties.put(name, value);
! 
          boolean done = setPropertyHook(ns, name, value, false, true, false);
          if (done) {
              return;
          }
          properties.put(name, value);
      }
! 
      /**
       * Sets a user property, which cannot be overwritten by set/unset
       * property calls. Any previous value is overwritten. Also marks
--- 438,455 ----
       *              Must not be <code>null</code>.
       */
      public synchronized void setUserProperty(String ns, String name,
!     Object value) {
          project.log("Setting ro project property: " + name + " -> "
!         + value, Project.MSG_DEBUG);
          userProperties.put(name, value);
!         
          boolean done = setPropertyHook(ns, name, value, false, true, false);
          if (done) {
              return;
          }
          properties.put(name, value);
      }
!     
      /**
       * Sets a user property, which cannot be overwritten by set/unset
       * property calls. Any previous value is overwritten. Also marks
***************
*** 388,409 ****
       *              Must not be <code>null</code>.
       */
      public synchronized void setInheritedProperty(String ns, String name,
!                                                   Object value) {
          inheritedProperties.put(name, value);
! 
          project.log("Setting ro project property: " + name + " -> "
!             + value, Project.MSG_DEBUG);
          userProperties.put(name, value);
! 
          boolean done = setPropertyHook(ns, name, value, true, false, false);
          if (done) {
              return;
          }
          properties.put(name, value);
      }
! 
      // -------------------- Getting properties  --------------------
! 
      /**
       * Returns the value of a property, if it is set.  You can override
       * this method in order to plug your own storage.
--- 462,483 ----
       *              Must not be <code>null</code>.
       */
      public synchronized void setInheritedProperty(String ns, String name,
!     Object value) {
          inheritedProperties.put(name, value);
!         
          project.log("Setting ro project property: " + name + " -> "
!         + value, Project.MSG_DEBUG);
          userProperties.put(name, value);
!         
          boolean done = setPropertyHook(ns, name, value, true, false, false);
          if (done) {
              return;
          }
          properties.put(name, value);
      }
!     
      // -------------------- Getting properties  --------------------
!     
      /**
       * Returns the value of a property, if it is set.  You can override
       * this method in order to plug your own storage.
***************
*** 418,429 ****
          if (name == null) {
              return null;
          }
! 
          Object o = getPropertyHook(ns, name, false);
          if (o != null) {
              return o;
          }
! 
          return properties.get(name);
      }
      /**
--- 492,503 ----
          if (name == null) {
              return null;
          }
!         
          Object o = getPropertyHook(ns, name, false);
          if (o != null) {
              return o;
          }
!         
          return properties.get(name);
      }
      /**
***************
*** 445,459 ****
          }
          return  userProperties.get(name);
      }
! 
! 
      // -------------------- Access to property tables  --------------------
      // This is used to support ant call and similar tasks. It should be
      // deprecated, it is possible to use a better (more efficient)
      // mechanism to preserve the context.
! 
      // TODO: do we need to delegate ?
! 
      /**
       * Returns a copy of the properties table.
       * @return a hashtable containing all properties
--- 519,533 ----
          }
          return  userProperties.get(name);
      }
!     
!     
      // -------------------- Access to property tables  --------------------
      // This is used to support ant call and similar tasks. It should be
      // deprecated, it is possible to use a better (more efficient)
      // mechanism to preserve the context.
!     
      // TODO: do we need to delegate ?
!     
      /**
       * Returns a copy of the properties table.
       * @return a hashtable containing all properties
***************
*** 461,497 ****
       */
      public Hashtable getProperties() {
          Hashtable propertiesCopy = new Hashtable();
! 
          Enumeration e = properties.keys();
          while (e.hasMoreElements()) {
              Object name = e.nextElement();
              Object value = properties.get(name);
              propertiesCopy.put(name, value);
          }
! 
          // There is a better way to save the context. This shouldn't
          // delegate to next, it's for backward compatibility only.
! 
          return propertiesCopy;
      }
! 
      /**
       * Returns a copy of the user property hashtable
       * @return a hashtable containing just the user properties
       */
      public Hashtable getUserProperties() {
          Hashtable propertiesCopy = new Hashtable();
! 
          Enumeration e = userProperties.keys();
          while (e.hasMoreElements()) {
              Object name = e.nextElement();
              Object value = properties.get(name);
              propertiesCopy.put(name, value);
          }
! 
          return propertiesCopy;
      }
! 
      /**
       * Copies all user properties that have not been set on the
       * command line or a GUI tool from this instance to the Project
--- 535,571 ----
       */
      public Hashtable getProperties() {
          Hashtable propertiesCopy = new Hashtable();
!         
          Enumeration e = properties.keys();
          while (e.hasMoreElements()) {
              Object name = e.nextElement();
              Object value = properties.get(name);
              propertiesCopy.put(name, value);
          }
!         
          // There is a better way to save the context. This shouldn't
          // delegate to next, it's for backward compatibility only.
!         
          return propertiesCopy;
      }
!     
      /**
       * Returns a copy of the user property hashtable
       * @return a hashtable containing just the user properties
       */
      public Hashtable getUserProperties() {
          Hashtable propertiesCopy = new Hashtable();
!         
          Enumeration e = userProperties.keys();
          while (e.hasMoreElements()) {
              Object name = e.nextElement();
              Object value = properties.get(name);
              propertiesCopy.put(name, value);
          }
!         
          return propertiesCopy;
      }
!     
      /**
       * Copies all user properties that have not been set on the
       * command line or a GUI tool from this instance to the Project
***************
*** 515,521 ****
              other.setInheritedProperty(arg, value.toString());
          }
      }
! 
      /**
       * Copies all user properties that have been set on the command
       * line or a GUI tool from this instance to the Project instance
--- 589,595 ----
              other.setInheritedProperty(arg, value.toString());
          }
      }
!     
      /**
       * Copies all user properties that have been set on the command
       * line or a GUI tool from this instance to the Project instance
***************
*** 539,561 ****
              other.setUserProperty(arg.toString(), value.toString());
          }
      }
! 
      // -------------------- Property parsing  --------------------
      // Moved from ProjectHelper. You can override the static method -
      // this is used for backward compatibility (for code that calls
      // the parse method in ProjectHelper).
! 
      /** Default parsing method. It is here only to support backward 
compatibility
       * for the static ProjectHelper.parsePropertyString().
       */
      static void parsePropertyStringDefault(String value, Vector fragments,
!                                     Vector propertyRefs)
!         throws BuildException {
          int prev = 0;
          int pos;
          //search for the next instance of $ from the 'prev' position
          while ((pos = value.indexOf("$", prev)) >= 0) {
! 
              //if there was any text before this, add it as a fragment
              //TODO, this check could be modified to go if pos>prev;
              //seems like this current version could stick empty strings
--- 613,635 ----
              other.setUserProperty(arg.toString(), value.toString());
          }
      }
!     
      // -------------------- Property parsing  --------------------
      // Moved from ProjectHelper. You can override the static method -
      // this is used for backward compatibility (for code that calls
      // the parse method in ProjectHelper).
!     
      /** Default parsing method. It is here only to support backward 
compatibility
       * for the static ProjectHelper.parsePropertyString().
       */
      static void parsePropertyStringDefault(String value, Vector fragments,
!     Vector propertyRefs)
!     throws BuildException {
          int prev = 0;
          int pos;
          //search for the next instance of $ from the 'prev' position
          while ((pos = value.indexOf("$", prev)) >= 0) {
!             
              //if there was any text before this, add it as a fragment
              //TODO, this check could be modified to go if pos>prev;
              //seems like this current version could stick empty strings
***************
*** 574,580 ****
                  /*
                  fragments.addElement(value.substring(pos + 1, pos + 2));
                  prev = pos + 2;
!                 */
                  if (value.charAt(pos + 1) == '$') {
                      //backwards compatibility two $ map to one mode
                      fragments.addElement("$");
--- 648,654 ----
                  /*
                  fragments.addElement(value.substring(pos + 1, pos + 2));
                  prev = pos + 2;
!                  */
                  if (value.charAt(pos + 1) == '$') {
                      //backwards compatibility two $ map to one mode
                      fragments.addElement("$");
***************
*** 584,596 ****
                      fragments.addElement(value.substring(pos, pos + 2));
                      prev = pos + 2;
                  }
! 
              } else {
                  //property found, extract its name or bail on a typo
                  int endName = value.indexOf('}', pos);
                  if (endName < 0) {
                      throw new BuildException("Syntax error in property: "
!                                                  + value);
                  }
                  String propertyName = value.substring(pos + 2, endName);
                  fragments.addElement(null);
--- 658,670 ----
                      fragments.addElement(value.substring(pos, pos + 2));
                      prev = pos + 2;
                  }
!                 
              } else {
                  //property found, extract its name or bail on a typo
                  int endName = value.indexOf('}', pos);
                  if (endName < 0) {
                      throw new BuildException("Syntax error in property: "
!                     + value);
                  }
                  String propertyName = value.substring(pos + 2, endName);
                  fragments.addElement(null);
***************
*** 604,608 ****
              fragments.addElement(value.substring(prev));
          }
      }
! 
  }
--- 678,682 ----
              fragments.addElement(value.substring(prev));
          }
      }
!     
  }

/*
 * RecursivePropertyParser.java
 *
 * Created on June 2, 2004, 3:29 PM
 */

package org.apache.tools.ant.util;

/**
 * Class of static methods to parse property expressions
 * @author  jax
 */
public class RecursivePropertyParser {
    
    /** No marker found in a string search */
    public final static int NO_MARKER = -1;
    public final static String OPENER = "${";
    public final static String CLOSER = "}";
    public final static int OPENER_LENGTH = OPENER.length();
    public final static int CLOSER_LENGTH = CLOSER.length();
    
    /** Never creates a new instance of RecursivePropertyParser */
    protected RecursivePropertyParser() {
    }
    
    /**
     * Find the next expression opener
     *
     * @param value The source string
     * @return The index of the start of the opener, NO_MARKER if not found
     */
    public static int nextOpener(final String value, int index) {
        int result = NO_MARKER;
        if (value != null && !(index > value.length())) {
            result = value.indexOf(OPENER, index);
        }
        return result;
    }
    
    /**
     * Find the next expression closer
     *
     * @param index offsetr in the source string from which to start looking 
for a closer
     * @param value The source string
     * @return The index of the closer, NO_MARKER if not found
     */
    public static int nextCloser(final String value, int index) {
        int result = NO_MARKER;
        
        if (value != null && !(index > value.length())) {
            result = value.indexOf(CLOSER, index);
            // /* debug */ System.out.println("nextCloser: result = " + result);
        }
        return result;
    }
    
    /**
     * Finds the balancing expression closer.
     *
     * @param index offset in the source string from which to start looking for 
a closer
     * @param value The source string
     * @return The index of the closer, NO_MARKER if not found
     */
    public static int balancingCloser(final String value, int index) {
        int result = NO_MARKER;
        int lastOpener = index;
        if (value != null) {
            int unbalancedOpeners = 1;
            int currentOpener = nextOpener(value, lastOpener+1);
            int currentCloser = nextCloser(value, lastOpener);
            
            while (unbalancedOpeners > 0) {
                // /* debug */ System.out.println("balancingCloser(): 
unbalancedOpeners=" + unbalancedOpeners +"currentOpener=" + currentOpener + " 
currentCloser=" + currentCloser);
                
                // We can't find a matching closing '}'
                if (currentCloser == NO_MARKER) {
                    break; // doom
                }
                
                // We found another '${' before finding a '}'
                if (currentOpener != NO_MARKER && currentOpener < 
currentCloser) {
                    unbalancedOpeners++;
                    lastOpener = currentOpener;
                    currentOpener = nextOpener(value, lastOpener+1);
                }
                
                // We found a closer '}' before any new openers '${'
                else {
                    unbalancedOpeners--;
                    
                    if (unbalancedOpeners == 0) { // We're done if we are 
balanced
                        result = currentCloser;
                    }
                    
                    else { // We have to go round again trying to find closers 
to balance
                        currentCloser=nextCloser(value, currentCloser+1);
                    }
                }
            }
        }
        return result;
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="all" name="changeme">
    <target name="all">
        <property name="a" value="arf"/>
        <property name="b" value="bark"/>
        <property name="arf.bark" value="woof woof woof"/>
        <echo message="The expansion comes out to ${${a}.${b}}"/>
        <echo message="The expansion comes out to ${${a}.${${a}.${b}}}"/>
        <!-- this one will fail with a build exception -->
        <echo message="${${a}.${b}${"/>
    </target>
</project>


---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to