    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.nio.charset.StandardCharsets;
    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(final Properties defaultValues )
    058    {
    059        super( defaultValues );
    060    }
    061
    062    /**
    063     *  {@inheritDoc}
    064     */
    065    @Override
    066    public synchronized void load(final InputStream inStream ) throws IOException
    067    {
    068        // Load the file itself into a string
    069        m_propertyString = FileUtil.readContents( inStream, StandardCharsets.ISO_8859_1.name() );
    070
    071        // Now load it into the properties object as normal
    072        super.load( new ByteArrayInputStream( m_propertyString.getBytes(StandardCharsets.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    @Override
    082    public synchronized void load(final Reader in ) throws IOException
    083    {
    084        m_propertyString = FileUtil.readContents( in );
    085
    086        // Now load it into the properties object as normal
    087        super.load( new ByteArrayInputStream( m_propertyString.getBytes(StandardCharsets.ISO_8859_1) ) );
    088    }
    089
    090    /**
    091     * {@inheritDoc}
    092     */
    093    @Override
    094    public synchronized Object setProperty(final String key, final String value )
    095    {
    096        return put(key, value);
    097    }
    098
    099    /**
    100     * {@inheritDoc}
    101     */
    102    @Override
    103    public synchronized void store(final OutputStream out, final String comments ) throws IOException
    104    {
    105        final byte[] bytes = m_propertyString.getBytes( StandardCharsets.ISO_8859_1 );
    106        FileUtil.copyContents( new ByteArrayInputStream( bytes ), out );
    107        out.flush();
    108    }
    109
    110    /**
    111     * {@inheritDoc}
    112     */
    113    @Override
    114    public synchronized Object put(final Object arg0, final Object arg1 )
    115    {
    116        // Write the property to the stored string
    117        writeProperty( arg0, arg1 );
    118
    119        // Return the result of from the superclass properties object
    120        return super.put(arg0, arg1);
    121    }
    122
    123    /**
    124     * {@inheritDoc}
    125     */
    126    @Override
    127    public synchronized void putAll(final Map< ? , ? > arg0 )
    128    {
    129        // Shove all of the entries into the property string
    130        for (final Entry<?, ?> value : arg0.entrySet()) {
    131            @SuppressWarnings("unchecked") final Entry<Object, Object> entry = (Entry<Object, Object>) value;
    132            writeProperty(entry.getKey(), entry.getValue());
    133        }
    134
    135        // Call the superclass method
    136        super.putAll(arg0);
    137    }
    138
    139    /**
    140     * {@inheritDoc}
    141     */
    142    @Override
    143    public synchronized Object remove(final Object key )
    144    {
    145        // Remove from the property string
    146        deleteProperty( key );
    147
    148        // Call the superclass method
    149        return super.remove(key);
    150    }
    151
    152    /**
    153     * {@inheritDoc}
    154     */
    155    @Override
    156    public synchronized String toString()
    157    {
    158        return m_propertyString;
    159    }
    160
    161    private void deleteProperty(final Object arg0 )
    162    {
    163        // Get key and value
    164        if ( arg0 == null )
    165        {
    166            throw new IllegalArgumentException( "Key cannot be null." );
    167        }
    168        final String key = arg0.toString();
    169
    170        // Iterate through each line and replace anything matching our key
    171        int idx = 0;
    172        while( ( idx < m_propertyString.length() ) && ( ( idx = m_propertyString.indexOf( key, idx ) ) != -1 ) )
    173        {
    174            final int prevret = m_propertyString.lastIndexOf( "\n", idx );
    175            if ( prevret != -1 )
    176            {
    177                // Commented lines are skipped
    178                if ( m_propertyString.charAt( prevret + 1 ) == '#' )
    179                {
    180                    idx += key.length();
    181                    continue;
    182                }
    183            }
    184
    185            // If "=" present, delete the entire line
    186            final int eqsign = m_propertyString.indexOf( "=", idx );
    187            if ( eqsign != -1 )
    188            {
    189                final int ret = m_propertyString.indexOf( "\n", eqsign );
    190                m_propertyString = TextUtil.replaceString( m_propertyString, prevret, ret, "" );
    191                return;
    192            }
    193        }
    194    }
    195
    196    private void writeProperty(final Object arg0, Object arg1 )
    197    {
    198        // Get key and value
    199        if ( arg0 == null )
    200        {
    201            throw new IllegalArgumentException( "Key cannot be null." );
    202        }
    203        if ( arg1 == null )
    204        {
    205            arg1 = "";
    206        }
    207        final String key = arg0.toString();
    208        final String value = TextUtil.native2Ascii( arg1.toString() );
    209
    210        // Iterate through each line and replace anything matching our key
    211        int idx = 0;
    212        while( ( idx < m_propertyString.length() ) && ( ( idx = m_propertyString.indexOf( key, idx ) ) != -1 ) )
    213        {
    214            final int prevret = m_propertyString.lastIndexOf( "\n", idx );
    215            if ( prevret != -1 )
    216            {
    217                // Commented lines are skipped
    218                if ( m_propertyString.charAt( prevret + 1 ) == '#' )
    219                {
    220                    idx += key.length();
    221                    continue;
    222                }
    223            }
    224
    225            // If "=" present, replace everything in line after it
    226            final int eqsign = m_propertyString.indexOf( "=", idx );
    227            if ( eqsign != -1 )
    228            {
    229                int ret = m_propertyString.indexOf( "\n", eqsign );
    230                if ( ret == -1 )
    231                {
    232                    ret = m_propertyString.length();
    233                }
    234                m_propertyString = TextUtil.replaceString( m_propertyString, eqsign + 1, ret, value );
    235                return;
    236            }
    237        }
    238
    239        // If it was not found, we'll add it to the end.
    240        m_propertyString += "\n" + key + " = " + value + "\n";
    241    }
    242
    243}