Re: New plugin - regular expression based properties

"Ing. Daniel TIHELKA Ph.D." <[email protected]> Fri, 20 Apr 2012 13:18:45 +0200
Newsgroups gmane.comp.java.cruise-control.devel
Message-ID <58c7-4f914600-fb-47c5ca00@71600694>
Here they are.
Dan T. 


On Friday, April 20, 2012 12:36 CEST, Julian Simpson <[email protected]> wrote: 
 
> On 19 April 2012 22:30, Daniel Tihelka <[email protected]> wrote:
> 
> > **
> >
> > Hallo,
> >
> > I have just committed change which allows to write 3rd party properties
> > plugin (e.g. getting properties from a database).
> >
> >
> > I also have an implementation of "regular expression properties plugin"
> > which parses a given text file and defines properties based on the regular
> > expressions.
> >
> >
> > [snip]
> >
> >
> > Now, the question is: do you want to have this regexproperties plugin
> > included in CC? I can manage it as 3rd party plugin, but I thing it may
> > be quite useful for other uses as well.
> >
> 
> 
> I think it's worth looking at - where can we see the source?
> 
> Best
> 
> Julian.
> 
> >
> > Best regards,
> >
> > Dan T.
> >
> >
> >
> >
> > ------------------------------------------------------------------------------
> > For Developers, A Lot Can Happen In A Second.
> > Boundary is the first to Know...and Tell You.
> > Monitor Your Applications in Ultra-Fine Resolution. Try it FREE!
> > http://p.sf.net/sfu/Boundary-d2dvs2
> > _______________________________________________
> > Cruisecontrol-devel mailing list
> > [email protected]
> > https://lists.sourceforge.net/lists/listinfo/cruisecontrol-devel
> >
> >
> 
> 
> -- 
> Julian Simpson
> The Build Doctor Ltd.
> http://www.build-doctor.com
> [email protected]
> (+44) 207 183 0323

------------------------------------------------------------------------------
For Developers, A Lot Can Happen In A Second.
Boundary is the first to Know...and Tell You.
Monitor Your Applications in Ultra-Fine Resolution. Try it FREE!
http://p.sf.net/sfu/Boundary-d2dvs2

_______________________________________________
Cruisecontrol-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/cruisecontrol-devel
RegexProperties.java (text/x-java, 10.9 KB)
/********************************************************************************
 * 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 zcu.kky;

import net.sourceforge.cruisecontrol.config.FileResolver;
import net.sourceforge.cruisecontrol.config.PropertiesPlugin;
import net.sourceforge.cruisecontrol.config.XmlResolver;
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 value 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
}
RegexPropertiesTest.java (text/x-java, 16 KB)
/********************************************************************************
 *
 * 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 zcu.kky;

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;
    }
    
    
}