You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2009/01/28 04:57:48 UTC

svn commit: r738356 - in /incubator/jspwiki/trunk: ./ src/com/ecyrd/jspwiki/ src/com/ecyrd/jspwiki/ui/migrator/ src/com/ecyrd/jspwiki/util/ tests/com/ecyrd/jspwiki/ui/migrator/ tests/com/ecyrd/jspwiki/util/ tests/etc/

Author: ajaquith
Date: Wed Jan 28 03:57:46 2009
New Revision: 738356

URL: http://svn.apache.org/viewvc?rev=738356&view=rev
Log:
Checked in BundleMigrator, which makes it easier to move, rename, or delete resource bundle messages across a range of property files with a single command. This necessitated changes to CommentedProperties, because it was not properly parsing multi-line messages.

Added:
    incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/ui/migrator/BundleMigrator.java
    incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/ui/migrator/BundleMigratorTest.java
Modified:
    incubator/jspwiki/trunk/ChangeLog
    incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/Release.java
    incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/CommentedProperties.java
    incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/FileUtil.java
    incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/ui/migrator/AllTests.java
    incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/util/CommentedPropertiesTest.java
    incubator/jspwiki/trunk/tests/etc/test.properties

Modified: incubator/jspwiki/trunk/ChangeLog
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/ChangeLog?rev=738356&r1=738355&r2=738356&view=diff
==============================================================================
--- incubator/jspwiki/trunk/ChangeLog (original)
+++ incubator/jspwiki/trunk/ChangeLog Wed Jan 28 03:57:46 2009
@@ -1,3 +1,12 @@
+2009-01-27  Andrew Jaquith <ajaquith AT apache DOT org>
+
+        * 3.0.0-svn-56
+
+        * Checked in BundleMigrator, which makes it easier to move, rename,
+        or delete resource bundle messages across a range of property files
+        with a single command. This necessitated changes to CommentedProperties,
+        because it was not properly parsing multi-line messages.
+
 2009-01-23  Harry Metske <me...@apache.org>
 
         * 3.0.0-svn-55

Modified: incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/Release.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/Release.java?rev=738356&r1=738355&r2=738356&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/Release.java (original)
+++ incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/Release.java Wed Jan 28 03:57:46 2009
@@ -77,7 +77,7 @@
      *  <p>
      *  If the build identifier is empty, it is not added.
      */
-    public static final String     BUILD         = "55";
+    public static final String     BUILD         = "56";
     
     /**
      *  This is the generic version string you should use

Added: incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/ui/migrator/BundleMigrator.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/ui/migrator/BundleMigrator.java?rev=738356&view=auto
==============================================================================
--- incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/ui/migrator/BundleMigrator.java (added)
+++ incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/ui/migrator/BundleMigrator.java Wed Jan 28 03:57:46 2009
@@ -0,0 +1,414 @@
+package com.ecyrd.jspwiki.ui.migrator;
+
+import java.io.*;
+import java.util.*;
+
+import com.ecyrd.jspwiki.util.CommentedProperties;
+
+/**
+ * Utility class for copying, moving, deleting and renaming message keys between
+ * two message bundles.
+ */
+public class BundleMigrator
+{
+
+    public static class Bundle
+    {
+        private final Map<Locale, File> m_bundleFiles = new HashMap<Locale, File>();
+
+        private final Map<Locale, Properties> m_bundleProps = new HashMap<Locale, Properties>();
+
+        private final File m_baseFile;
+
+        private Properties m_baseProps;
+
+        /**
+         * Constructs a new Bundle whose base file name is supplied. During
+         * construction, the base bundle file is verified by appending
+         * <code>.properties</code> to the base name and testing that the
+         * resulting file exists. For example, if the base file
+         * <code>src/Default</code> is passed, the file
+         * <code>src/Default.properties</code> will be checked. If the file is
+         * found, all possible combinations of bundle files are checked next,
+         * for example <code>src/Default_en.properties</code>,
+         * <code>src/Default_fr.properties</code>, and so on. If the base
+         * bundle file does not exist, this method throws an
+         * IllegalArgumentException.
+         * 
+         * @param baseFile the path to the base bundle file, minus the trailing
+         *            locale suffix and <code>.properties</code> extension
+         * @throws FileNotFoundException if the base bundle file does not exist
+         */
+        public Bundle( String baseFile ) throws FileNotFoundException
+        {
+            super();
+            m_baseFile = new File( baseFile + ".properties" );
+            m_baseProps = new CommentedProperties();
+            findBundleFiles( baseFile );
+        }
+
+        /**
+         * Returns the path to the bundle file for a supplied Locale.
+         * 
+         * @param locale if <code>null</code>, the base file will be returned
+         * @return the file
+         */
+        public File getFile( Locale locale )
+        {
+            return locale == null ? m_baseFile : m_bundleFiles.get( locale );
+        }
+
+        /**
+         * Returns the Locales associated with this Bundle.
+         * 
+         * @return the collection of Locale objects
+         */
+        public Collection<Locale> getLocales()
+        {
+            return Collections.unmodifiableSet( m_bundleFiles.keySet() );
+        }
+
+        /**
+         * Returns the Properties object for a supplied Locale.
+         * 
+         * @param locale if <code>null</code>, the base Properties object
+         *            will be returned
+         * @return the file
+         */
+        public Properties getProperties( Locale locale )
+        {
+            return locale == null ? m_baseProps : m_bundleProps.get( locale );
+        }
+
+        /**
+         * Loads the set of bundle files from disk.
+         */
+        public void load() throws IOException
+        {
+            // Load the default properties file first
+            m_baseProps.load( new FileInputStream( m_baseFile ) );
+
+            // Now load one for each Locale
+            for( Map.Entry<Locale, File> entry : m_bundleFiles.entrySet() )
+            {
+                Properties props = new CommentedProperties();
+                props.load( new FileInputStream( entry.getValue() ) );
+                m_bundleProps.put( entry.getKey(), props );
+            }
+        }
+
+        /**
+         * Saves the set of bundle files to disk.
+         * 
+         * @throws IOException
+         */
+        public void save() throws IOException
+        {
+            // Save the default properties file firs
+            m_baseProps.store( new FileOutputStream( m_baseFile ), null );
+
+            // Now store each Locale's file
+            for( Map.Entry<Locale, File> entry : m_bundleFiles.entrySet() )
+            {
+                Properties props = m_bundleProps.get( entry.getKey() );
+                props.store( new FileOutputStream( entry.getValue() ), null );
+            }
+        }
+
+        /**
+         * Populates a Bundle, starting with a supplied base file and iterating
+         * through every possible Locale combination.
+         * 
+         * @param baseFile the path to the base bundle file, minus the trailing
+         *            locale and <code>.properties</code>
+         */
+        protected void findBundleFiles( String baseFile ) throws FileNotFoundException
+        {
+            File baseBundle = new File( baseFile + ".properties" );
+            if( !baseBundle.exists() )
+            {
+                throw new FileNotFoundException( "Bundle file " + baseBundle.toString() + " not found." );
+            }
+
+            // Add all bundle files that exist in the file system
+            Locale[] locales = Locale.getAvailableLocales();
+            for( Locale locale : locales )
+            {
+                File file = new File( baseFile + "_" + locale.toString() + ".properties" );
+                if( file.exists() )
+                {
+                    m_bundleFiles.put( locale, file );
+                    m_bundleProps.put( locale, new CommentedProperties() );
+                }
+            }
+        }
+    }
+
+    private Bundle m_source;
+
+    /**
+     * Constructs a new BundleMigrator.
+     */
+    public BundleMigrator()
+    {
+        super();
+    }
+
+    /**
+     * Copies a message key from the source Bundle to a target Bundle.
+     * 
+     * @param key the name of the key to copy
+     * @param target the target Bundle
+     */
+    public void copy( String key, Bundle target ) throws IOException
+    {
+        // Look for the property in the base file
+        String value = m_source.getProperties( null ).getProperty( key );
+        if( value == null )
+        {
+            throw new IllegalArgumentException( "Key " + key + " not found in bundle." );
+        }
+
+        // Load the source and target bundles
+        m_source.load();
+        target.load();
+
+        // Copy the base property file's key first
+        Properties props = target.getProperties( null );
+        props.put( key, value );
+
+        // Copy the key for each locale file
+        Collection<Locale> locales = m_source.getLocales();
+        for( Locale locale : locales )
+        {
+            props = m_source.getProperties( locale );
+            value = props.getProperty( key );
+            if( value != null )
+            {
+                props = target.getProperties( locale );
+                if( props != null )
+                {
+                    props.put( key, value );
+                }
+            }
+        }
+
+        // Save the target bundle
+        target.save();
+    }
+
+    /**
+     * Returns the source {@link Bundle} for the BundleMigrator to operate on.
+     * 
+     * @return the source bundle
+     */
+    public Bundle getSource()
+    {
+        return m_source;
+    }
+
+    /**
+     * Moves a message key from the source Bundle to a target Bundle.
+     * 
+     * @param key the name of the key to move
+     * @param target the target Bundle
+     */
+    public void move( String key, Bundle target ) throws IOException
+    {
+        copy( key, target );
+        remove( key );
+    }
+
+    /**
+     * Deletes a message key from the source Bundle.
+     * 
+     * @param key the name of the key to remove
+     */
+    public void remove( String key ) throws IOException
+    {
+        // Look for the property in the base file
+        String value = m_source.getProperties( null ).getProperty( key );
+        if( value == null )
+        {
+            throw new IllegalArgumentException( "Key " + key + " not found in bundle." );
+        }
+
+        // Load the source bundle
+        m_source.load();
+
+        // Rename the base property file's key first
+        Properties props = m_source.getProperties( null );
+        props.remove( key );
+
+        // Remove the key from each locale file
+        Collection<Locale> locales = m_source.getLocales();
+        for( Locale locale : locales )
+        {
+            props = m_source.getProperties( locale );
+            value = props.getProperty( key );
+            if( value != null )
+            {
+                props.remove( key );
+            }
+        }
+
+        // Save the bundle
+        m_source.save();
+    }
+
+    /**
+     * Renames a message key contained in the source Bundle.
+     * 
+     * @param key the name of the key to change
+     * @param newKey the new name for the key
+     */
+    public void rename( String key, String newKey ) throws IOException
+    {
+        // Look for the property in the base file
+        String value = m_source.getProperties( null ).getProperty( key );
+        if( value == null )
+        {
+            throw new IllegalArgumentException( "Key " + key + " not found in bundle." );
+        }
+        if( newKey == null )
+        {
+            throw new IllegalArgumentException( "New key name must not be null." );
+        }
+
+        // Load the source bundle
+        m_source.load();
+
+        // Rename the base property file's key first
+        Properties props = m_source.getProperties( null );
+        props.remove( key );
+        props.put( newKey, value );
+
+        // Rename the key in each locale file
+        Collection<Locale> locales = m_source.getLocales();
+        for( Locale locale : locales )
+        {
+            props = m_source.getProperties( locale );
+            value = props.getProperty( key );
+            if( value != null )
+            {
+                props.remove( key );
+                props.put( newKey, value );
+            }
+        }
+
+        // Save the bundle
+        m_source.save();
+
+    }
+
+    /**
+     * Sets the source {@link Bundle} to operate on, and loads it.
+     * 
+     * @param source the Bundle to operate on
+     * @throws FileNotFoundException if the base bundle file does not exist
+     */
+    public void setBundle( Bundle source ) throws IOException
+    {
+        m_source = source;
+        source.load();
+    }
+
+    /**
+     * <p>
+     * Command-line interface to BundleMigrator. The general syntax for running
+     * BundleMigrator is:
+     * </p>
+     * <blockquote>
+     * <code>BundleMigrator <var>action source keyname [destination | newkeyname]</var></code>
+     * </blockquote>
+     * <p>
+     * ...where <var>action</var> is an action verb: <code>copy</code>,
+     * <code>delete</code>, <code>move</code> or <code>rename</code>.
+     * </p>
+     * <p>
+     * The <code>source</code> parameter indicates the path to the base bundle
+     * file, minus the trailing locale suffix and <code>.properties</code>
+     * extension; for example, <code>etc/i18n/CoreResources</code>. The
+     * <code>destination</code> parameter indicates the destination bundle for
+     * move and rename actions.
+     * </p>
+     * <p>
+     * The <code>keyname</code> denotes the key to copy, move, delete or
+     * rename. When renaming a key, <code>newkeyname</code> denotes the new
+     * key name.
+     * </p>
+     * <p>
+     * Here are examples for the entire set of valid commands:
+     * </p>
+     * <blockquote>
+     * <code>BundleMigrator rename src/i18n/CoreResources error.oldname error.newname<br/>
+     * BundleMigrator delete src/i18n/CoreResources error.oldname<br/>
+     * BundleMigrator copy src/i18n/CoreResources error.oldname src/i18n/templates/default<br/>
+     * BundleMigrator move src/i18n/CoreResources error.oldname src/i18n/templates/default</code></blockquote>
+     * 
+     * @param args the command line arguments
+     */
+    public static final void main( String[] args )
+    {
+        String validSyntax = "BundleMigrator action source keyname [newkeyname | destination]";
+        if( args.length == 0 )
+        {
+            System.err.println( "Too few arguments. Valid syntax: " + validSyntax );
+            System.err.println( "Valid actions are: copy, delete, move, and rename." );
+            return;
+        }
+
+        try
+        {
+            // Get the verb and source bundle
+            String action = args[0].trim();
+            Bundle source = new Bundle( args[1].trim() );
+            String key = args[2].trim();
+            BundleMigrator migrator = new BundleMigrator();
+            migrator.setBundle( source );
+
+            // Execute the action
+            if( "delete".equals( action ) )
+            {
+                if  ( wrongNumberArgs( 3, args, "BundleMigrator delete source keyname" ) ) { return; }
+                migrator.remove( key );
+            }
+            else if( "rename".equals( action ) )
+            {
+                if ( wrongNumberArgs( 4, args, "BundleMigrator rename source keyname newkeyname" ) ) { return; }
+                String newKey = args[3].trim();
+                migrator.rename( key, newKey );
+            }
+            else if( "copy".equals( action ) )
+            {
+                if ( wrongNumberArgs( 4, args, "BundleMigrator copy source keyname destination" ) ) { return; }
+                Bundle destination = new Bundle( args[3].trim() );
+                migrator.copy( key, destination );
+            }
+            else if( "move".equals( action ) )
+            {
+                if ( wrongNumberArgs( 4, args, "BundleMigrator move source keyname destination" ) ) { return; }
+                Bundle destination = new Bundle( args[3].trim() );
+                migrator.move( key, destination );
+            }
+            else
+            {
+                System.err.println( "Invalid syntax. Valid syntax is: " + validSyntax );
+            }
+        }
+        catch ( IOException e )
+        {
+            System.err.println( "Error: " + e.getMessage() );
+        }
+    }
+
+    private static boolean wrongNumberArgs( int requiredArgs, String[] args, String validSyntax )
+    {
+        if( args.length != requiredArgs )
+        {
+            System.out.println( "Wrong number of arguments. Valid syntax: " + validSyntax );
+            return true;
+        }
+        return false;
+    }
+}

Modified: incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/CommentedProperties.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/CommentedProperties.java?rev=738356&r1=738355&r2=738356&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/CommentedProperties.java (original)
+++ incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/CommentedProperties.java Wed Jan 28 03:57:46 2009
@@ -21,16 +21,15 @@
 package com.ecyrd.jspwiki.util;
 
 import java.io.*;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Map.Entry;
-
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Extends {@link java.util.Properties} by providing support for comment
- * preservation. When the properties are written to disk, previous
- * comments present in the file are preserved.
+ * preservation. When the properties are written to disk, previous comments
+ * present in the file are preserved.
+ * 
  * @author Andrew Jaquith
  * @since 2.4.22
  */
@@ -38,7 +37,18 @@
 {
     private static final long serialVersionUID = 8057284636436329669L;
 
-    private String m_propertyString;
+    /** Map with property names as keys, and comments as values. */
+    private Map<String, String> m_propertyComments = new HashMap<String, String>();
+
+    /**
+     * Ordered map with property names inserted in the order encountered in the
+     * text file.
+     */
+    private Set<Object> m_keys = new LinkedHashSet<Object>();
+
+    private String m_trailingComment = null;
+
+    private final String m_br;
 
     /**
      * @see java.util.Properties#Properties()
@@ -46,53 +56,52 @@
     public CommentedProperties()
     {
         super();
+        m_br = System.getProperty( "line.separator" );
     }
 
     /**
-     *  Creates new properties.
-     *
-     *  @param defaultValues A list of default values, which are used if in subsequent gets
-     *                       a key is not found.
+     * Creates new properties.
+     * 
+     * @param defaultValues A list of default values, which are used if in
+     *            subsequent gets a key is not found.
      */
     public CommentedProperties( Properties defaultValues )
     {
         super( defaultValues );
+        m_br = System.getProperty( "line.separator" );
     }
 
     /**
-     *  {@inheritDoc}
+     * {@inheritDoc}
      */
     @Override
     public synchronized void load( InputStream inStream ) throws IOException
     {
         // Load the file itself into a string
-        m_propertyString = FileUtil.readContents( inStream, "ISO-8859-1" );
+        String propertyString = FileUtil.readContents( inStream, "ISO-8859-1" );
 
         // Now load it into the properties object as normal
-        super.load( new ByteArrayInputStream( m_propertyString.getBytes("ISO-8859-1") ) );
+        super.load( new ByteArrayInputStream( propertyString.getBytes( "ISO-8859-1" ) ) );
+
+        // Load all of the comments
+        loadComments( propertyString );
     }
 
     /**
-     *  Loads properties from a file opened by a supplied Reader.
-     *  
-     *  @param in The reader to read properties from
-     *  @throws IOException in case something goes wrong.
+     * Loads properties from a file opened by a supplied Reader.
+     * 
+     * @param in The reader to read properties from
+     * @throws IOException in case something goes wrong.
      */
     public synchronized void load( Reader in ) throws IOException
     {
-        m_propertyString = FileUtil.readContents( in );
+        String propertyString = FileUtil.readContents( in );
 
         // Now load it into the properties object as normal
-        super.load( new ByteArrayInputStream( m_propertyString.getBytes("ISO-8859-1") ) );
-    }
+        super.load( new ByteArrayInputStream( propertyString.getBytes( "ISO-8859-1" ) ) );
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public synchronized Object setProperty( String key, String value )
-    {
-        return put(key, value);
+        // Load all of the comments
+        loadComments( propertyString );
     }
 
     /**
@@ -101,7 +110,7 @@
     @Override
     public synchronized void store( OutputStream out, String comments ) throws IOException
     {
-        byte[] bytes = m_propertyString.getBytes("ISO-8859-1");
+        byte[] bytes = toString().getBytes( "ISO-8859-1" );
         FileUtil.copyContents( new ByteArrayInputStream( bytes ), out );
         out.flush();
     }
@@ -110,135 +119,247 @@
      * {@inheritDoc}
      */
     @Override
-    public synchronized Object put( Object arg0, Object arg1 )
+    public synchronized String toString()
     {
-        // Write the property to the stored string
-        writeProperty( arg0, arg1 );
+        StringBuilder b = new StringBuilder();
+        for( Object key : m_keys )
+        {
+            Object value = get( key );
+            String comment = m_propertyComments.get( key );
+            if( comment != null )
+            {
+                b.append( comment );
+                b.append( m_br );
+            }
+            printProperty( b, key, value );
+        }
 
-        // Return the result of from the superclass properties object
-        return super.put(arg0, arg1);
+        // Now print the keys we did not encounter (i.e., were added after the
+        // load method)
+        for( Map.Entry<Object, Object> entry : this.entrySet() )
+        {
+            String key = entry.getKey().toString();
+            if( !m_keys.contains( key ) )
+            {
+                Object value = entry.getValue();
+                printProperty( b, key, value );
+            }
+        }
+
+        // Add any trailing comments
+        if( m_trailingComment != null )
+        {
+            b.append( m_trailingComment );
+            b.append( m_br );
+        }
+        return b.toString();
+    }
+
+    private void printProperty( StringBuilder b, Object key, Object value )
+    {
+        b.append( key.toString() );
+        b.append( ' ' );
+        b.append( '=' );
+        b.append( ' ' );
+        b.append( value.toString() );
+        b.append( m_br );
     }
 
     /**
-     * {@inheritDoc}
+     * Trims whitespace from property line strings: \n \r \u0020 \t \u0009 \f
+     * \u000c space
      */
-    @SuppressWarnings("unchecked")
-    @Override
-    public synchronized void putAll( Map arg0 )
+    private static final Pattern LINE_TRIMMER = Pattern.compile( "^[ \\r\\n\\t\\f]*(.*?)[ \\r\\n\\t\\f]*$" );
+
+    /**
+     * Determines if a line consists entirely of whitespace.
+     */
+    private static final Pattern BLANK_LINE_DETECTOR = Pattern.compile( "^[ \\r\\n\\t\\f]*$" );
+
+    /**
+     * Parses the comments from the properties file (stored as a string)
+     */
+    private void loadComments( String propertyString ) throws IOException
     {
-        // Shove all of the entries into the property string
-        for( Iterator it = arg0.entrySet().iterator(); it.hasNext(); )
+        LineNumberReader reader = new LineNumberReader( new StringReader( propertyString ) );
+        String line = null;
+        boolean inProperty = false;
+        String comment = null;
+        while ( (line = reader.readLine()) != null )
         {
-            Entry entry = (Entry)it.next();
-            writeProperty( entry.getKey(), entry.getValue() );
+            // Trim line of leading/trailing whitespace
+            Matcher m = LINE_TRIMMER.matcher( line );
+            if( m.matches() )
+            {
+                String text = m.group( 1 );
+
+                // Is first character ! or #? We are in a comment line...
+                boolean isComment = text.startsWith( "#" ) || text.startsWith( "!" );
+                boolean isWhitespace = BLANK_LINE_DETECTOR.matcher( text ).matches();
+                if( isComment )
+                {
+                    comment = comment == null ? text : comment + m_br + text;
+                    inProperty = false;
+                }
+
+                // If all whitespace and part of comment, append it
+                else if( isWhitespace && !inProperty )
+                {
+                    comment = comment == null ? text : comment + m_br + text;
+                }
+
+                // Otherwise, see if we're starting a new property key
+                else if( !inProperty )
+                {
+                    // If we are, lookup the key and add the comment
+                    String key = extractKey( text );
+                    if( key != null )
+                    {
+                        String value = getProperty( key );
+                        if( value != null && comment != null )
+                        {
+                            m_propertyComments.put( key, comment );
+                            m_keys.add( key );
+                        }
+                        inProperty = true;
+                        comment = null;
+                    }
+                }
+            }
         }
 
-        // Call the superclass method
-        super.putAll(arg0);
+        // Any leftover comments are "trailing" comments that go at the end of
+        // the file
+        m_trailingComment = comment;
+
+        reader.close();
     }
 
     /**
-     * {@inheritDoc}
+     * Extracts a key name from a trimmed line of text, which may include
+     * escaped characters.
+     * 
+     * @param text
+     * @return the key
      */
-    @Override
-    public synchronized Object remove( Object key )
+    protected String extractKey( String text )
     {
-        // Remove from the property string
-        deleteProperty( key );
-
-        // Call the superclass method
-        return super.remove(key);
+        char[] chars = text.toCharArray();
+        char lastChar = ' ';
+        for( int i = 0; i < chars.length; i++ )
+        {
+            char ch = chars[i];
+            switch( ch )
+            {
+                case ' ':
+                case '\t':
+                case ':':
+                case '=':
+                case '\f': {
+                    if( lastChar != '\'' )
+                    {
+                        return text.substring( 0, i );
+                    }
+                }
+            }
+        }
+        return null;
     }
 
     /**
-     * {@inheritDoc}
+     * Returns the comment for a supplied key, as parsed by the
+     * {@link #load(Reader)} or {@link #load(InputStream)} methods, <em>or</em>
+     * as supplied to the{@link #setProperty(String, String, String)} method.
+     * 
+     * @param key the key to look up
+     * @return the comment (including the trailing <code>!</code> or
+     *         <code>#</code> character, or <code>null</code> if not found
      */
-    @Override
-    public synchronized String toString()
+    public String getComment( String key )
     {
-        return m_propertyString;
+        return(m_propertyComments.get( key ));
     }
 
-    private void deleteProperty( Object arg0 )
+    /**
+     * Sets a property value for a supplied key, and adds a comment that will be
+     * written to disk when the {@link #store(OutputStream, String)} method is
+     * called. This method behaves otherwise identically to
+     * {@link #setProperty(String, String)}.
+     * 
+     * @param key the string key to store
+     * @param value the property value associated with the key
+     * @param comment the comment to add
+     * @return the the previous value of the specified key in this property
+     *         list, or <code>null</code> if it did not have one.
+     */
+    public synchronized Object setProperty( String key, String value, String comment )
     {
-        // Get key and value
-        if ( arg0 == null )
-        {
-            throw new IllegalArgumentException( "Key cannot be null." );
-        }
-        String key = arg0.toString();
-
-        // Iterate through each line and replace anything matching our key
-        int idx = 0;
-        while( ( idx < m_propertyString.length() ) && ( ( idx = m_propertyString.indexOf( key, idx ) ) != -1 ) )
+        if( key != null )
         {
-            int prevret = m_propertyString.lastIndexOf( "\n", idx );
-            if ( prevret != -1 )
+            if( comment != null )
             {
-                // Commented lines are skipped
-                if ( m_propertyString.charAt( prevret + 1 ) == '#' )
+                comment = comment.trim();
+                if( !comment.startsWith( "#" ) )
                 {
-                    idx += key.length();
-                    continue;
+                    comment = "# " + comment;
                 }
+                m_propertyComments.put( key, comment );
             }
-
-            // If "=" present, delete the entire line
-            int eqsign = m_propertyString.indexOf( "=", idx );
-            if ( eqsign != -1 )
-            {
-                int ret = m_propertyString.indexOf( "\n", eqsign );
-                m_propertyString = TextUtil.replaceString( m_propertyString, prevret, ret, "" );
-                return;
-            }
+            m_keys.add( key );
         }
+        return super.setProperty( key, value );
     }
 
-    private void writeProperty( Object arg0, Object arg1 )
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public synchronized Object setProperty( String key, String value )
     {
-        // Get key and value
-        if ( arg0 == null )
-        {
-            throw new IllegalArgumentException( "Key cannot be null." );
-        }
-        if ( arg1 == null )
+        return setProperty( key, value, null );
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public synchronized Object put( Object key, Object value )
+    {
+        if( key != null )
         {
-            arg1 = "";
+            m_keys.add( key );
         }
-        String key = arg0.toString();
-        String value = TextUtil.native2Ascii( arg1.toString() );
+        return super.put( key, value );
+    }
 
-        // Iterate through each line and replace anything matching our key
-        int idx = 0;
-        while( ( idx < m_propertyString.length() ) && ( ( idx = m_propertyString.indexOf( key, idx ) ) != -1 ) )
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public synchronized void putAll( Map<? extends Object, ? extends Object> t )
+    {
+        for( Object key : t.keySet() )
         {
-            int prevret = m_propertyString.lastIndexOf( "\n", idx );
-            if ( prevret != -1 )
-            {
-                // Commented lines are skipped
-                if ( m_propertyString.charAt( prevret + 1 ) == '#' )
-                {
-                    idx += key.length();
-                    continue;
-                }
-            }
-
-            // If "=" present, replace everything in line after it
-            int eqsign = m_propertyString.indexOf( "=", idx );
-            if ( eqsign != -1 )
+            if( key != null )
             {
-                int ret = m_propertyString.indexOf( "\n", eqsign );
-                if ( ret == -1 )
-                {
-                    ret = m_propertyString.length();
-                }
-                m_propertyString = TextUtil.replaceString( m_propertyString, eqsign + 1, ret, value );
-                return;
+                m_keys.add( key );
             }
         }
+        super.putAll( t );
+    }
 
-        // If it was not found, we'll add it to the end.
-        m_propertyString += "\n" + key + " = " + value + "\n";
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public synchronized Object remove( Object key )
+    {
+        if( key != null )
+        {
+            m_propertyComments.remove( key );
+            m_keys.remove( key );
+        }
+        return super.remove( key );
     }
 
 }

Modified: incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/FileUtil.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/FileUtil.java?rev=738356&r1=738355&r2=738356&view=diff
==============================================================================
--- incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/FileUtil.java (original)
+++ incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/FileUtil.java Wed Jan 28 03:57:46 2009
@@ -39,7 +39,6 @@
 {
     /** Size of the buffer used when copying large chunks of data. */
     private static final int      BUFFER_SIZE = 4096;
-    private static final Logger   log         = LoggerFactory.getLogger(FileUtil.class);
 
     /**
      *  Private constructor prevents instantiation.
@@ -118,6 +117,7 @@
     {
         StringBuilder result = new StringBuilder();
 
+        Logger log = LoggerFactory.getLogger(FileUtil.class);
         log.info("Running simple command "+command+" in "+directory);
 
         Process process = Runtime.getRuntime().exec( command, null, new File(directory) );
@@ -293,6 +293,7 @@
             }
             catch( Exception e )
             {
+                Logger log = LoggerFactory.getLogger(FileUtil.class);
                 log.error("Not able to close the stream while reading contents.");
             }
         }

Modified: incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/ui/migrator/AllTests.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/ui/migrator/AllTests.java?rev=738356&r1=738355&r2=738356&view=diff
==============================================================================
--- incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/ui/migrator/AllTests.java (original)
+++ incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/ui/migrator/AllTests.java Wed Jan 28 03:57:46 2009
@@ -29,6 +29,7 @@
     public static Test suite()
     {
         TestSuite suite = new TestSuite("JSP migration tests");
+        suite.addTest( BundleMigratorTest.suite() );
         suite.addTest( JspDocumentTest.suite() );
         suite.addTest( JspParserTest.suite() );
         suite.addTest( JSPWikiJspTransformerTest.suite() );

Added: incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/ui/migrator/BundleMigratorTest.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/ui/migrator/BundleMigratorTest.java?rev=738356&view=auto
==============================================================================
--- incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/ui/migrator/BundleMigratorTest.java (added)
+++ incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/ui/migrator/BundleMigratorTest.java Wed Jan 28 03:57:46 2009
@@ -0,0 +1,346 @@
+/*
+    JSPWiki - a JSP-based WikiWiki clone.
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.    
+ */
+package com.ecyrd.jspwiki.ui.migrator;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Properties;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import com.ecyrd.jspwiki.ui.migrator.BundleMigrator.Bundle;
+import com.ecyrd.jspwiki.util.CommentedProperties;
+
+public class BundleMigratorTest extends TestCase
+{
+    private static final String[] LOCALES = { "", "en", "de", "zh_CN" };
+    public static Test suite()
+    {
+        return new TestSuite( BundleMigratorTest.class );
+    }
+    private String m_source = null;
+
+    private String m_dest = null;
+    
+    public void setUp() throws Exception
+    {
+        // Create dummy property files
+        String tmpdir = System.getProperty( "java.io.tmpdir" );
+        File tmp = new File( tmpdir );
+        for( String locale : LOCALES )
+        {
+            // Create sample "to" and "from" property files
+            Properties sourceProps = new CommentedProperties();
+            sourceProps.put( "source.name", "Full name_" + locale );
+            sourceProps.put( "source.email", "E-mail_" + locale );
+            sourceProps.put( "source.password", "Password_" + locale );
+            File sourcePropfile = new File( tmp, ( locale.length() == 0 ? "source" : "source_" + locale ) + ".properties" );
+            sourceProps.store( new FileOutputStream( sourcePropfile), null );
+            m_source = tmpdir + "/source";
+            
+            Properties destProps = new CommentedProperties();
+            destProps.put( "dest.timeZone", "Time zone_" + locale );
+            destProps.put( "dest.orientation", "Orientation_" + locale );
+            File destPropfile = new File( tmp, ( locale.length() == 0 ? "dest" : "dest_" + locale ) + ".properties" );
+            destProps.store( new FileOutputStream( destPropfile), null );
+            m_dest = tmpdir + "/dest";
+        }
+    }
+
+    public void tearDown() throws Exception
+    {
+        // Delete dummy property files
+        File tmp = new File( System.getProperty( "java.io.tmpdir" ) );
+        for( String locale : LOCALES )
+        {
+            // Delete sample "to" and "from" property files
+            File sourcePropfile = new File( tmp, ( locale.length() == 0 ? "source" : "source_" + locale ) + ".properties" );
+            //sourcePropfile.delete();
+            
+            File destPropfile = new File( tmp, ( locale.length() == 0 ? "dest" : "dest_" + locale ) + ".properties" );
+            //destPropfile.delete();
+        }
+    }
+    
+    public void testCopyKey() throws Exception
+    {
+        Bundle source = new Bundle( m_source );
+        BundleMigrator m = new BundleMigrator();
+        m.setBundle( source );
+        
+        // Move a key to target
+        Bundle target = new Bundle( m_dest );
+        m.copy( "source.name", target );
+        
+        // Verify the contents were copied to target
+        source = new Bundle( m_dest );
+        source.load();
+        Properties p;
+
+        p = source.getProperties( null );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_", p.get( "source.name" ) );
+
+        p = source.getProperties( new Locale( "en" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_en", p.get( "source.name" ) );
+        
+        p = source.getProperties( new Locale( "de" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_de", p.get( "source.name" ) );
+        
+        p = source.getProperties( new Locale( "zh", "CN" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_zh_CN", p.get( "source.name" ) );
+    }
+
+    public void testMoveKey() throws Exception
+    {
+        Bundle source = new Bundle( m_source );
+        BundleMigrator m = new BundleMigrator();
+        m.setBundle( source );
+        
+        // Move a key to target
+        Bundle target = new Bundle( m_dest );
+        m.move( "source.name", target );
+        
+        // Verify the key was deleted from source
+        source = new Bundle( m_source );
+        source.load();
+        Properties p;
+
+        p = source.getProperties( null );
+        assertEquals( 2, p.size() );
+        assertEquals( null, p.get( "source.name" ) );
+
+        p = source.getProperties( new Locale( "en" ) );
+        assertEquals( 2, p.size() );
+        assertEquals( null, p.get( "source.name" ) );
+        
+        p = source.getProperties( new Locale( "de" ) );
+        assertEquals( 2, p.size() );
+        assertEquals( null, p.get( "source.name" ) );
+        
+        p = source.getProperties( new Locale( "zh", "CN" ) );
+        assertEquals( 2, p.size() );
+        assertEquals( null, p.get( "source.name" ) );
+        
+        // Verify the key was copied to target
+        target = new Bundle( m_dest );
+        target.load();
+        p = target.getProperties( null );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_", p.get( "source.name" ) );
+
+        p = target.getProperties( new Locale( "en" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_en", p.get( "source.name" ) );
+        
+        p = target.getProperties( new Locale( "de" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_de", p.get( "source.name" ) );
+        
+        p = target.getProperties( new Locale( "zh", "CN" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_zh_CN", p.get( "source.name" ) );
+    }
+    
+    public void testLoadBundle() throws Exception
+    {
+        Bundle b = new Bundle( m_source );
+        // Check that there isn't anything in there yet
+        Properties p;
+        
+        p = b.getProperties( null );
+        assertEquals( 0, p.size() );
+        p = b.getProperties( new Locale( "en" ) );
+        assertEquals( 0, p.size() );
+        p = b.getProperties( new Locale( "de" ) );
+        assertEquals( 0, p.size() );
+        p = b.getProperties( new Locale( "zh", "CN" ) );
+        assertEquals( 0, p.size() );
+        
+        // Load the files
+        b.load();
+        
+        // Verify the contents were loaded
+        p = b.getProperties( null );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_", p.get( "source.name" ) );
+
+        p = b.getProperties( new Locale( "en" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_en", p.get( "source.name" ) );
+        
+        p = b.getProperties( new Locale( "de" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_de", p.get( "source.name" ) );
+        
+        p = b.getProperties( new Locale( "zh", "CN" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_zh_CN", p.get( "source.name" ) );
+    }
+
+    public void testNewBundle() throws Exception
+    {
+        // Source: verify the base file and all other files
+        Bundle bundle = new Bundle( m_source );
+        assertEquals( new File( m_source + ".properties" ), bundle.getFile( null ) );
+        Collection<Locale> map = bundle.getLocales();
+        assertEquals( 3, map.size() );
+        assertNotNull( bundle.getFile( new Locale( "en" ) ) );
+        assertNotNull( bundle.getFile( new Locale( "de" ) ) );
+        assertNotNull( bundle.getFile( new Locale( "zh", "CN" ) ) );
+        
+        // Dest: verify the base file and all other files
+        bundle = new Bundle( m_dest );
+        assertEquals( new File( m_dest + ".properties" ), bundle.getFile( null ) );
+        assertEquals( 3, map.size() );
+        assertNotNull( bundle.getFile( new Locale( "en" ) ) );
+        assertNotNull( bundle.getFile( new Locale( "de" ) ) );
+        assertNotNull( bundle.getFile( new Locale( "zh", "CN" ) ) );
+    }
+    
+    public void testRemoveKey() throws Exception
+    {
+        Bundle b = new Bundle( m_source );
+        BundleMigrator m = new BundleMigrator();
+        m.setBundle( b );
+        
+        // Remove a key, then save
+        m.remove( "source.name" );
+        
+        // Reload & verify key was deleted
+        b = new Bundle( m_source );
+        b.load();
+        
+        // Verify the contents were loaded
+        Properties p;
+        p = b.getProperties( null );
+        assertEquals( 2, p.size() );
+
+        p = b.getProperties( new Locale( "en" ) );
+        assertEquals( 2, p.size() );
+        
+        p = b.getProperties( new Locale( "de" ) );
+        assertEquals( 2, p.size() );
+        
+        p = b.getProperties( new Locale( "zh", "CN" ) );
+        assertEquals( 2, p.size() );
+    }
+    
+    public void testRenameKey() throws Exception
+    {
+        Bundle b = new Bundle( m_source );
+        BundleMigrator m = new BundleMigrator();
+        m.setBundle( b );
+        
+        // Rename a key, then save
+        m.rename( "source.name", "source.rename" );
+        
+        // Reload & verify key was deleted
+        b = new Bundle( m_source );
+        b.load();
+        
+        // Verify the contents were loaded
+        Properties p;
+        p = b.getProperties( null );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_", p.get( "source.rename" ) );
+
+        p = b.getProperties( new Locale( "en" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_en", p.get( "source.rename" ) );
+        
+        p = b.getProperties( new Locale( "de" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_de", p.get( "source.rename" ) );
+        
+        p = b.getProperties( new Locale( "zh", "CN" ) );
+        assertEquals( 3, p.size() );
+        assertEquals( "Full name_zh_CN", p.get( "source.rename" ) );
+    }
+
+    public void testSaveBundle() throws Exception
+    {
+        Bundle b = new Bundle( m_source );
+        b.load();
+        
+        // Modify the properties by adding a new key
+        Properties p;
+        p = b.getProperties( null );
+        p.put( "new.key", "Value_" );
+        p = b.getProperties( new Locale( "en" ) );
+        p.put( "new.key", "Value_en" );
+        p = b.getProperties( new Locale( "de" ) );
+        p.put( "new.key", "Value_de" );
+        p = b.getProperties( new Locale( "zh", "CN" ) );
+        p.put( "new.key", "Value_zh_CN" );
+        
+        // Save 'em
+        b.save();
+        
+        // Reload & verify contents were saved
+        b = new Bundle( m_source );
+        b.load();
+        
+        // Verify the contents were loaded
+        p = b.getProperties( null );
+        assertEquals( 4, p.size() );
+        assertEquals( "Value_", p.get( "new.key" ) );
+
+        p = b.getProperties( new Locale( "en" ) );
+        assertEquals( 4, p.size() );
+        assertEquals( "Value_en", p.get( "new.key" ) );
+        
+        p = b.getProperties( new Locale( "de" ) );
+        assertEquals( 4, p.size() );
+        assertEquals( "Value_de", p.get( "new.key" ) );
+        
+        p = b.getProperties( new Locale( "zh", "CN" ) );
+        assertEquals( 4, p.size() );
+        assertEquals( "Value_zh_CN", p.get( "new.key" ) );
+    }
+
+    public void testSetSource() throws Exception
+    {
+        BundleMigrator m = new BundleMigrator();
+
+        // Try with a bundle base file that obviously does not exist
+        try
+        {
+            m.setBundle( new Bundle( "src/Default" ) );
+        }
+        catch( FileNotFoundException e )
+        {
+            // Good! This is what we expect
+        }
+
+        // Try with one that does exist
+        m.setBundle( new Bundle( "etc/i18n/CoreResources" ) );
+    }
+
+}

Modified: incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/util/CommentedPropertiesTest.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/util/CommentedPropertiesTest.java?rev=738356&r1=738355&r2=738356&view=diff
==============================================================================
--- incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/util/CommentedPropertiesTest.java (original)
+++ incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/util/CommentedPropertiesTest.java Wed Jan 28 03:57:46 2009
@@ -21,11 +21,7 @@
 
 package com.ecyrd.jspwiki.util;
 
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.io.*;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
@@ -37,73 +33,107 @@
 
 public class CommentedPropertiesTest extends TestCase
 {
-    Properties m_props = new CommentedProperties();
-    
+    CommentedProperties m_props = new CommentedProperties();
+
     public void setUp() throws IOException
     {
         InputStream in = CommentedPropertiesTest.class.getClassLoader().getResourceAsStream( "test.properties" );
         m_props.load( in );
         in.close();
     }
-    
+
     public void testLoadProperties()
     {
-        assertEquals( 5, m_props.keySet().size() );
+        assertEquals( 6, m_props.keySet().size() );
         assertEquals( "Foo", m_props.get( "testProp1" ) );
         assertEquals( "Bar", m_props.get( "testProp2" ) );
         assertEquals( "", m_props.get( "testProp3" ) );
         assertEquals( "FooAgain", m_props.get( "testProp4" ) );
         assertEquals( "BarAgain", m_props.get( "testProp5" ) );
-        assertNull( m_props.get( "testProp6" ) );
-        
-        // String we read in, including comments is 208 bytes
-        assertEquals( 208, m_props.toString().length() );
+        assertNotNull( m_props.get( "testProp6" ) );
+        assertNull( m_props.get( "testProp7" ) );
     }
-    
+
     public void testSetProperty()
     {
+        int propsLen = m_props.toString().length();
         m_props.setProperty( "testProp1", "newValue" );
-        
+
         // Length of stored string should now be 5 bytes more
-        assertEquals( 208+5, m_props.toString().length() );
+        assertEquals( propsLen + 5, m_props.toString().length() );
         assertTrue( m_props.toString().indexOf( "newValue" ) != -1 );
-        
-        // Create new property; should add 21 (1+7+3+9+1) bytes
+
+        // Create new property; should add 20 (7+3+9+1) bytes
         m_props.setProperty( "newProp", "newValue2" );
         m_props.containsKey( "newProp" );
         m_props.containsValue( "newValue2" );
-        assertEquals( 208+5+21, m_props.toString().length() );
+        assertEquals( propsLen + 5 + 20, m_props.toString().length() );
         assertTrue( m_props.toString().indexOf( "newProp = newValue2" ) != -1 );
     }
+
+    public void testGetComment()
+    {
+        String cr = System.getProperty( "line.separator" );
+        assertEquals( "# This is a sample properties file with comments", m_props.getComment( "testProp1" ) );
+        assertEquals( "# This is a comment" + cr + "#   with two lines", m_props.getComment( "testProp2" ) );
+        assertEquals( "# This is a property with no value", m_props.getComment( "testProp3" ) );
+        assertEquals( "# Two final properties", m_props.getComment( "testProp4" ) );
+        assertEquals( null, m_props.getComment( "testProp5" ) );
+        assertEquals( "# This is a property that spans more than 1 line", m_props.getComment( "testProp6" ) );
+    }
+    
+    public void testSetComment()
+    {
+        m_props.setProperty( "testProp7", "TestValue","This is a comment" );
+        assertEquals( "TestValue", m_props.getProperty( "testProp7" ) );
+        assertEquals( "# This is a comment", m_props.getComment( "testProp7" ) );
+        
+        // Make sure it was actually added to the string returned by toString()
+        assertTrue( m_props.toString().contains( "# This is a comment\ntestProp7 = TestValue" ) );
+    }
     
+    public void testMultilineProperties()
+    {
+        assertTrue( m_props.containsKey( "testProp6" ) );
+        assertEquals( "Your new properties have been saved to jspwiki.properties.", m_props.get( "testProp6" ) );
+    }
+
     public void testRemove()
     {
-        // Remove prop 1; length of stored string should be 14 (1+9+1+3) bytes less
+        int propsLen = m_props.toString().length();
+
+        // Remove prop 1; length of stored string should be 16 (9+3+3+1) bytes
+        // less for property
+        // and 49 bytes less for the comment above it. Total difference: 65
+        // bytes
         m_props.remove( "testProp1" );
         assertFalse( m_props.containsKey( "testProp1" ) );
-        assertEquals( 208-14, m_props.toString().length() );
-        
-        // Remove prop 2; length of stored string should be 15 (1+9+2+3) bytes less
+        assertEquals( propsLen - 65, m_props.toString().length() );
+
+        // Remove prop 2; length of stored string should be 55 (20+19+16) bytes
+        // less
         m_props.remove( "testProp2" );
         assertFalse( m_props.containsKey( "testProp2" ) );
-        assertEquals( 208-14-15, m_props.toString().length() );
-        
-        // Remove prop 3; length of stored string should be 11 (1+9+1) bytes less
+        assertEquals( propsLen - 65 - 55, m_props.toString().length() );
+
+        // Remove prop 3; length of stored string should be 48 (35+13) bytes
+        // less
         m_props.remove( "testProp3" );
         assertFalse( m_props.containsKey( "testProp3" ) );
-        assertEquals( 208-14-15-11, m_props.toString().length() );
-        
-        // Remove prop 4; length of stored string should be 19 (1+9+1+8) bytes less
+        assertEquals( propsLen - 65 - 55 - 48, m_props.toString().length() );
+
+        // Remove prop 4; length of stored string should be 44 (23+21) bytes
+        // less
         m_props.remove( "testProp4" );
         assertFalse( m_props.containsKey( "testProp4" ) );
-        assertEquals( 208-14-15-11-19, m_props.toString().length() );
-        
-        // Remove prop 5; length of stored string should be 19 (1+9+1+8) bytes less
+        assertEquals( propsLen - 65 - 55 - 48 - 44, m_props.toString().length() );
+
+        // Remove prop 5; length of stored string should be 21 bytes less
         m_props.remove( "testProp5" );
         assertFalse( m_props.containsKey( "testProp5" ) );
-        assertEquals( 208-14-15-11-19-19, m_props.toString().length() );
+        assertEquals( propsLen - 65 - 55 - 48 - 44 - 21, m_props.toString().length() );
     }
-    
+
     public void testStore() throws Exception
     {
         // Write results to a new file
@@ -111,14 +141,14 @@
         OutputStream out = new FileOutputStream( outFile );
         m_props.store( out, null );
         out.close();
-        
+
         // Load the file into new props object; should return identical strings
         Properties props2 = new CommentedProperties();
         InputStream in = CommentedPropertiesTest.class.getClassLoader().getResourceAsStream( "test2.properties" );
         props2.load( in );
         in.close();
         assertEquals( m_props.toString(), props2.toString() );
-        
+
         // Remove props1, 2, 3 & resave props to new file
         m_props.remove( "testProp1" );
         m_props.remove( "testProp2" );
@@ -127,7 +157,7 @@
         out = new FileOutputStream( outFile );
         m_props.store( out, null );
         out.close();
-        
+
         // Load the new file; should not have props1/2/3 & is shorter
         Properties props3 = new CommentedProperties();
         in = CommentedPropertiesTest.class.getClassLoader().getResourceAsStream( "test3.properties" );
@@ -139,15 +169,15 @@
         assertFalse( props3.containsKey( "testProp3" ) );
         assertTrue( props3.containsKey( "testProp4" ) );
         assertTrue( props3.containsKey( "testProp5" ) );
-        
+
         // Clean up
         File file = getFile( "test2.properties" );
-        if ( file != null && file.exists() )
+        if( file != null && file.exists() )
         {
             file.delete();
         }
         file = getFile( "test3.properties" );
-        if ( file != null && file.exists() )
+        if( file != null && file.exists() )
         {
             file.delete();
         }
@@ -157,22 +187,22 @@
     {
         // Get the test.properties file
         URL url = CommentedPropertiesTest.class.getClassLoader().getResource( "test.properties" );
-        if ( url == null )
+        if( url == null )
         {
             throw new IllegalStateException( "Very odd. We can't find test.properties!" );
         }
-        
+
         // Construct new file in same directory
-        File testFile = new File( new URI(url.toString()) );
+        File testFile = new File( new URI( url.toString() ) );
         File dir = testFile.getParentFile();
         return new File( dir, file );
     }
-    
+
     private File getFile( String name )
     {
         // Get the test.properties file
         URL url = CommentedPropertiesTest.class.getClassLoader().getResource( name );
-        if ( url == null )
+        if( url == null )
         {
             throw new IllegalStateException( "Very odd. We can't find test.properties!" );
         }
@@ -181,9 +211,9 @@
 
         try
         {
-            file = new File( new URI(url.toString()) );
+            file = new File( new URI( url.toString() ) );
         }
-        catch (URISyntaxException e)
+        catch( URISyntaxException e )
         {
             // TODO Auto-generated catch block
             e.printStackTrace();
@@ -191,11 +221,9 @@
 
         return file;
     }
-        
+
     public static Test suite()
     {
         return new TestSuite( CommentedPropertiesTest.class );
     }
 }
-
-

Modified: incubator/jspwiki/trunk/tests/etc/test.properties
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/tests/etc/test.properties?rev=738356&r1=738355&r2=738356&view=diff
==============================================================================
--- incubator/jspwiki/trunk/tests/etc/test.properties (original)
+++ incubator/jspwiki/trunk/tests/etc/test.properties Wed Jan 28 03:57:46 2009
@@ -2,6 +2,7 @@
 testProp1=Foo
 
 # This is a comment
+#   with two lines
 testProp2 =Bar
 
 # This is a property with no value
@@ -10,3 +11,7 @@
 testProp4=FooAgain
 testProp5=BarAgain
 
+# This is a property that spans more than 1 line
+testProp6=Your new properties have been saved \
+    to jspwiki.properties.
+  
\ No newline at end of file