Re: Creating custom propertis plugin

Daniel Tihelka <[email protected]> Tue, 22 Nov 2011 08:28:57 +0100
Newsgroups gmane.comp.java.cruise-control.devel
Message-ID <[email protected]>
Hallo,
it seems that I made some progress with the possibility to write 3rd party 
custom properties plugins.

I have attached props_plugin_01.patch which contains changes necessary for 
correct registration of classes implementing PropertiesPlugin interface. I 
have made few tests and it seems to be working.

Also, if you are interested, I have attached the second patch containing the 
same changes as the first one, plus RegexProperties class (and a test for it) 
which I plan to use for our builds. It's up to you, if you want to have this 
class officially in CC sources - if so, I will write documentation for it, if 
not, I am OK keeping it in our 3rd party CC extensions library.

So, I would like to ask any of you to have a look at the proposed changes 
(especially those in patch 01). I tried the existing unit tests and nothing 
seems to be broken, but I would like to have your acknowledge before pushing 
the patch in.

Thank you very much. Best regards,
Dan T. (CC committer)

------------------------------------------------------------------------------
All the data continuously generated in your IT infrastructure 
contains a definitive record of customers, application performance, 
security threats, fraudulent activity, and more. Splunk takes this 
data and makes sense of it. IT sense. And common sense.
http://p.sf.net/sfu/splunk-novd2d

_______________________________________________
Cruisecontrol-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/cruisecontrol-devel
props_plugin_01.patch (text/x-patch, 2.3 KB)
Index: src/net/sourceforge/cruisecontrol/CruiseControlConfig.java
===================================================================
--- src/net/sourceforge/cruisecontrol/CruiseControlConfig.java	(revision 4601)
+++ src/net/sourceforge/cruisecontrol/CruiseControlConfig.java	(working copy)
@@ -408,6 +408,18 @@
             ProjectXMLHelper.registerProperty(nonFullyResolvedProjectProperties, propertyElement,
                     resolvers, FAIL_UPON_MISSING_PROPERTY);
         }
+        // And custom properties plugins
+        for (final Object o : projectElement.getChildren()) {
+            final Element childElement = (Element) o;
+            final String nodeName = childElement.getName();
+            if (KNOWN_ROOT_CHILD_NAMES.contains(nodeName)) {
+                continue;
+            }
+            if (isCustomPropertiesPlugin(nodeName)) {
+                ProjectXMLHelper.registerCustomProperty(nonFullyResolvedProjectProperties, childElement, 
+                    resolvers, FAIL_UPON_MISSING_PROPERTY, PluginRegistry.createRegistry(rootPlugins));
+            }
+        }
 
         // add the resolved rootProperties to the project's properties
         final Map<String, String> thisProperties = nonFullyResolvedProjectProperties.thisMap;
Index: src/net/sourceforge/cruisecontrol/ProjectConfig.java
===================================================================
--- src/net/sourceforge/cruisecontrol/ProjectConfig.java	(revision 4601)
+++ src/net/sourceforge/cruisecontrol/ProjectConfig.java	(working copy)
@@ -49,6 +49,7 @@
 
 import net.sourceforge.cruisecontrol.config.DefaultPropertiesPlugin;
 import net.sourceforge.cruisecontrol.config.PluginPlugin;
+import net.sourceforge.cruisecontrol.config.PropertiesPlugin;
 import net.sourceforge.cruisecontrol.gendoc.annotations.Cardinality;
 import net.sourceforge.cruisecontrol.gendoc.annotations.Default;
 import net.sourceforge.cruisecontrol.gendoc.annotations.Optional;
@@ -156,6 +157,11 @@
         this.forceBuildNewProject = forceBuildNewProject;
     }
 
+    @Description("Registers a general plug-in inherrited from PropertiesPlugin interface.")
+    public void add(PropertiesPlugin plugin) {
+        // Must be empty, plugin is registered somewhere else
+    }
+
     /**
      * @deprecated exists only for gendoc, should not be called.
      */
props_plugin_02.patch (text/x-patch, 30 KB)
Index: test/net/sourceforge/cruisecontrol/config/RegexPropertiesTest.java
===================================================================
--- test/net/sourceforge/cruisecontrol/config/RegexPropertiesTest.java	(revision 0)
+++ test/net/sourceforge/cruisecontrol/config/RegexPropertiesTest.java	(revision 0)
@@ -0,0 +1,389 @@
+/********************************************************************************
+ *
+ * CruiseControl, a Continuous Integration Toolkit
+ * Copyright (c) 2003, ThoughtWorks, Inc.
+ * 200 E. Randolph, 25th Floor
+ * Chicago, IL 60601 USA
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *     + Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *     + Redistributions in binary form must reproduce the above
+ *       copyright notice, this list of conditions and the following
+ *       disclaimer in the documentation and/or other materials provided
+ *       with the distribution.
+ *
+ *     + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
+ *       names of its contributors may be used to endorse or promote
+ *       products derived from this software without specific prior
+ *       written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ ********************************************************************************/
+package net.sourceforge.cruisecontrol.config;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import net.sourceforge.cruisecontrol.CruiseControlException;
+import net.sourceforge.cruisecontrol.config.FileResolver;
+import net.sourceforge.cruisecontrol.config.XMLConfigManager;
+import net.sourceforge.cruisecontrol.testutil.TestUtil.FilesToDelete;
+import net.sourceforge.cruisecontrol.util.IO;
+
+
+/**
+ * The test case for {@link RegexProperties} class.
+ */
+public final class RegexPropertiesTest extends TestCase
+{
+    /** The properties to parse - the reference Map which must be achieved. */
+    private final Map<String, String> propsRefer;
+    /** The pattern of properties, see {@link RegexProperties#setPattern(String)}.
+     *  - the name is a sequence of letters followed by a sequence of numbers
+     *  - the value is anything except [ and ] characters */
+    private final String propPattern = "([a-zA-Z]+\\d+)\\[([^\\[\\]]*)\\]";
+    /** The template of property name, see {@link RegexProperties#setName(String)}. */
+    private final String propNameTempl = "\\1";
+    /** The template of property value, see {@link RegexProperties#setValue(String)}. */
+    private final String propValTempl = "\\2";
+
+    
+    /** The list of files created during the test - they are deleted by {@link #tearDown()} 
+     *  method ... */
+    private final FilesToDelete filesToDel;
+    
+    /** String which will be replaced by a property in the defined format */
+    private final String PROP_HERE = "XXXXXXXXX";
+    
+    /**
+     * Constructor.
+     */
+    public RegexPropertiesTest() {
+        
+        filesToDel = new FilesToDelete();
+        propsRefer = new HashMap<String, String>();
+
+        /* Fill the reference properties */
+        propsRefer.put("name1", "value1");
+        propsRefer.put("name2", "the second value");
+        propsRefer.put("name3", "value/third");
+        propsRefer.put("name4", "");
+        propsRefer.put("name5", "qwertyuiop1234567890");
+    }
+    
+    /**
+     * Setup test environment.
+     */
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+    }    
+    /**
+     * Clears test environment.
+     */
+    @Override
+    protected void tearDown() throws Exception { 
+        super.tearDown();
+        filesToDel.delete();
+    }    
+    
+
+    /**
+     * Checks the validation when file to parse is not set (it is required item).
+     */
+    public void testValidate_noFile() {
+        final RegexProperties propsParser = new RegexProperties();
+
+        /* setFile() is not called ... */
+        propsParser.setPattern(propPattern);
+        propsParser.setName(propNameTempl);
+        propsParser.setValue(propValTempl);
+    
+        /* Must not be validated */
+        try {
+            propsParser.validate();
+            fail("RegexProperties  should throw an exception when required 'file' attribute is not set.");
+            
+        } catch (CruiseControlException e) {
+            assertEquals("exception message when the required file attribute is not set",
+                    "'file' is required for RegexProperties", e.getMessage());
+        }
+    }
+    
+    /**
+     * Checks the validation when regular expression pattern is not set (it is required item).
+     * @throws IOException 
+     */
+    public void testValidate_noPattern() throws IOException {
+        final RegexProperties propsParser = new RegexProperties();
+
+        /* does not matter that the file is empty */
+        propsParser.setFile(getFile("foo", ".txt").getAbsolutePath()); 
+        /* setPattern() is not called ... */
+        propsParser.setName(propNameTempl);
+        propsParser.setValue(propValTempl);
+    
+        /* Must not be validated */
+        try {
+            propsParser.validate();
+            fail("RegexProperties  should throw an exception when required 'pattern' attribute is not set.");
+            
+        } catch (CruiseControlException e) {
+            assertEquals("exception message when the required pattern attribute is not set",
+                    "'pattern' is required for RegexProperties", e.getMessage());
+        }
+    }
+    
+    /**
+     * Checks the validation when regular expression pattern is invalid.
+     * @throws IOException 
+     */
+    public void testValidate_badPattern() throws IOException {
+        final RegexProperties propsParser = new RegexProperties();
+        final String invalidPattern = "([a-zA-Z]+\\d+\\[(\\S*)\\]"; /* one closing group bracket is 
+                                                                       missing. */
+        /* does not matter that the file is empty */
+        propsParser.setFile(getFile("foo", ".txt").getAbsolutePath());
+        propsParser.setPattern(invalidPattern);
+        propsParser.setName(propNameTempl);
+        propsParser.setValue(propValTempl);
+    
+        /* Must not be validated */
+        try {
+            propsParser.validate();
+            fail("RegexProperties  should throw an exception when required 'pattern' attribute is not set.");
+            
+        } catch (CruiseControlException e) {
+            assertEquals("pattern '" + invalidPattern + "' is not valid", e.getMessage());
+        }
+    }
+    
+    /**
+     * Checks the validation when name template is not set (it is required item).
+     * @throws IOException 
+     */
+    public void testValidate_noName() throws IOException {
+        final RegexProperties propsParser = new RegexProperties();
+
+        /* does not matter that the file is empty */
+        propsParser.setFile(getFile("foo", ".txt").getAbsolutePath());
+        propsParser.setPattern(propPattern);
+        /* setName() is not called ... */
+        propsParser.setValue(propValTempl);
+    
+        /* Must not be validated */
+        try {
+            propsParser.validate();
+            fail("RegexProperties  should throw an exception when required 'pattern' attribute is not set.");
+            
+        } catch (CruiseControlException e) {
+            assertEquals("exception message when the required name attribute is not set",
+                    "'name' is required for RegexProperties", e.getMessage());
+        }
+    }
+
+    /**
+     * Checks the validation when value template is not set (it is required item).
+     * @throws IOException 
+     */
+    public void testValidate_noValue() throws IOException {
+        final RegexProperties propsParser = new RegexProperties();
+
+        /* does not matter that the file is empty */
+        propsParser.setFile(getFile("foo", ".txt").getAbsolutePath());
+        propsParser.setPattern(propPattern);
+        propsParser.setName(propNameTempl);
+        /* setValue() is not called ... */
+    
+        /* Must not be validated */
+        try {
+            propsParser.validate();
+            fail("RegexProperties  should throw an exception when required 'pattern' attribute is not set.");
+            
+        } catch (CruiseControlException e) {
+            assertEquals("exception message when the required value attribute is not set",
+                    "'value' is required for RegexProperties", e.getMessage());
+        }
+    }
+
+    /**
+     * Checks the correct function of the properties parsing. Each property is stored on its own
+     * line. 
+     * 
+     * @throws CruiseControlException if the parsing fails! 
+     * @throws IOException if the file from the properties are parsed cannot be created. 
+     */
+    public void testLoadProperties_correctSingleLine() throws CruiseControlException, IOException {
+        final File file = getFile("foo", ".txt");
+        final RegexProperties propsParser = new RegexProperties();
+        final Map<String, String> propsRead = new HashMap<String, String>();
+
+        /* Create the content of the file, 'XXXXXXX' strings will be replaced by the properties */
+        final String[] fileTemplate = {"sdffsdfgsf s gsgsfhs fhgsfh dsfadf", 
+                                       "dsffad s dgf sdgstrsd",
+                                       PROP_HERE,
+                                       "dsf d f ds g dsgf sg s fgsfdgdf zd",
+                                       "",
+                                       "",
+                                       PROP_HERE + " dsf sdfsadfasdf<sdf df",
+                                       PROP_HERE,
+                                       "",
+                                       "dfd fdfadsfa dsfasdfasf",
+                                       "    " + PROP_HERE,
+                                       "dsf " + PROP_HERE + "dsfasdf fzf  ",
+                                       "f s fgsg sfgsfhgsgfh dfad fdfadf d",
+                                       };
+        // Write properties to file
+        writeFile(file, fileTemplate);
+        /* Fill the properties options */
+        propsParser.setFileResolver(new FileResolver.DummyResolver());
+        propsParser.setFile(file.getAbsolutePath());
+        propsParser.setPattern(propPattern);
+        propsParser.setName(propNameTempl);
+        propsParser.setValue(propValTempl);
+        /* Validate and run */
+        propsParser.validate();
+        propsParser.loadProperties(propsRead, false);
+        
+        /* Check properties read to those required */
+        assertEquals(propsRefer, propsRead);
+    }
+    
+    /**
+     * Checks the correct function of the properties parsing. There are several properties stored 
+     * on one line.
+     * 
+     * @throws CruiseControlException 
+     * @throws IOException 
+     */
+    public void testLoadProperties_correctMultiLine() throws CruiseControlException, IOException {
+        final File file = getFile("foo", ".txt");
+        final RegexProperties propsParser = new RegexProperties();
+        final Map<String, String> propsRead = new HashMap<String, String>();
+
+        /* Create the content of the file, 'XXXXXXX' strings will be replaced by the properties */
+        final String[] fileTemplate = {"sdffsdfgsf s gsgsfhs   dsfadf g", 
+                                       PROP_HERE + " dsfsd " + PROP_HERE,
+                                       "dfd fdfadsfa dsfasdfd fdgadfgdf",
+                                       "f s fgsg sfgsfhgsgfhdf fdfadf d",
+                                       PROP_HERE + PROP_HERE + PROP_HERE,
+                                       "sdsdf adsf adsfasdf fadsfasdasd",
+                                       };
+        // Write properties to file
+        writeFile(file, fileTemplate);
+        /* Fill the properties options */
+        propsParser.setFileResolver(new FileResolver.DummyResolver());
+        propsParser.setFile(file.getAbsolutePath());
+        propsParser.setPattern(propPattern);
+        propsParser.setName(propNameTempl);
+        propsParser.setValue(propValTempl);
+        /* Validate and run */
+        propsParser.validate();
+        propsParser.loadProperties(propsRead, false);
+        
+        /* Check properties read to those required */
+        assertEquals(propsRefer, propsRead);
+    }
+
+    /**
+     * Checks if the properties are reloaded when the content of file is changed.
+     * @throws CruiseControlException
+     * @throws IOException
+     */
+    public void testShouldDetectChangesToPropertyFile() throws CruiseControlException, IOException {
+        // properties file
+        File propertyFile = getFile("foo", ".txt");
+        writeFile(propertyFile, new String[]{PROP_HERE + " some other string",
+                                             PROP_HERE + " another string" });
+        // project file
+        File projectFile = getFile("config", ".xml");
+        IO.write(projectFile, "<cruisecontrol>"
+                             + "  <plugin name='regexproperties' classname='" + RegexProperties.class.getName() + "' />"
+                            + "  <project name='DOESNTMATTER'>"
+                            + "    <regexproperties file='" + propertyFile.getName() + "'"
+                            + "       pattern='" + propPattern + "'"
+                            + "       name='" + propNameTempl + "' value='" + propValTempl + "' />"
+                            + "    <schedule> <ant/> </schedule>"
+                            + "  </project>"
+                            + "</cruisecontrol>");
+
+        // Must create the whole config
+        XMLConfigManager config = new XMLConfigManager(projectFile); 
+        assertFalse(config.reloadIfNecessary());
+
+        // Change the properties file
+        writeFile(propertyFile, new String[]{PROP_HERE + " " + PROP_HERE});
+
+        assertTrue(config.reloadIfNecessary());
+        assertFalse(config.reloadIfNecessary());
+    }    
+    
+
+    /**
+     * From the template of file to parse and the properties defined in {@link #propsRefer}
+     * creates a real file. Each occurrence of {@link #PROP_HERE} in the file template is
+     * replaced by one property from {@link #propsRefer}. 
+     * 
+     * The properties are stored in the form: <code>name[value]</code> (see {@link #propPattern}).
+     *
+     * @param file the file to write data into.
+     * @param fileTemplate the array of strings, each item gives one line.
+     * @throws CruiseControlException
+     */
+    private void writeFile(final File file, final String[] fileTemplate) throws CruiseControlException
+    {
+        final StringBuffer fileData = new StringBuffer();
+        final Iterator<String> propNames = this.propsRefer.keySet().iterator();
+
+        /* Create the content of file according to the template */
+        for (String line : fileTemplate) {
+             while (line.indexOf(PROP_HERE) >= 0 && propNames.hasNext()) {
+                 String name = propNames.next();
+                 line = line.replaceFirst(PROP_HERE, name + "[" + propsRefer.get(name) + "]");
+             }
+             fileData.append(line);
+             fileData.append('\n');
+        }
+        /* write to file */
+        IO.write(file, fileData.toString());
+    }
+    
+    /** 
+     * Generates temporary file and stores it into {@link #files} array. The file is deleted 
+     * by {@link #tearDown()} method.
+     * 
+     * @throws IOException when the file cannot be created
+     * @todo move to FilesToDelete 
+     */
+    private File getFile(String prefix, String suffix) throws IOException {
+        File file;
+        
+        // create the file and register it for deletition
+        filesToDel.add(file = File.createTempFile(prefix, suffix));
+        return file;
+    }
+    
+    
+}
Index: src/net/sourceforge/cruisecontrol/CruiseControlConfig.java
===================================================================
--- src/net/sourceforge/cruisecontrol/CruiseControlConfig.java	(revision 4601)
+++ src/net/sourceforge/cruisecontrol/CruiseControlConfig.java	(working copy)
@@ -408,6 +408,18 @@
             ProjectXMLHelper.registerProperty(nonFullyResolvedProjectProperties, propertyElement,
                     resolvers, FAIL_UPON_MISSING_PROPERTY);
         }
+        // And custom properties plugins
+        for (final Object o : projectElement.getChildren()) {
+            final Element childElement = (Element) o;
+            final String nodeName = childElement.getName();
+            if (KNOWN_ROOT_CHILD_NAMES.contains(nodeName)) {
+                continue;
+            }
+            if (isCustomPropertiesPlugin(nodeName)) {
+                ProjectXMLHelper.registerCustomProperty(nonFullyResolvedProjectProperties, childElement, 
+                    resolvers, FAIL_UPON_MISSING_PROPERTY, PluginRegistry.createRegistry(rootPlugins));
+            }
+        }
 
         // add the resolved rootProperties to the project's properties
         final Map<String, String> thisProperties = nonFullyResolvedProjectProperties.thisMap;
Index: src/net/sourceforge/cruisecontrol/config/RegexProperties.java
===================================================================
--- src/net/sourceforge/cruisecontrol/config/RegexProperties.java	(revision 0)
+++ src/net/sourceforge/cruisecontrol/config/RegexProperties.java	(revision 0)
@@ -0,0 +1,253 @@
+/********************************************************************************
+ * CruiseControl, a Continuous Integration Toolkit
+ * Copyright (c) 2007, ThoughtWorks, Inc.
+ * 200 E. Randolph, 25th Floor
+ * Chicago, IL 60601 USA
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *     + Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *     + Redistributions in binary form must reproduce the above
+ *       copyright notice, this list of conditions and the following
+ *       disclaimer in the documentation and/or other materials provided
+ *       with the distribution.
+ *
+ *     + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
+ *       names of its contributors may be used to endorse or promote
+ *       products derived from this software without specific prior
+ *       written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ ********************************************************************************/
+package net.sourceforge.cruisecontrol.config;
+
+import net.sourceforge.cruisecontrol.gendoc.annotations.SkipDoc;
+import net.sourceforge.cruisecontrol.util.Util;
+import net.sourceforge.cruisecontrol.util.ValidationHelper;
+import net.sourceforge.cruisecontrol.ProjectXMLHelper;
+import net.sourceforge.cruisecontrol.CruiseControlException;
+import net.sourceforge.cruisecontrol.ResolverUser;
+
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * <p>The <i>regular expression property</i> plugin is used to set a property (or a set of 
+ * properties) from such lines in a general text file (with unspecified format) which matches
+ * the given regular expression . The properties set through this plugin behave equally to 
+ * the properties set by the <code>&lt;property&gt;</code> element.
+ *
+ * To define a property from a file with unspecified format, the following attributes must be
+ * set: 
+ * <ol>
+ *     <li>The name of file to parse.</li>
+ *     <li>Regular expression against which lines in the file are matched. The regular expression
+ *         should contain at least 2 groups, one standing for the name of property, one standing
+ *         for its value.</li>
+ *     <li>The definition of the property name template - it may be as simple as just the reference 
+ *         to the first group, however, the name may be adjusted, if necessary.</li>
+ *     <li>The definition of the property value template, following the same principle as for the
+ *         property name.</li>
+ * </ol>
+ * 
+ * As an example, let us consider the file with lines as follows:
+ * <pre>
+ *   name1[value1]
+ *   name2[value2]  Anything may follow ...
+ *   
+ *   there may be anything as well ...
+ *   
+ *   name3[value3]; name4[value4]; name5[value5]
+ * </pre>
+ * 
+ * and we want to define properties based on this. Moreover, we want to alter the names into
+ * the form: <code>build.attrib.special_name_1</code>. To achieve this, we need to define regex
+ * pattern:
+ * <pre>
+ *   \s*([a-zA-Z]+)(\d+)[(\S+)]
+ * </pre>
+ * then, the template of property name must be set to (referring to groups 1 and 2):
+ * <pre>
+ *   build.attrib.special_\1_\2 
+ * </pre>
+ * and the name of property value is simple reference to group 3:
+ * <pre>
+ *   \3 
+ * </pre>
+ *
+ * In CruiseControl project, the properties such defined are accessible by standard way, i.e.
+ * <code>${build.attrib.special_name_1}</code>, <code>${build.attrib.special_name_2}</code>, 
+ * etc. 
+ */
+public class RegexProperties implements PropertiesPlugin, ResolverUser {
+
+    /** The attribute set by {@link #setFile(String)} */
+    private String file = null; 
+    /** The attribute set by {@link #setPattern(String)} */
+    private String matchPattern = null;
+    /** The attribute set by {@link #setName(String)} */
+    private String nameTemplate = null;
+    /** The attribute set by {@link #setValue(String)} */
+    private String valueTemplate = null;
+  
+    /** The guard of properties file changes, set by #setFileResolver(FileResolver) */
+    private FileResolver fileResolver;
+
+
+    /**
+     * Sets the instance of {@link FileResolver}. As it is claimed in
+     * {@link ResolverUser#setFileResolver(FileResolver)} documentation, it must be ensured 
+     * that this method is called earlier than the other methods using the file resolver.
+     *
+     * @param resolver the instance to fill;
+     */
+    @SkipDoc
+    @Override
+    public void setFileResolver(final FileResolver resolver) {
+        fileResolver = resolver;
+    }
+
+    /**
+     * The implementation of {@link ResolverUser#setXmlResolver(XmlResolver)}. Since XML 
+     * resolver is not required here, it ignores the call.
+     */
+    @SkipDoc
+    @Override
+    public void setXmlResolver(XmlResolver arg0) {
+        // Not needed ...
+    }
+
+    /**
+     * Called after the configuration is read to make sure that all the mandatory parameters were 
+     * specified and have correct values.
+     * 
+     * @throws CruiseControlException if there was a configuration error.
+     */
+    public void validate() throws CruiseControlException {
+        /* Are the required attributes set? */
+        ValidationHelper.assertIsSet(file, "file", this.getClass());
+        ValidationHelper.assertIsSet(matchPattern, "pattern", this.getClass());
+        ValidationHelper.assertIsSet(nameTemplate, "name", this.getClass());
+        ValidationHelper.assertIsSet(valueTemplate, "value", this.getClass());
+        /* The file must exist */
+        ValidationHelper.assertExists(new File(this.file), "file", this.getClass());
+            /* Check, if the pattern is correct */
+        try {
+            Pattern.compile(matchPattern);
+        } catch (PatternSyntaxException e) {
+            ValidationHelper.fail("pattern '" + matchPattern + "' is not valid", e);
+        }
+    } // validate
+
+    /**
+     * Called to parse the file and define the properties.
+     * 
+     * @param props the map into which to set the properties parsed.
+     * @param failIfMissing if to fail when .... ????
+     * @throws CruiseControlException if there was an critical error
+     */
+    @Override
+    public void loadProperties(final Map<String, String> props, final boolean failIfMissing)
+        throws CruiseControlException {
+    
+        final BufferedReader reader;
+        final Pattern pattern;
+        
+        // TODO FIXME: how to handle failIfMissing attribute?
+        try {
+            reader = new BufferedReader(new InputStreamReader(fileResolver.getInputStream(this.file)));
+            pattern = Pattern.compile(this.matchPattern);
+
+            try {
+                /* Read the theFile line by line and match them against the pattern. Expand macros
+                 * as we go. We must do this manually to preserve the order of the properties. */
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    final Matcher matcher = pattern.matcher(line);
+                    
+                    /* Find all matches at the line ... */
+                    while (matcher.find()) {
+                        String parsedName = this.nameTemplate;
+                        String parsedValue = this.valueTemplate;
+                        /* Replace group references in the templates by the values */
+                        for (int i = 1; i <= matcher.groupCount(); i++) {
+                            parsedName = parsedName.replace("\\" + Integer.toString(i), matcher.group(i));
+                            parsedValue = parsedValue.replace("\\" + Integer.toString(i), matcher.group(i));
+                        }
+                        /* Replace ${....} definitions by values from properties already read */
+                        parsedName = Util.parsePropertiesInString(props, parsedName, failIfMissing);
+                        parsedValue = Util.parsePropertiesInString(props, parsedValue, failIfMissing);
+                        /* Set the parsed property */ 
+                        ProjectXMLHelper.setProperty(props, parsedName, parsedValue);
+                    }
+                }
+            } finally {
+                reader.close();
+            }
+
+        } catch (IOException e) {
+            throw new CruiseControlException("Could not parse properties from the file \"" + this.file
+                  + "\".", e);
+        }
+    } // loadProperties
+
+
+    /**
+     * Sets the name of file to read and parse.
+     * @param file the name of file to read and parse.
+     * @required Yes
+     */
+    public void setFile(String file) {
+        this.file = file;
+    } // setFile
+
+    /**
+     * Sets the pattern against which the lines read are matched.
+     * @param pattern the regular expression pattern of interest.
+     * @required Yes.
+     */
+    public void setPattern(String pattern) {
+        this.matchPattern = pattern;
+    } // setPattern
+
+    /**
+     * Sets the template of property name, used in conjunction with <code>pattern</code>.
+     * @param name the template from which the property name is created (used to refer to at 
+     *        least one group the the pattern set by {@link #setPattern(String)})
+     * @required Yes.
+     */
+    public void setName(String name) {
+        this.nameTemplate = name;
+    } // setName
+
+    /**
+     * Sets the template of property value, used in conjunction with <code>pattern</code>.
+     * @param value the template from which the property value is created (used to refer to at 
+     *        least one group the the pattern set by {@link #setPattern(String)})
+     * @required Yes.
+     */
+    public void setValue(String value) {
+        this.valueTemplate = value;
+    } // setValue
+}
Index: src/net/sourceforge/cruisecontrol/ProjectConfig.java
===================================================================
--- src/net/sourceforge/cruisecontrol/ProjectConfig.java	(revision 4601)
+++ src/net/sourceforge/cruisecontrol/ProjectConfig.java	(working copy)
@@ -49,6 +49,7 @@
 
 import net.sourceforge.cruisecontrol.config.DefaultPropertiesPlugin;
 import net.sourceforge.cruisecontrol.config.PluginPlugin;
+import net.sourceforge.cruisecontrol.config.PropertiesPlugin;
 import net.sourceforge.cruisecontrol.gendoc.annotations.Cardinality;
 import net.sourceforge.cruisecontrol.gendoc.annotations.Default;
 import net.sourceforge.cruisecontrol.gendoc.annotations.Optional;
@@ -156,6 +157,11 @@
         this.forceBuildNewProject = forceBuildNewProject;
     }
 
+    @Description("Registers a general plug-in inherrited from PropertiesPlugin interface.")
+    public void add(PropertiesPlugin plugin) {
+        // Must be empty, plugin is registered somewhere else
+    }
+
     /**
      * @deprecated exists only for gendoc, should not be called.
      */
signature.asc (application/pgp-signature, 198 B)
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.17 (GNU/Linux)

iEYEABECAAYFAk7LTz0ACgkQuYlVhxo/P1H2tACgiq/BY+jPeHclqWSzLb6p/YPM
DNkAn2kCjj/JiZVDDfYjsP/Rap1tWBnN
=08k5
-----END PGP SIGNATURE-----