You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by gn...@apache.org on 2017/04/07 14:35:12 UTC

svn commit: r1790562 - in /felix/trunk/utils: ./ src/main/java/org/apache/felix/utils/properties/ src/test/java/org/apache/felix/utils/properties/ src/test/resources/

Author: gnodet
Date: Fri Apr  7 14:35:12 2017
New Revision: 1790562

URL: http://svn.apache.org/viewvc?rev=1790562&view=rev
Log:
[FELIX-5608] New TypedProperties object to support typed properties file

Added:
    felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/ConfigurationHandler.java
    felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/TypedProperties.java
    felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/TypedPropertiesTest.java
    felix/trunk/utils/src/test/resources/typed.properties
Modified:
    felix/trunk/utils/pom.xml
    felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java
    felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/Properties.java

Modified: felix/trunk/utils/pom.xml
URL: http://svn.apache.org/viewvc/felix/trunk/utils/pom.xml?rev=1790562&r1=1790561&r2=1790562&view=diff
==============================================================================
--- felix/trunk/utils/pom.xml (original)
+++ felix/trunk/utils/pom.xml Fri Apr  7 14:35:12 2017
@@ -27,7 +27,7 @@
     <modelVersion>4.0.0</modelVersion>
     <name>Apache Felix Utils</name>
     <description>Utility classes for OSGi.</description>
-    <version>1.9.1-SNAPSHOT</version>
+    <version>1.10.0-SNAPSHOT</version>
     <artifactId>org.apache.felix.utils</artifactId>
 
     <scm>

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/ConfigurationHandler.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/ConfigurationHandler.java?rev=1790562&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/ConfigurationHandler.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/ConfigurationHandler.java Fri Apr  7 14:35:12 2017
@@ -0,0 +1,874 @@
+/*
+ * 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 org.apache.felix.utils.properties;
+
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PushbackReader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * The <code>ConfigurationHandler</code> class implements configuration reading
+ * form a <code>java.io.InputStream</code> and writing to a
+ * <code>java.io.OutputStream</code> on behalf of the
+ * {@link FilePersistenceManager} class.
+ *
+ * <pre>
+ * cfg = prop &quot;=&quot; value .
+ *  prop = symbolic-name . // 1.4.2 of OSGi Core Specification
+ *  symbolic-name = token { &quot;.&quot; token } .
+ *  token = { [ 0..9 ] | [ a..z ] | [ A..Z ] | '_' | '-' } .
+ *  value = [ type ] ( &quot;[&quot; values &quot;]&quot; | &quot;(&quot; values &quot;)&quot; | simple ) .
+ *  values = simple { &quot;,&quot; simple } .
+ *  simple = &quot;&quot;&quot; stringsimple &quot;&quot;&quot; .
+ *  type = // 1-char type code .
+ *  stringsimple = // quoted string representation of the value .
+ * </pre>
+ */
+public class ConfigurationHandler
+{
+    protected static final String ENCODING = "UTF-8";
+
+    protected static final int TOKEN_NAME = 'N';
+    protected static final int TOKEN_EQ = '=';
+    protected static final int TOKEN_ARR_OPEN = '[';
+    protected static final int TOKEN_ARR_CLOS = ']';
+    protected static final int TOKEN_VEC_OPEN = '(';
+    protected static final int TOKEN_VEC_CLOS = ')';
+    protected static final int TOKEN_COMMA = ',';
+    protected static final int TOKEN_VAL_OPEN = '"'; // '{';
+    protected static final int TOKEN_VAL_CLOS = '"'; // '}';
+
+    protected static final int TOKEN_COMMENT = '#';
+
+    // simple types (string & primitive wrappers)
+    protected static final int TOKEN_SIMPLE_STRING = 'T';
+    protected static final int TOKEN_SIMPLE_INTEGER = 'I';
+    protected static final int TOKEN_SIMPLE_LONG = 'L';
+    protected static final int TOKEN_SIMPLE_FLOAT = 'F';
+    protected static final int TOKEN_SIMPLE_DOUBLE = 'D';
+    protected static final int TOKEN_SIMPLE_BYTE = 'X';
+    protected static final int TOKEN_SIMPLE_SHORT = 'S';
+    protected static final int TOKEN_SIMPLE_CHARACTER = 'C';
+    protected static final int TOKEN_SIMPLE_BOOLEAN = 'B';
+
+    // primitives
+    protected static final int TOKEN_PRIMITIVE_INT = 'i';
+    protected static final int TOKEN_PRIMITIVE_LONG = 'l';
+    protected static final int TOKEN_PRIMITIVE_FLOAT = 'f';
+    protected static final int TOKEN_PRIMITIVE_DOUBLE = 'd';
+    protected static final int TOKEN_PRIMITIVE_BYTE = 'x';
+    protected static final int TOKEN_PRIMITIVE_SHORT = 's';
+    protected static final int TOKEN_PRIMITIVE_CHAR = 'c';
+    protected static final int TOKEN_PRIMITIVE_BOOLEAN = 'b';
+
+    protected static final String CRLF = "\r\n";
+    protected static final String INDENT = "  ";
+    protected static final String COLLECTION_LINE_BREAK = " \\\r\n";
+
+    protected static final Map code2Type;
+    protected static final Map type2Code;
+
+    // set of valid characters for "symblic-name"
+    private static final BitSet NAME_CHARS;
+    private static final BitSet TOKEN_CHARS;
+
+    static
+    {
+        type2Code = new HashMap();
+
+        // simple (exclusive String whose type code is not written)
+        type2Code.put( Integer.class, new Integer( TOKEN_SIMPLE_INTEGER ) );
+        type2Code.put( Long.class, new Integer( TOKEN_SIMPLE_LONG ) );
+        type2Code.put( Float.class, new Integer( TOKEN_SIMPLE_FLOAT ) );
+        type2Code.put( Double.class, new Integer( TOKEN_SIMPLE_DOUBLE ) );
+        type2Code.put( Byte.class, new Integer( TOKEN_SIMPLE_BYTE ) );
+        type2Code.put( Short.class, new Integer( TOKEN_SIMPLE_SHORT ) );
+        type2Code.put( Character.class, new Integer( TOKEN_SIMPLE_CHARACTER ) );
+        type2Code.put( Boolean.class, new Integer( TOKEN_SIMPLE_BOOLEAN ) );
+
+        // primitives
+        type2Code.put( Integer.TYPE, new Integer( TOKEN_PRIMITIVE_INT ) );
+        type2Code.put( Long.TYPE, new Integer( TOKEN_PRIMITIVE_LONG ) );
+        type2Code.put( Float.TYPE, new Integer( TOKEN_PRIMITIVE_FLOAT ) );
+        type2Code.put( Double.TYPE, new Integer( TOKEN_PRIMITIVE_DOUBLE ) );
+        type2Code.put( Byte.TYPE, new Integer( TOKEN_PRIMITIVE_BYTE ) );
+        type2Code.put( Short.TYPE, new Integer( TOKEN_PRIMITIVE_SHORT ) );
+        type2Code.put( Character.TYPE, new Integer( TOKEN_PRIMITIVE_CHAR ) );
+        type2Code.put( Boolean.TYPE, new Integer( TOKEN_PRIMITIVE_BOOLEAN ) );
+
+        // reverse map to map type codes to classes, string class mapping
+        // to be added manually, as the string type code is not written and
+        // hence not included in the type2Code map
+        code2Type = new HashMap();
+        for ( Iterator ti = type2Code.entrySet().iterator(); ti.hasNext(); )
+        {
+            Map.Entry entry = ( Map.Entry ) ti.next();
+            code2Type.put( entry.getValue(), entry.getKey() );
+        }
+        code2Type.put( new Integer( TOKEN_SIMPLE_STRING ), String.class );
+
+        NAME_CHARS = new BitSet();
+        for ( int i = '0'; i <= '9'; i++ )
+            NAME_CHARS.set( i );
+        for ( int i = 'a'; i <= 'z'; i++ )
+            NAME_CHARS.set( i );
+        for ( int i = 'A'; i <= 'Z'; i++ )
+            NAME_CHARS.set( i );
+        NAME_CHARS.set( '_' );
+        NAME_CHARS.set( '-' );
+        NAME_CHARS.set( '.' );
+        NAME_CHARS.set( '\\' );
+
+        TOKEN_CHARS = new BitSet();
+        TOKEN_CHARS.set( TOKEN_EQ );
+        TOKEN_CHARS.set( TOKEN_ARR_OPEN );
+        TOKEN_CHARS.set( TOKEN_ARR_CLOS );
+        TOKEN_CHARS.set( TOKEN_VEC_OPEN );
+        TOKEN_CHARS.set( TOKEN_VEC_CLOS );
+        TOKEN_CHARS.set( TOKEN_COMMA );
+        TOKEN_CHARS.set( TOKEN_VAL_OPEN );
+        TOKEN_CHARS.set( TOKEN_VAL_CLOS );
+        TOKEN_CHARS.set( TOKEN_SIMPLE_STRING );
+        TOKEN_CHARS.set( TOKEN_SIMPLE_INTEGER );
+        TOKEN_CHARS.set( TOKEN_SIMPLE_LONG );
+        TOKEN_CHARS.set( TOKEN_SIMPLE_FLOAT );
+        TOKEN_CHARS.set( TOKEN_SIMPLE_DOUBLE );
+        TOKEN_CHARS.set( TOKEN_SIMPLE_BYTE );
+        TOKEN_CHARS.set( TOKEN_SIMPLE_SHORT );
+        TOKEN_CHARS.set( TOKEN_SIMPLE_CHARACTER );
+        TOKEN_CHARS.set( TOKEN_SIMPLE_BOOLEAN );
+
+        // primitives
+        TOKEN_CHARS.set( TOKEN_PRIMITIVE_INT );
+        TOKEN_CHARS.set( TOKEN_PRIMITIVE_LONG );
+        TOKEN_CHARS.set( TOKEN_PRIMITIVE_FLOAT );
+        TOKEN_CHARS.set( TOKEN_PRIMITIVE_DOUBLE );
+        TOKEN_CHARS.set( TOKEN_PRIMITIVE_BYTE );
+        TOKEN_CHARS.set( TOKEN_PRIMITIVE_SHORT );
+        TOKEN_CHARS.set( TOKEN_PRIMITIVE_CHAR );
+        TOKEN_CHARS.set( TOKEN_PRIMITIVE_BOOLEAN );
+    }
+
+
+    /**
+     * Writes the configuration data from the <code>Dictionary</code> to the
+     * given <code>OutputStream</code>.
+     * <p>
+     * This method writes at the current location in the stream and does not
+     * close the outputstream.
+     *
+     * @param out
+     *            The <code>OutputStream</code> to write the configurtion data
+     *            to.
+     * @param properties
+     *            The <code>Dictionary</code> to write.
+     * @throws IOException
+     *             If an error occurrs writing to the output stream.
+     */
+    public static void write( OutputStream out, Dictionary properties ) throws IOException
+    {
+        BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( out, ENCODING ) );
+
+        for ( Enumeration ce = orderedKeys(properties); ce.hasMoreElements(); )
+        {
+            String key = ( String ) ce.nextElement();
+
+            // cfg = prop "=" value "." .
+            writeQuoted( bw, key );
+            bw.write( TOKEN_EQ );
+            writeValue( bw, properties.get( key ) );
+            bw.write( CRLF );
+        }
+
+        bw.flush();
+    }
+
+    public static String write( Object value ) throws IOException
+    {
+        StringWriter sw = new StringWriter();
+        writeValue(sw, value);
+        return sw.toString();
+    }
+
+    /**
+     * Generates an <code>Enumeration</code> for the given
+     * <code>Dictionary</code> where the keys of the <code>Dictionary</code>
+     * are provided in sorted order.
+     *
+     * @param properties
+     *                   The <code>Dictionary</code> that keys are sorted.
+     * @return An <code>Enumeration</code> that provides the keys of
+     *         properties in an ordered manner.
+     */
+    private static Enumeration orderedKeys(Dictionary properties) {
+        String[] keyArray = new String[properties.size()];
+        int i = 0;
+        for ( Enumeration ce = properties.keys(); ce.hasMoreElements(); )
+        {
+            keyArray[i] = ( String ) ce.nextElement();
+            i++;
+        }
+        Arrays.sort(keyArray);
+        return Collections.enumeration( Arrays.asList( keyArray ) );
+    }
+
+
+    /**
+     * Reads configuration data from the given <code>InputStream</code> and
+     * returns a new <code>Dictionary</code> object containing the data.
+     * <p>
+     * This method reads from the current location in the stream upto the end of
+     * the stream but does not close the stream at the end.
+     *
+     * @param ins
+     *            The <code>InputStream</code> from which to read the
+     *            configuration data.
+     * @return A <code>Dictionary</code> object containing the configuration
+     *         data. This object may be empty if the stream contains no
+     *         configuration data.
+     * @throws IOException
+     *             If an error occurrs reading from the stream. This exception
+     *             is also thrown if a syntax error is encountered.
+     */
+    public static Dictionary read( InputStream ins ) throws IOException
+    {
+        return new ConfigurationHandler().readInternal( ins );
+    }
+
+    public static Object read( String value ) throws IOException
+    {
+        PushbackReader pr = new PushbackReader( new StringReader( value ), 1 );
+        ConfigurationHandler ch = new ConfigurationHandler();
+        return ch.readValue(pr);
+    }
+
+    // private constructor, this class is not to be instantiated from the
+    // outside
+    private ConfigurationHandler()
+    {
+    }
+
+    // ---------- Configuration Input Implementation ---------------------------
+
+    private int token;
+    private String tokenValue;
+    private int line;
+    private int pos;
+
+
+    private Dictionary readInternal( InputStream ins ) throws IOException
+    {
+        BufferedReader br = new BufferedReader( new InputStreamReader( ins, ENCODING ) );
+        PushbackReader pr = new PushbackReader( br, 1 );
+
+        token = 0;
+        tokenValue = null;
+        line = 0;
+        pos = 0;
+
+        Hashtable configuration = new Hashtable();
+        token = 0;
+        while ( nextToken( pr, true ) == TOKEN_NAME )
+        {
+            String key = tokenValue;
+
+            // expect equal sign
+            if ( nextToken( pr, false ) != TOKEN_EQ )
+            {
+                throw readFailure( token, TOKEN_EQ );
+            }
+
+            // expect the token value
+            Object value = readValue( pr );
+            if ( value != null )
+            {
+                configuration.put( key, value );
+            }
+        }
+
+        return configuration;
+    }
+
+
+    /**
+     * value = type ( "[" values "]" | "(" values ")" | simple ) .
+     * values = value { "," value } .
+     * simple = "{" stringsimple "}" .
+     * type = // 1-char type code .
+     * stringsimple = // quoted string representation of the value .
+     *
+     * @param pr
+     * @return
+     * @throws IOException
+     */
+    private Object readValue( PushbackReader pr ) throws IOException
+    {
+        // read (optional) type code
+        int type = read( pr );
+
+        // read value kind code if type code is not a value kinde code
+        int code;
+        if ( code2Type.containsKey( new Integer( type ) ) )
+        {
+            code = read( pr );
+        }
+        else
+        {
+            code = type;
+            type = TOKEN_SIMPLE_STRING;
+        }
+
+        switch ( code )
+        {
+            case TOKEN_ARR_OPEN:
+                return readArray( type, pr );
+
+            case TOKEN_VEC_OPEN:
+                return readCollection( type, pr );
+
+            case TOKEN_VAL_OPEN:
+                Object value = readSimple( type, pr );
+                ensureNext( pr, TOKEN_VAL_CLOS );
+                return value;
+
+            default:
+                return null;
+        }
+    }
+
+
+    private Object readArray( int typeCode, PushbackReader pr ) throws IOException
+    {
+        List list = new ArrayList();
+        for ( ;; )
+        {
+            int c = ignorablePageBreakAndWhiteSpace( pr );
+            if ( c == TOKEN_VAL_OPEN )
+            {
+                Object value = readSimple( typeCode, pr );
+                if ( value == null )
+                {
+                    // abort due to error
+                    return null;
+                }
+
+                ensureNext( pr, TOKEN_VAL_CLOS );
+
+                list.add( value );
+
+                c = ignorablePageBreakAndWhiteSpace( pr );
+            }
+
+            if ( c == TOKEN_ARR_CLOS )
+            {
+                Class type = ( Class ) code2Type.get( new Integer( typeCode ) );
+                Object array = Array.newInstance( type, list.size() );
+                for ( int i = 0; i < list.size(); i++ )
+                {
+                    Array.set( array, i, list.get( i ) );
+                }
+                return array;
+            }
+            else if ( c < 0 )
+            {
+                return null;
+            }
+            else if ( c != TOKEN_COMMA )
+            {
+                return null;
+            }
+        }
+    }
+
+
+    private Collection readCollection( int typeCode, PushbackReader pr ) throws IOException
+    {
+        Collection collection = new ArrayList();
+        for ( ;; )
+        {
+            int c = ignorablePageBreakAndWhiteSpace( pr );
+            if ( c == TOKEN_VAL_OPEN )
+            {
+                Object value = readSimple( typeCode, pr );
+                if ( value == null )
+                {
+                    // abort due to error
+                    return null;
+                }
+
+                ensureNext( pr, TOKEN_VAL_CLOS );
+
+                collection.add( value );
+
+                c = ignorablePageBreakAndWhiteSpace( pr );
+            }
+
+            if ( c == TOKEN_VEC_CLOS )
+            {
+                return collection;
+            }
+            else if ( c < 0 )
+            {
+                return null;
+            }
+            else if ( c != TOKEN_COMMA )
+            {
+                return null;
+            }
+        }
+    }
+
+
+    private Object readSimple( int code, PushbackReader pr ) throws IOException
+    {
+        switch ( code )
+        {
+            case -1:
+                return null;
+
+            case TOKEN_SIMPLE_STRING:
+                return readQuoted( pr );
+
+                // Simple/Primitive, only use wrapper classes
+            case TOKEN_SIMPLE_INTEGER:
+            case TOKEN_PRIMITIVE_INT:
+                return Integer.valueOf( readQuoted( pr ) );
+
+            case TOKEN_SIMPLE_LONG:
+            case TOKEN_PRIMITIVE_LONG:
+                return Long.valueOf( readQuoted( pr ) );
+
+            case TOKEN_SIMPLE_FLOAT:
+            case TOKEN_PRIMITIVE_FLOAT:
+                int fBits = Integer.parseInt( readQuoted( pr ) );
+                return new Float( Float.intBitsToFloat( fBits ) );
+
+            case TOKEN_SIMPLE_DOUBLE:
+            case TOKEN_PRIMITIVE_DOUBLE:
+                long dBits = Long.parseLong( readQuoted( pr ) );
+                return new Double( Double.longBitsToDouble( dBits ) );
+
+            case TOKEN_SIMPLE_BYTE:
+            case TOKEN_PRIMITIVE_BYTE:
+                return Byte.valueOf( readQuoted( pr ) );
+
+            case TOKEN_SIMPLE_SHORT:
+            case TOKEN_PRIMITIVE_SHORT:
+                return Short.valueOf( readQuoted( pr ) );
+
+            case TOKEN_SIMPLE_CHARACTER:
+            case TOKEN_PRIMITIVE_CHAR:
+                String cString = readQuoted( pr );
+                if ( cString != null && cString.length() > 0 )
+                {
+                    return new Character( cString.charAt( 0 ) );
+                }
+                return null;
+
+            case TOKEN_SIMPLE_BOOLEAN:
+            case TOKEN_PRIMITIVE_BOOLEAN:
+                return Boolean.valueOf( readQuoted( pr ) );
+
+                // unknown type code
+            default:
+                return null;
+        }
+    }
+
+
+    private void ensureNext( PushbackReader pr, int expected ) throws IOException
+    {
+        int next = read( pr );
+        if ( next != expected )
+        {
+            readFailure( next, expected );
+        }
+    }
+
+
+    private String readQuoted( PushbackReader pr ) throws IOException
+    {
+        StringBuffer buf = new StringBuffer();
+        for ( ;; )
+        {
+            int c = read( pr );
+            switch ( c )
+            {
+                // escaped character
+                case '\\':
+                    c = read( pr );
+                    switch ( c )
+                    {
+                        // well known escapes
+                        case 'b':
+                            buf.append( '\b' );
+                            break;
+                        case 't':
+                            buf.append( '\t' );
+                            break;
+                        case 'n':
+                            buf.append( '\n' );
+                            break;
+                        case 'f':
+                            buf.append( '\f' );
+                            break;
+                        case 'r':
+                            buf.append( '\r' );
+                            break;
+                        case 'u':// need 4 characters !
+                            char[] cbuf = new char[4];
+                            if ( read( pr, cbuf ) == 4 )
+                            {
+                                c = Integer.parseInt( new String( cbuf ), 16 );
+                                buf.append( ( char ) c );
+                            }
+                            break;
+
+                        // just an escaped character, unescape
+                        default:
+                            buf.append( ( char ) c );
+                    }
+                    break;
+
+                // eof
+                case -1: // fall through
+
+                // separator token
+                case TOKEN_EQ:
+                case TOKEN_VAL_CLOS:
+                    pr.unread( c );
+                    return buf.toString();
+
+                // no escaping
+                default:
+                    buf.append( ( char ) c );
+            }
+        }
+    }
+
+    private int nextToken( PushbackReader pr, final boolean newLine ) throws IOException
+    {
+        int c = ignorableWhiteSpace( pr );
+
+        // immediately return EOF
+        if ( c < 0 )
+        {
+            return ( token = c );
+        }
+
+        // check for comment
+        if ( newLine && c == TOKEN_COMMENT )
+        {
+            // skip everything until end of line
+            do
+            {
+                c = read( pr );
+            } while ( c != -1 && c != '\n' );
+            if ( c == -1 )
+            {
+                return ( token = c);
+            }
+            // and start over
+            return nextToken( pr, true );
+        }
+
+        // check whether there is a name
+        if ( NAME_CHARS.get( c ) || !TOKEN_CHARS.get( c ) )
+        {
+            // read the property name
+            pr.unread( c );
+            tokenValue = readQuoted( pr );
+            return ( token = TOKEN_NAME );
+        }
+
+        // check another token
+        if ( TOKEN_CHARS.get( c ) )
+        {
+            return ( token = c );
+        }
+
+        // unexpected character -> so what ??
+        return ( token = -1 );
+    }
+
+
+    private int ignorableWhiteSpace( PushbackReader pr ) throws IOException
+    {
+        int c = read( pr );
+        while ( c >= 0 && Character.isWhitespace( ( char ) c ) )
+        {
+            c = read( pr );
+        }
+        return c;
+    }
+
+
+    private int ignorablePageBreakAndWhiteSpace( PushbackReader pr ) throws IOException
+    {
+        int c = ignorableWhiteSpace( pr );
+        for ( ;; )
+        {
+            if ( c != '\\' )
+            {
+                break;
+            }
+            int c1 = pr.read();
+            if ( c1 == '\r' || c1 == '\n' )
+            {
+                c = ignorableWhiteSpace( pr );
+            } else {
+                pr.unread(c1);
+                break;
+            }
+        }
+        return c;
+    }
+
+
+    private int read( PushbackReader pr ) throws IOException
+    {
+        int c = pr.read();
+        if ( c == '\r' )
+        {
+            int c1 = pr.read();
+            if ( c1 != '\n' )
+            {
+                pr.unread( c1 );
+            }
+            c = '\n';
+        }
+
+        if ( c == '\n' )
+        {
+            line++;
+            pos = 0;
+        }
+        else
+        {
+            pos++;
+        }
+
+        return c;
+    }
+
+
+    private int read( PushbackReader pr, char[] buf ) throws IOException
+    {
+        for ( int i = 0; i < buf.length; i++ )
+        {
+            int c = read( pr );
+            if ( c >= 0 )
+            {
+                buf[i] = ( char ) c;
+            }
+            else
+            {
+                return i;
+            }
+        }
+
+        return buf.length;
+    }
+
+
+    private IOException readFailure( int current, int expected )
+    {
+        return new IOException( "Unexpected token " + current + "; expected: " + expected + " (line=" + line + ", pos="
+            + pos + ")" );
+    }
+
+
+    // ---------- Configuration Output Implementation --------------------------
+
+    private static void writeValue( Writer out, Object value ) throws IOException
+    {
+        Class clazz = value.getClass();
+        if ( clazz.isArray() )
+        {
+            writeArray( out, value );
+        }
+        else if ( value instanceof Collection )
+        {
+            writeCollection( out, ( Collection ) value );
+        }
+        else
+        {
+            writeType( out, clazz );
+            writeSimple( out, value );
+        }
+    }
+
+
+    private static void writeArray( Writer out, Object arrayValue ) throws IOException
+    {
+        int size = Array.getLength( arrayValue );
+        writeType( out, arrayValue.getClass().getComponentType() );
+        out.write( TOKEN_ARR_OPEN );
+        out.write( COLLECTION_LINE_BREAK );
+        for ( int i = 0; i < size; i++ )
+        {
+            writeCollectionElement(out, Array.get( arrayValue, i ));
+        }
+        out.write( INDENT );
+        out.write( TOKEN_ARR_CLOS );
+    }
+
+
+    private static void writeCollection( Writer out, Collection collection ) throws IOException
+    {
+        if ( collection.isEmpty() )
+        {
+            out.write( TOKEN_VEC_OPEN );
+            out.write( COLLECTION_LINE_BREAK );
+            out.write( TOKEN_VEC_CLOS );
+        }
+        else
+        {
+            Iterator ci = collection.iterator();
+            Object firstElement = ci.next();
+
+            writeType( out, firstElement.getClass() );
+            out.write( TOKEN_VEC_OPEN );
+            out.write( COLLECTION_LINE_BREAK );
+
+            writeCollectionElement( out, firstElement );
+
+            while ( ci.hasNext() )
+            {
+                writeCollectionElement( out, ci.next() );
+            }
+            out.write( TOKEN_VEC_CLOS );
+        }
+    }
+
+
+    private static void writeCollectionElement(Writer out, Object element) throws IOException {
+        out.write( INDENT );
+        writeSimple( out, element );
+        out.write( TOKEN_COMMA );
+        out.write(COLLECTION_LINE_BREAK);
+    }
+
+
+    private static void writeType( Writer out, Class valueType ) throws IOException
+    {
+        Integer code = ( Integer ) type2Code.get( valueType );
+        if ( code != null )
+        {
+            out.write( ( char ) code.intValue() );
+        }
+    }
+
+
+    private static void writeSimple( Writer out, Object value ) throws IOException
+    {
+        if ( value instanceof Double )
+        {
+            double dVal = ( ( Double ) value ).doubleValue();
+            value = new Long( Double.doubleToRawLongBits( dVal ) );
+        }
+        else if ( value instanceof Float )
+        {
+            float fVal = ( ( Float ) value ).floatValue();
+            value = new Integer( Float.floatToRawIntBits( fVal ) );
+        }
+
+        out.write( TOKEN_VAL_OPEN );
+        writeQuoted( out, String.valueOf( value ) );
+        out.write( TOKEN_VAL_CLOS );
+    }
+
+
+    private static void writeQuoted( Writer out, String simple ) throws IOException
+    {
+        if ( simple == null || simple.length() == 0 )
+        {
+            return;
+        }
+
+        char c = 0;
+        int len = simple.length();
+        for ( int i = 0; i < len; i++ )
+        {
+            c = simple.charAt( i );
+            switch ( c )
+            {
+                case '\\':
+                case TOKEN_VAL_CLOS:
+                case ' ':
+                case TOKEN_EQ:
+                    out.write( '\\' );
+                    out.write( c );
+                    break;
+
+                // well known escapes
+                case '\b':
+                    out.write( "\\b" );
+                    break;
+                case '\t':
+                    out.write( "\\t" );
+                    break;
+                case '\n':
+                    out.write( "\\n" );
+                    break;
+                case '\f':
+                    out.write( "\\f" );
+                    break;
+                case '\r':
+                    out.write( "\\r" );
+                    break;
+
+                // other escaping
+                default:
+                    if ( c < ' ' )
+                    {
+                        String t = "000" + Integer.toHexString( c );
+                        out.write( "\\u" + t.substring( t.length() - 4 ) );
+                    }
+                    else
+                    {
+                        out.write( c );
+                    }
+            }
+        }
+    }
+}

Modified: felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java?rev=1790562&r1=1790561&r2=1790562&view=diff
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java (original)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java Fri Apr  7 14:35:12 2017
@@ -46,7 +46,7 @@ public class InterpolationHelper {
      */
     public interface SubstitutionCallback {
 
-        public String getValue(String key);
+        String getValue(String key);
 
     }
 
@@ -298,6 +298,7 @@ public class InterpolationHelper {
         // return the existing value.
         if ((startDelim < 0) || (stopDelim < 0))
         {
+            cycleMap.remove(currentKey);
             return val;
         }
 
@@ -397,6 +398,8 @@ public class InterpolationHelper {
         // be substitutions to make.
         val = doSubstVars(val, currentKey, cycleMap, configProps, callback, substituteFromConfig, substituteFromSystemProperties, defaultsToEmptyString);
 
+        cycleMap.remove(currentKey);
+
         // Return the value.
         return val;
     }

Modified: felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/Properties.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/Properties.java?rev=1790562&r1=1790561&r2=1790562&view=diff
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/Properties.java (original)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/Properties.java Fri Apr  7 14:35:12 2017
@@ -68,7 +68,7 @@ public class Properties extends Abstract
      * The default encoding (ISO-8859-1 as specified by
      * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
      */
-    private static final String DEFAULT_ENCODING = "ISO-8859-1";
+    static final String DEFAULT_ENCODING = "ISO-8859-1";
 
     /** Constant for the platform specific line separator.*/
     private static final String LINE_SEPARATOR = AccessController.doPrivileged(new PrivilegedAction<String>() {
@@ -89,7 +89,8 @@ public class Properties extends Abstract
     private List<String> footer;
     private File location;
     private InterpolationHelper.SubstitutionCallback callback;
-    private boolean substitute = true;
+    boolean substitute = true;
+    boolean typed;
 
     public Properties() {
     }
@@ -109,11 +110,11 @@ public class Properties extends Abstract
             load(location);
     }
 
-    public Properties(boolean substitute) throws IOException {
+    public Properties(boolean substitute) {
         this.substitute = substitute;
     }
 
-    public Properties(File location, boolean substitute) throws IOException {
+    public Properties(File location, boolean substitute) {
         this.location = location;
         this.substitute = substitute;
     }
@@ -141,7 +142,7 @@ public class Properties extends Abstract
     }
 
     public void load(Reader reader) throws IOException {
-        loadLayout(reader);
+        loadLayout(reader, false);
     }
 
     public void save() throws IOException {
@@ -162,7 +163,7 @@ public class Properties extends Abstract
     }
 
     public void save(Writer writer) throws IOException {
-        saveLayout(writer);
+        saveLayout(writer, typed);
     }
 
     /**
@@ -291,17 +292,18 @@ public class Properties extends Abstract
             sb.append(escapedKey).append("=");
         } else {
             String val0 = valueLines.get(0);
+            String rv0 = typed ? val0 : escapeJava(val0);
             if (!val0.trim().startsWith(escapedKey)) {
-                valueLines.set(0, escapedKey + " = " + escapeJava(val0) /*+ (0 < lastLine? "\\": "")*/);
-                sb.append(escapedKey).append(" = ").append(escapeJava(val0));
+                valueLines.set(0, escapedKey + " = " + rv0 /*+ (0 < lastLine? "\\": "")*/);
+                sb.append(escapedKey).append(" = ").append(rv0);
             } else {
-                valueLines.set(0, escapeJava(val0) /*+ (0 < lastLine? "\\": "")*/);
-                sb.append(escapeJava(val0));
+                valueLines.set(0, rv0 /*+ (0 < lastLine? "\\": "")*/);
+                sb.append(rv0);
             }
         }
         for (int i = 1; i < valueLines.size(); i++) {
             String val = valueLines.get(i);
-            valueLines.set(i, escapeJava(val) /*+ (i < lastLine? "\\": "")*/);
+            valueLines.set(i, typed ? val : escapeJava(val) /*+ (i < lastLine? "\\": "")*/);
             while (val.length() > 0 && Character.isWhitespace(val.charAt(0))) {
                 val = val.substring(1);
             }
@@ -449,9 +451,9 @@ public class Properties extends Abstract
      * @param in the reader to the properties file
      * @throws java.io.IOException if an error occurs
      */
-    protected void loadLayout(Reader in) throws IOException
+    protected void loadLayout(Reader in, boolean maybeTyped) throws IOException
     {
-        PropertiesReader reader = new PropertiesReader(in);
+        PropertiesReader reader = new PropertiesReader(in, maybeTyped);
         boolean hasProperty = false;
         while (reader.nextProperty())
         {
@@ -464,6 +466,12 @@ public class Properties extends Abstract
                                     null,
                                new ArrayList<String>(reader.getValueLines())));
         }
+        typed = maybeTyped && reader.typed != null && reader.typed;
+        if (!typed) {
+            for (Map.Entry<String,String> e : storage.entrySet()) {
+                e.setValue(unescapeJava(e.getValue()));
+            }
+        }
         if (hasProperty) {
             footer = new ArrayList<String>(reader.getCommentLines());
         } else {
@@ -482,14 +490,11 @@ public class Properties extends Abstract
 
     public void substitute(InterpolationHelper.SubstitutionCallback callback)
     {
-        if(callback != null)
+        if (callback == null)
         {
-            InterpolationHelper.performSubstitution(storage, callback);
-        }
-        else {
-            InterpolationHelper.performSubstitution(storage);
+            callback = new InterpolationHelper.BundleContextSubstitutionCallback(null);
         }
-
+        InterpolationHelper.performSubstitution(storage, callback);
     }
 
     /**
@@ -499,9 +504,9 @@ public class Properties extends Abstract
      * @param out the writer
      * @throws java.io.IOException if an error occurs
      */
-    protected void saveLayout(Writer out) throws IOException
+    protected void saveLayout(Writer out, boolean typed) throws IOException
     {
-        PropertiesWriter writer = new PropertiesWriter(out);
+        PropertiesWriter writer = new PropertiesWriter(out, typed);
         if (header != null)
         {
             for (String s : header)
@@ -853,17 +858,23 @@ public class Properties extends Abstract
         /** Stores the value of the last read property.*/
         private String propertyValue;
 
+        private boolean maybeTyped;
+
+        /** Stores if the properties are typed or not */
+        Boolean typed;
+
         /**
          * Creates a new instance of <code>PropertiesReader</code> and sets
          * the underlaying reader and the list delimiter.
          *
          * @param reader the reader
          */
-        public PropertiesReader(Reader reader)
+        public PropertiesReader(Reader reader, boolean maybeTyped)
         {
             super(reader);
             commentLines = new ArrayList<String>();
             valueLines = new ArrayList<String>();
+            this.maybeTyped = maybeTyped;
         }
 
         /**
@@ -938,8 +949,17 @@ public class Properties extends Abstract
 
             // parse the line
             String[] property = parseProperty(line);
+            boolean typed = false;
+            if (maybeTyped && property[1].length() >= 2) {
+                typed = property[1].matches("\\s*[TILFDXSCBilfdxscb]?(\\[[\\S\\s]*\\]|\\{[\\S\\s]*\\}|\"[\\S\\s]*\")\\s*");
+            }
+            if (this.typed == null) {
+                this.typed = typed;
+            } else {
+                this.typed = this.typed & typed;
+            }
             propertyName = unescapeJava(property[0]);
-            propertyValue = unescapeJava(property[1]);
+            propertyValue = property[1];
             return true;
         }
 
@@ -1133,14 +1153,17 @@ public class Properties extends Abstract
      */
     public static class PropertiesWriter extends FilterWriter
     {
+        private boolean typed;
+
         /**
          * Constructor.
          *
          * @param writer a Writer object providing the underlying stream
          */
-        public PropertiesWriter(Writer writer)
+        public PropertiesWriter(Writer writer, boolean typed)
         {
             super(writer);
+            this.typed = typed;
         }
 
         /**
@@ -1154,7 +1177,7 @@ public class Properties extends Abstract
         {
             write(escapeKey(key));
             write(" = ");
-            write(escapeJava(value));
+            write(typed ? value : escapeJava(value));
             writeln(null);
         }
 

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/TypedProperties.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/TypedProperties.java?rev=1790562&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/TypedProperties.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/TypedProperties.java Fri Apr  7 14:35:12 2017
@@ -0,0 +1,458 @@
+/*
+ * 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 org.apache.felix.utils.properties;
+
+import org.osgi.framework.BundleContext;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.felix.utils.properties.InterpolationHelper.SubstitutionCallback;
+import org.apache.felix.utils.properties.InterpolationHelper.BundleContextSubstitutionCallback;
+
+import static org.apache.felix.utils.properties.InterpolationHelper.performSubstitution;
+import static org.apache.felix.utils.properties.InterpolationHelper.substVars;
+
+/**
+ * <p>
+ * Map to load / store / update untyped or typed properties.
+ * The map is untyped if all properties are strings.
+ * When this is the case, the properties are stored without
+ * any encoding, else all properties are encoded using
+ * the {@link ConfigurationHandler}.
+ * </p>
+ *
+ * @author gnodet
+ */
+public class TypedProperties extends AbstractMap<String, Object> {
+
+    private final Properties storage;
+    private final SubstitutionCallback callback;
+    private final boolean substitute;
+
+    public TypedProperties() {
+        this(null, true);
+    }
+
+    public TypedProperties(boolean substitute) {
+        this(null, substitute);
+    }
+
+    public TypedProperties(BundleContext context) {
+        this(wrap(new BundleContextSubstitutionCallback(context)), true);
+    }
+
+    public TypedProperties(SubstitutionCallback callback) {
+        this(callback, true);
+    }
+
+    public TypedProperties(SubstitutionCallback callback, boolean substitute) {
+        this.storage = new Properties(false);
+        this.callback = callback;
+        this.substitute = substitute;
+    }
+
+    public void load(File location) throws IOException {
+        InputStream is = new FileInputStream(location);
+        try {
+            load(is);
+        } finally {
+            is.close();
+        }
+    }
+
+    public void load(URL location) throws IOException {
+        InputStream is = location.openStream();
+        try {
+            load(is);
+        } finally {
+            is.close();
+        }
+    }
+
+    public void load(InputStream is) throws IOException {
+        load(new InputStreamReader(is, Properties.DEFAULT_ENCODING));
+    }
+
+    public void load(Reader reader) throws IOException {
+        storage.loadLayout(reader, true);
+        substitute(callback);
+    }
+
+    public void save(File location) throws IOException {
+        storage.save(location);
+    }
+
+    public void save(OutputStream os) throws IOException {
+        storage.save(os);
+    }
+
+    public void save(Writer writer) throws IOException {
+        storage.save(writer);
+    }
+
+    /**
+     * Store a properties into a output stream, preserving comments, special character, etc.
+     * This method is mainly to be compatible with the java.util.Properties class.
+     *
+     * @param os an output stream.
+     * @param comment this parameter is ignored as this Properties
+     * @throws IOException If storing fails
+     */
+    public void store(OutputStream os, String comment) throws IOException {
+        storage.store(os, comment);
+    }
+
+    @Override
+    public Set<Entry<String, Object>> entrySet() {
+        return new AbstractSet<Entry<String, Object>>() {
+            @Override
+            public Iterator<Entry<String, Object>> iterator() {
+                return new Iterator<Entry<String, Object>>() {
+                    final Iterator<String> keyIterator = storage.keySet().iterator();
+                    public boolean hasNext() {
+                        return keyIterator.hasNext();
+                    }
+                    public Entry<String, Object> next() {
+                        final String key = keyIterator.next();
+                        return new Entry<String, Object>() {
+                            public String getKey() {
+                                return key;
+                            }
+                            public Object getValue() {
+                                return TypedProperties.this.get(key);
+                            }
+                            public Object setValue(Object value) {
+                                return TypedProperties.this.put(key, value);
+                            }
+                        };
+                    }
+                    public void remove() {
+                        keyIterator.remove();
+                    }
+                };
+            }
+
+            @Override
+            public int size() {
+                return storage.size();
+            }
+        };
+    }
+
+    @Override
+    public Object put(String key, Object value) {
+        if (value instanceof String && !storage.typed) {
+            return storage.put(key, (String) value);
+        } else {
+            ensureTyped();
+            String old = storage.put(key, convertToString(value));
+            return old != null ? convertFromString(old) : null;
+        }
+    }
+
+    @Override
+    public Object get(Object key) {
+        String v = storage.get(key);
+        return storage.typed && v != null ? convertFromString(v) : v;
+    }
+
+    public Object put(String key, List<String> commentLines, Object value) {
+        if (value instanceof String && !storage.typed) {
+            return storage.put(key, commentLines, (String) value);
+        } else {
+            ensureTyped();
+            return put(key, commentLines, Arrays.asList(convertToString(value).split("\n")));
+        }
+    }
+
+    public Object put(String key, String comment, Object value) {
+        return put(key, Collections.singletonList(comment), value);
+    }
+
+    private Object put(String key, List<String> commentLines, List<String> valueLines) {
+        String old = storage.put(key, commentLines, valueLines);
+        return old != null ? storage.typed ? convertFromString(old) : old : null;
+    }
+
+    private void ensureTyped() {
+        if (!storage.typed) {
+            storage.typed = true;
+            Set<String> keys = new HashSet<String>(storage.keySet());
+            for (String key : keys) {
+                storage.put(key,
+                            storage.getComments(key),
+                            Arrays.asList(convertToString(storage.get(key)).split("\n")));
+            }
+        }
+    }
+
+    public boolean update(Map<String, Object> props) {
+        TypedProperties properties;
+        if (props instanceof TypedProperties) {
+            properties = (TypedProperties) props;
+        } else {
+            properties = new TypedProperties();
+            for (Entry<String, Object> e : props.entrySet()) {
+                properties.put(e.getKey(), e.getValue());
+            }
+        }
+        return update(properties);
+    }
+
+    public boolean update(TypedProperties properties) {
+        return storage.update(properties.storage);
+    }
+
+    public List<String> getRaw(String key) {
+        return storage.getRaw(key);
+    }
+
+    public List<String> getComments(String key) {
+        return storage.getComments(key);
+    }
+
+    @Override
+    public Object remove(Object key) {
+        return storage.remove(key);
+    }
+
+    @Override
+    public void clear() {
+        storage.clear();
+    }
+
+    /**
+     * Return the comment header.
+     *
+     * @return the comment header
+     */
+    public List<String> getHeader()
+    {
+        return storage.getHeader();
+    }
+
+    /**
+     * Set the comment header.
+     *
+     * @param header the header to use
+     */
+    public void setHeader(List<String> header)
+    {
+        storage.setHeader(header);
+    }
+
+    /**
+     * Return the comment footer.
+     *
+     * @return the comment footer
+     */
+    public List<String> getFooter()
+    {
+        return storage.getFooter();
+    }
+
+    /**
+     * Set the comment footer.
+     *
+     * @param footer the footer to use
+     */
+    public void setFooter(List<String> footer)
+    {
+        storage.setFooter(footer);
+    }
+
+    public void substitute(final SubstitutionCallback cb) {
+        if (!substitute) {
+            return;
+        }
+        final SubstitutionCallback callback = cb != null ? cb  : wrap(new BundleContextSubstitutionCallback(null));
+        Map<String, TypedProperties> props = Collections.singletonMap("root", this);
+        substitute(props, prepare(props), callback, true);
+    }
+
+    private static SubstitutionCallback wrap(final InterpolationHelper.SubstitutionCallback cb) {
+        return new SubstitutionCallback() {
+            public String getValue(String name, String key, String value) {
+                return cb.getValue(value);
+            }
+        };
+    }
+
+    public interface SubstitutionCallback {
+
+        String getValue(String name, String key, String value);
+
+    }
+
+    public static Map<String, Map<String, String>> prepare(Map<String, TypedProperties> properties) {
+        Map<String, Map<String, String>> dynamic = new HashMap<String, Map<String, String>>();
+        for (Map.Entry<String, TypedProperties> entry : properties.entrySet()) {
+            String name = entry.getKey();
+            dynamic.put(name, new DynamicMap(name, entry.getValue().storage));
+        }
+        return dynamic;
+    }
+
+    public static void substitute(Map<String, TypedProperties> properties,
+                                  Map<String, Map<String, String>> dynamic,
+                                  SubstitutionCallback callback,
+                                  boolean finalSubstitution) {
+        for (Map<String, String> map : dynamic.values()) {
+            ((DynamicMap) map).init(callback, finalSubstitution);
+        }
+        for (Map.Entry<String, TypedProperties> entry : properties.entrySet()) {
+            entry.getValue().storage.putAll(dynamic.get(entry.getKey()));
+        }
+    }
+
+    private static String convertToString(Object value) {
+        try {
+            return ConfigurationHandler.write(value);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static Object convertFromString(String value) {
+        try {
+            return ConfigurationHandler.read(value);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static class DynamicMap extends AbstractMap<String, String> {
+        private final String name;
+        private final Properties storage;
+        private final Map<String, String> computed;
+        private final Map<String, String> cycles;
+        private SubstitutionCallback callback;
+        private boolean finalSubstitution;
+
+        public DynamicMap(String name, Properties storage) {
+            this.name = name;
+            this.storage = storage;
+            this.computed = new HashMap<String, String>();
+            this.cycles = new HashMap<String, String>();
+        }
+
+        public void init(SubstitutionCallback callback, boolean finalSubstitution) {
+            this.callback = callback;
+            this.finalSubstitution = finalSubstitution;
+        }
+
+        @Override
+        public Set<Entry<String, String>> entrySet() {
+            return new AbstractSet<Entry<String, String>>() {
+                @Override
+                public Iterator<Entry<String, String>> iterator() {
+                    final Iterator<String> iterator = storage.keySet().iterator();
+                    return new Iterator<Entry<String, String>>() {
+                        public boolean hasNext() {
+                            return iterator.hasNext();
+                        }
+                        public Entry<String, String> next() {
+                            final String key = iterator.next();
+                            return new Entry<String, String>() {
+                                public String getKey() {
+                                    return key;
+                                }
+                                public String getValue() {
+                                    String v = computed.get(key);
+                                    if (v == null) {
+                                        v = compute(key);
+//                                        computed.put(key, v);
+                                    }
+                                    return v;
+                                }
+                                public String setValue(String value) {
+                                    throw new UnsupportedOperationException();
+                                }
+                            };
+                        }
+                        public void remove() {
+                            throw new UnsupportedOperationException();
+                        }
+                    };
+                }
+
+                @Override
+                public int size() {
+                    return storage.size();
+                }
+            };
+        }
+
+        private String compute(final String key) {
+            InterpolationHelper.SubstitutionCallback wrapper = new InterpolationHelper.SubstitutionCallback() {
+                public String getValue(String value) {
+                    if (finalSubstitution) {
+                        String str = DynamicMap.this.get(value);
+                        if (str != null) {
+                            if (storage.typed) {
+                                boolean mult;
+                                boolean hasType;
+                                char t = str.charAt(0);
+                                if (t == '[' || t == '(') {
+                                    mult = true;
+                                    hasType = false;
+                                } else if (t == '"') {
+                                    mult = false;
+                                    hasType = false;
+                                } else {
+                                    t = str.charAt(1);
+                                    mult = t == '[' || t == '(';
+                                    hasType = true;
+                                }
+                                if (mult) {
+                                    throw new IllegalArgumentException("Can't substitute from a collection/array value: " + value);
+                                }
+                                return (String) convertFromString(hasType ? str.substring(1) : str);
+                            } else {
+                                return str;
+                            }
+                        }
+                    }
+                    return callback.getValue(name, key, value);
+                }
+            };
+            String value = storage.get(key);
+            String v = substVars(value, key, cycles, this, wrapper, false, finalSubstitution, finalSubstitution);
+            return v;
+        }
+    }
+}

Added: felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/TypedPropertiesTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/TypedPropertiesTest.java?rev=1790562&view=auto
==============================================================================
--- felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/TypedPropertiesTest.java (added)
+++ felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/TypedPropertiesTest.java Fri Apr  7 14:35:12 2017
@@ -0,0 +1,79 @@
+/*
+ * 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 org.apache.felix.utils.properties;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * Unit tests on <code>TypedProperties</code>.
+ * </p>
+ *
+ * @author gnodet
+ */
+public class TypedPropertiesTest extends TestCase {
+
+    private final static String TEST_PROPERTIES_FILE = "test.properties";
+    private final static String TEST_TYPED_PROPERTIES_FILE = "typed.properties";
+
+    public void testConfigInterpolation() throws IOException
+    {
+        TypedProperties properties = new TypedProperties();
+        properties.load(this.getClass().getClassLoader().getResourceAsStream(TEST_TYPED_PROPERTIES_FILE));
+
+        assertEquals(8101, properties.get("port"));
+        assertEquals("127.0.0.1:8101", properties.get("url"));
+    }
+
+    public void testSetProp() throws IOException
+    {
+        TypedProperties properties = new TypedProperties();
+        properties.load(this.getClass().getClassLoader().getResourceAsStream(TEST_TYPED_PROPERTIES_FILE));
+
+        properties.put("port", 8101);
+        properties.put("port2", 8103);
+        properties.put("url", "127.0.0.1:8101");
+
+        properties.save(System.out);
+    }
+
+    public void testLoadNonTypedProps() throws IOException
+    {
+        TypedProperties properties = new TypedProperties();
+        properties.load(this.getClass().getClassLoader().getResourceAsStream(TEST_PROPERTIES_FILE));
+        properties.put("port", 8101);
+
+        StringWriter sw = new StringWriter();
+        properties.save(sw);
+
+        TypedProperties p2 = new TypedProperties();
+        p2.load(new StringReader(sw.toString()));
+        assertEquals(8101, p2.get("port"));
+        assertEquals("test", p2.get("test"));
+    }
+
+}

Added: felix/trunk/utils/src/test/resources/typed.properties
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/resources/typed.properties?rev=1790562&view=auto
==============================================================================
--- felix/trunk/utils/src/test/resources/typed.properties (added)
+++ felix/trunk/utils/src/test/resources/typed.properties Fri Apr  7 14:35:12 2017
@@ -0,0 +1,37 @@
+##---------------------------------------------------------------------------
+##  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.
+##---------------------------------------------------------------------------
+#
+# typed.properties
+# Used in the TypedPropertiesTest
+#
+
+prefix = "81"
+
+# Port
+port = I"${prefix}01"
+
+# Second port
+port2 = I"${prefix}02"
+
+# List of ports
+ports = i[ "${port}", "${port2}" ]
+
+# Host
+host = "127.0.0.1"
+
+# Primary url
+url = "${host}:${port}"