001/* 
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.  
018 */
019package org.apache.wiki.util;
020
021import java.io.ByteArrayInputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.io.Reader;
026import java.util.Iterator;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.Properties;
030
031/**
032 * Extends {@link java.util.Properties} by providing support for comment
033 * preservation. When the properties are written to disk, previous
034 * comments present in the file are preserved.
035 * @since 2.4.22
036 */
037public class CommentedProperties extends Properties
038{
039    private static final long serialVersionUID = 8057284636436329669L;
040
041    private String m_propertyString;
042
043    /**
044     * @see java.util.Properties#Properties()
045     */
046    public CommentedProperties()
047    {
048        super();
049    }
050
051    /**
052     *  Creates new properties.
053     *
054     *  @param defaultValues A list of default values, which are used if in subsequent gets
055     *                       a key is not found.
056     */
057    public CommentedProperties( Properties defaultValues )
058    {
059        super( defaultValues );
060    }
061
062    /**
063     *  {@inheritDoc}
064     */
065    @Override
066    public synchronized void load( InputStream inStream ) throws IOException
067    {
068        // Load the file itself into a string
069        m_propertyString = FileUtil.readContents( inStream, "ISO-8859-1" );
070
071        // Now load it into the properties object as normal
072        super.load( new ByteArrayInputStream( m_propertyString.getBytes("ISO-8859-1") ) );
073    }
074
075    /**
076     *  Loads properties from a file opened by a supplied Reader.
077     *  
078     *  @param in The reader to read properties from
079     *  @throws IOException in case something goes wrong.
080     */
081    public synchronized void load( Reader in ) throws IOException
082    {
083        m_propertyString = FileUtil.readContents( in );
084
085        // Now load it into the properties object as normal
086        super.load( new ByteArrayInputStream( m_propertyString.getBytes("ISO-8859-1") ) );
087    }
088
089    /**
090     * {@inheritDoc}
091     */
092    @Override
093    public synchronized Object setProperty( String key, String value )
094    {
095        return put(key, value);
096    }
097
098    /**
099     * {@inheritDoc}
100     */
101    @Override
102    public synchronized void store( OutputStream out, String comments ) throws IOException
103    {
104        byte[] bytes = m_propertyString.getBytes("ISO-8859-1");
105        FileUtil.copyContents( new ByteArrayInputStream( bytes ), out );
106        out.flush();
107    }
108
109    /**
110     * {@inheritDoc}
111     */
112    @Override
113    public synchronized Object put( Object arg0, Object arg1 )
114    {
115        // Write the property to the stored string
116        writeProperty( arg0, arg1 );
117
118        // Return the result of from the superclass properties object
119        return super.put(arg0, arg1);
120    }
121
122    /**
123     * {@inheritDoc}
124     */
125    @SuppressWarnings("unchecked")
126    @Override
127    public synchronized void putAll( Map arg0 )
128    {
129        // Shove all of the entries into the property string
130        for( Iterator< Entry< ?, ? > > it = arg0.entrySet().iterator(); it.hasNext(); )
131        {
132            Entry< ?, ? > entry = it.next();
133            writeProperty( entry.getKey(), entry.getValue() );
134        }
135
136        // Call the superclass method
137        super.putAll(arg0);
138    }
139
140    /**
141     * {@inheritDoc}
142     */
143    @Override
144    public synchronized Object remove( Object key )
145    {
146        // Remove from the property string
147        deleteProperty( key );
148
149        // Call the superclass method
150        return super.remove(key);
151    }
152
153    /**
154     * {@inheritDoc}
155     */
156    @Override
157    public synchronized String toString()
158    {
159        return m_propertyString;
160    }
161
162    private void deleteProperty( Object arg0 )
163    {
164        // Get key and value
165        if ( arg0 == null )
166        {
167            throw new IllegalArgumentException( "Key cannot be null." );
168        }
169        String key = arg0.toString();
170
171        // Iterate through each line and replace anything matching our key
172        int idx = 0;
173        while( ( idx < m_propertyString.length() ) && ( ( idx = m_propertyString.indexOf( key, idx ) ) != -1 ) )
174        {
175            int prevret = m_propertyString.lastIndexOf( "\n", idx );
176            if ( prevret != -1 )
177            {
178                // Commented lines are skipped
179                if ( m_propertyString.charAt( prevret + 1 ) == '#' )
180                {
181                    idx += key.length();
182                    continue;
183                }
184            }
185
186            // If "=" present, delete the entire line
187            int eqsign = m_propertyString.indexOf( "=", idx );
188            if ( eqsign != -1 )
189            {
190                int ret = m_propertyString.indexOf( "\n", eqsign );
191                m_propertyString = TextUtil.replaceString( m_propertyString, prevret, ret, "" );
192                return;
193            }
194        }
195    }
196
197    private void writeProperty( Object arg0, Object arg1 )
198    {
199        // Get key and value
200        if ( arg0 == null )
201        {
202            throw new IllegalArgumentException( "Key cannot be null." );
203        }
204        if ( arg1 == null )
205        {
206            arg1 = "";
207        }
208        String key = arg0.toString();
209        String value = TextUtil.native2Ascii( arg1.toString() );
210
211        // Iterate through each line and replace anything matching our key
212        int idx = 0;
213        while( ( idx < m_propertyString.length() ) && ( ( idx = m_propertyString.indexOf( key, idx ) ) != -1 ) )
214        {
215            int prevret = m_propertyString.lastIndexOf( "\n", idx );
216            if ( prevret != -1 )
217            {
218                // Commented lines are skipped
219                if ( m_propertyString.charAt( prevret + 1 ) == '#' )
220                {
221                    idx += key.length();
222                    continue;
223                }
224            }
225
226            // If "=" present, replace everything in line after it
227            int eqsign = m_propertyString.indexOf( "=", idx );
228            if ( eqsign != -1 )
229            {
230                int ret = m_propertyString.indexOf( "\n", eqsign );
231                if ( ret == -1 )
232                {
233                    ret = m_propertyString.length();
234                }
235                m_propertyString = TextUtil.replaceString( m_propertyString, eqsign + 1, ret, value );
236                return;
237            }
238        }
239
240        // If it was not found, we'll add it to the end.
241        m_propertyString += "\n" + key + " = " + value + "\n";
242    }
243
244}