summaryrefslogtreecommitdiff
path: root/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/ContextualProperties.java
blob: 14de96d165ff4f3aaf97ebe15a7c53255d629eb7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
/*
 *
 * 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.qpid.junit.extensions.util;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

/**
 * ContextualProperties is an extension of {@link java.util.Properties} that automatically selects properties based on an
 * environment parameter (defined by the system property {@link #ENV_SYS_PROPERTY}), the name of a class, plus a modifier
 * (which can be used to name a method of a class) and a property key. It also supports the definition of arrays of
 * property values using indexes. The properties are searched in the following order until a match is found:
 *
 * <ol>
 * <li>environment + class name with package name + modifier + key
 * <li>environment + class name with package name + key
 * <li>environment + key
 * <li>class name with package name + modifier + key
 * <li>class name with package name + key
 * <li>key
 * </ol>
 *
 * <p>To create arrays of property values add index numbers onto the end of the property keys. An array of string values
 * will be created with the elements of the array set to the value of the property at the matching index. Ideally the
 * index values will be contiguous, starting at 0. This does not need to be the case however. If an array definition
 * begins at index n, and ends at index m, Then an array big enough to hold m + 1 elements will be created and populated
 * with values from n to m. Values before n and any missing values between n and m will be null in the array.
 *
 * <p>To give an example, suppose you have two different environments 'DEVELOPMENT' and 'PRODUCTION' and they each need
 * the same properties but set to different values for each environment and some properties the same in both, you could
 * create a properties file like:
 *
 * <p><code>
 * # Project configuration properties file.<br/>
 * <br/>
 * # These properties are environment specific.<br/>
 * DEVELOPMENT.debug=true<br/>
 * PRODUCTION.debug=false<br/>
 * <br/>
 * # Always debug MyClass in all environments but not the myMethod method.<br/>
 * MyClass.debug=true<br/>
 * MyClass.myMethod.debug=false<br/>
 * <br/>
 * # Set up an array of my ten favourite animals. Leave elements 3 to 8 as null as I haven't decided on them yet.<br/>
 * animals.0=cat<br/>
 * animals.1=dog<br/>
 * animals.2=elephant<br/>
 * animals.9=lion<br/>
 * <br/>
 * # This is a default value that will be used when the environment is not known.<br/>
 * debug=false<br/>
 * </code>
 *
 * <p>The most specific definition of a property is searched for first moving out to the most general. This allows
 * general property defaults to be set and then overiden for specific uses by some classes and modifiers.
 *
 * <p>A ContextualProperties object can be loaded in the same way as a java.utils.Properties. A recommended way to do
 * this that does not assume that the properties file is a file (it could be in a jar) is to load the properties from the
 * url for the resource lookup up on the classpath:
 *
 * <p><code>
 * Properties configProperties = new ContextualProperties();<br/>
 * configProperties.load(this.getClass().getClassLoader().getResourceAsStream("config.properties"));<br/>
 * </code>
 *
 * <p>EnvironmentProperties will load the 'DEVELOPMENT.debug' property or 'PROUCTION.debug' property based on the setting
 * of the system environment property. If a matching property for the environment cannot be found then the simple property
 * name without the environment pre-pended onto it will be used instead. This 'use of default environments' behaviour is
 * turned on initially but it can be disabled by calling the {@link #useDefaultEnvironments} method.
 *
 * <p>When a property matching a key cannot be found then the property accessor methods will always return null. If a
 * default value for a property exists but the 'use of default environments' behavious prevents it being used then the
 * accessor methods will return null.
 *
 * <p><table id="crc"><caption>CRC Card</caption>
 * <tr><th> Responsibilities <th> Collaborations
 * <tr><td> Automatically select properties dependant on environment, class name and modifier as well as property key.
 * <tr><td> Convert indexed properties into arrays.
 * </table>
 *
 * @author Rupert Smith
 */
public class ContextualProperties extends ParsedProperties
{
    /** The name of the system property that is used to define the environment. */
    public static final String ENV_SYS_PROPERTY = "environment";

    /**
     * <p>Holds the iteration count down order.
     *
     * <p>If e = 4, b = 2, m = 1 then the iteration order or i is 7,6,4 and then if using environment defaults 3,2,0
     * where the accessor key is:
     * (i & e != 0 ? environment : "") + (i & b != 0 ? base : "") + (1 + m != 0 ? modifier : "") + key
     *
     * <p>In other words the presence or otherwise of the three least significant bits when counting down from 7
     * specifies which of the environment, base and modifier are to be included in the key where the environment, base
     * and modifier stand for the bits in positions 2, 1 and 0. The numbers 5 and 1 are missed out of the count because
     * they stand for the case where the modifier is used without the base which is not done.
     */
    private static final int[] ORDER = new int[] { 7, 6, 4, 3, 2, 0 };

    /**
     * Defines the point in the iteration count order below which the 'use environment defaults' feature is being used.
     */
    private static final int ENVIRONMENT_DEFAULTS_CUTOFF = 4;

    /** Defines the bit representation for the environment in the key ordering. See {@link #ORDER}. */
    private static final int E = 4;

    /** Defines the bit representation for the base in the key ordering. See {@link #ORDER}. */
    private static final int B = 2;

    /** Defines the bit representation for the modifier in the key ordering. See {@link #ORDER}. */
    private static final int M = 1;

    /** Used to hold the value of the environment system property. */
    private String environment;

    /** Used to indicate that the 'use of defaults' behaviour should be used. */
    private boolean useDefaults = true;

    /** Used to hold all the array properties. This is a mapping from property names to ArrayLists of Strings. */
    protected Map arrayProperties = new HashMap();

    /**
     * Default constructor that builds a ContextualProperties that uses environment defaults.
     */
    public ContextualProperties()
    {
        super();

        // Keep the value of the system environment property.
        environment = System.getProperty(ENV_SYS_PROPERTY);
    }

    /**
     * Creates a ContextualProperties that uses environment defaults and is initialized with the specified properties.
     *
     * @param props The properties to initialize this with.
     */
    public ContextualProperties(Properties props)
    {
        super(props);

        // Keep the value of the system environment property.
        environment = System.getProperty(ENV_SYS_PROPERTY);

        // Extract any array properties as arrays.
        createArrayProperties();
    }

    /**
     * Parses an input stream as properties.
     *
     * @param inStream The input stream to read the properties from.
     *
     * @exception IOException If there is an IO error during reading from the input stream.
     */
    public void load(InputStream inStream) throws IOException
    {
        super.load(inStream);

        // Extract any array properties as arrays.
        createArrayProperties();
    }

    /**
     * Tells this environment aware properties object whether it should use default environment properties without a
     * pre-pended environment when a property for the current environment cannot be found.
     *
     * @param flag True to use defaults, false to not use defaults.
     */
    public void useDefaultEnvironments(boolean flag)
    {
        useDefaults = flag;
    }

    /**
     * Looks up a property value relative to the environment, callers class and method. The default environment will be
     * checked for a matching property if defaults are being used. In order to work out the callers class and method this
     * method throws an exception and then searches one level up its stack frames.
     *
     * @param key The property key.
     *
     * @return The value of this property searching from the most specific definition (environment, class, method, key)
     * to the most general (key only), unless use of default environments is turned off in which case the most general
     * proeprty searched is (environment, key).
     */
    public String getProperty(String key)
    {
        // Try to get the callers class name and method name by examing the stack.
        String className = null;
        String methodName = null;

        // Java 1.4 onwards only.
        /*try
        {
            throw new Exception();
        }
        catch (Exception e)
        {
            StackTraceElement[] stack = e.getStackTrace();

            // Check that the stack trace contains at least two elements, one for this method and one for the caller.
            if (stack.length >= 2)
            {
                className = stack[1].getClassName();
                methodName = stack[1].getMethodName();
            }
        }*/

        // Java 1.5 onwards only.
        StackTraceElement[] stack = Thread.currentThread().getStackTrace();

        // Check that the stack trace contains at least two elements, one for this method and one for the caller.
        if (stack.length >= 2)
        {
            className = stack[1].getClassName();
            methodName = stack[1].getMethodName();
        }

        // Java 1.3 and before? Not sure, some horrible thing that parses the text spat out by printStackTrace?

        return getProperty(className, methodName, key);
    }

    /**
     * Looks up a property value relative to the environment, base class and modifier. The default environment will be
     * checked for a matching property if defaults are being used.
     *
     * @param base An object of the class to retrieve properties relative to.
     * @param modifier The modifier (which may stand for a method of the class).
     * @param key The property key.
     *
     * @return The value of this property searching from the most specific definition (environment, class, modifier, key)
     * to the most general (key only), unless use of default environments is turned off in which case the most general
     * property searched is (environment, key).
     */
    public String getProperty(Object base, String modifier, String key)
    {
        return getProperty(base.getClass().getName(), modifier, key);
    }

    /**
     * Looks up a property value relative to the environment, base class and modifier. The default environment will be
     * checked for a matching property if defaults are being used.
     *
     * @param base The name of the class to retrieve properties relative to.
     * @param modifier The modifier (which may stand for a method of the class).
     * @param key The property key.
     *
     * @return The value of this property searching from the most specific definition (environment, class, modifier, key)
     * to the most general (key only), unless use of default environments is turned off in which case the most general
     * property searched is (environment, key).
     */
    public String getProperty(String base, String modifier, String key)
    {
        String result = null;

        // Loop over the key orderings, from the most specific to the most general, until a matching value is found.
        for (Iterator i = getKeyIterator(base, modifier, key); i.hasNext();)
        {
            String nextKey = (String) i.next();

            result = super.getProperty(nextKey);

            if (result != null)
            {
                break;
            }
        }

        return result;
    }

    /**
     * Looks up an array property value relative to the environment, callers class and method. The default environment
     * will be checked for a matching array property if defaults are being used. In order to work out the callers class
     * and method this method throws an exception and then searches one level up its stack frames.
     *
     * @param key The property key.
     *
     * @return The array value of this indexed property searching from the most specific definition (environment, class,
     *         method, key) to the most general (key only), unless use of default environments is turned off in which
     *         case the most general proeprty searched is (environment, key).
     */
    public String[] getProperties(String key)
    {
        // Try to get the callers class name and method name by throwing an exception an searching the stack frames.
        String className = null;
        String methodName = null;

        /* Java 1.4 onwards only.
           try {
             throw new Exception();
           } catch (Exception e) {
             StackTraceElement[] stack = e.getStackTrace();
             // Check that the stack trace contains at least two elements, one for this method and one for the caller.
             if (stack.length >= 2) {
               className = stack[1].getClassName();
               methodName = stack[1].getMethodName();
             }
           }*/
        return getProperties(className, methodName, key);
    }

    /**
     * Looks up an array property value relative to the environment, base class and modifier. The default environment will
     * be checked for a matching array property if defaults are being used.
     *
     * @param base An object of the class to retrieve properties relative to.
     * @param modifier The modifier (which may stand for a method of the class).
     * @param key The property key.
     *
     * @return The array value of this indexed property searching from the most specific definition (environment, class,
     *         modifier, key) to the most general (key only), unless use of default environments is turned off in which
     *         case the most general proeprty searched is (environment, key).
     */
    public String[] getProperties(Object base, String modifier, String key)
    {
        return getProperties(base.getClass().getName(), modifier, key);
    }

    /**
     * Looks up an array property value relative to the environment, base class and modifier. The default environment will
     * be checked for a matching array property if defaults are being used.
     *
     * @param base The name of the class to retrieve properties relative to.
     * @param modifier The modifier (which may stand for a method of the class).
     * @param key The property key.
     *
     * @return The array value of this indexed property searching from the most specific definition (environment, class,
     *         modifier, key) to the most general (key only), unless use of default environments is turned off in which
     *         case the most general property searched is (environment, key).
     */
    public String[] getProperties(String base, String modifier, String key)
    {
        String[] result = null;

        // Loop over the key orderings, from the most specific to the most general, until a matching value is found.
        for (Iterator i = getKeyIterator(base, modifier, key); i.hasNext();)
        {
            String nextKey = (String) i.next();
            ArrayList arrayList = (ArrayList) arrayProperties.get(nextKey);

            if (arrayList != null)
            {
                result = (String[]) arrayList.toArray(new String[] {});

                break;
            }
        }

        return result;
    }

    /**
     * For a given environment, base, modifier and key and setting of the use of default environments feature this
     * generates an iterator that walks over the order in which to try and access properties.
     *
     * <p>See the {@link #ORDER} constant for an explanation of how the key ordering is generated.
     *
     * @param base The name of the class to retrieve properties relative to.
     * @param modifier The modifier (which may stand for a method of the class).
     * @param key The property key.
     *
     * @return An Iterator over String keys defining the order in which properties should be accessed.
     */
    protected Iterator getKeyIterator(final String base, final String modifier, final String key)
    {
        return new Iterator()
            {
                // The key ordering count always begins at the start of the ORDER array.
                private int i = 0;

                public boolean hasNext()
                {
                    return (useDefaults ? ((i < ORDER.length) && (ORDER[i] > ENVIRONMENT_DEFAULTS_CUTOFF))
                                        : (i < ORDER.length));
                }

                public Object next()
                {
                    // Check that there is a next element and return null if not.
                    if (!hasNext())
                    {
                        return null;
                    }

                    // Get the next ordering count.
                    int o = ORDER[i];

                    // Do bit matching on the count to choose which elements to include in the key.
                    String result =
                        (((o & E) != 0) ? (environment + ".") : "") + (((o & B) != 0) ? (base + ".") : "")
                        + (((o & M) != 0) ? (modifier + ".") : "") + key;

                    // Increment the iterator to get the next key on the next call.
                    i++;

                    return result;
                }

                public void remove()
                {
                    // This method is not supported.
                    throw new UnsupportedOperationException("remove() is not supported on this key order iterator as "
                        + "the ordering cannot be changed");
                }
            };
    }

    /**
     * Scans all the properties in the parent Properties object and creates arrays for any array property definitions.
     *
     * <p>Array properties are defined with indexes. For example:
     *
     * <p><code>
     * property.1=one<br/>
     * property.2=two<br/>
     * property.3=three<br/>
     * </code>
     *
     * <p>Note that these properties will be stored as the 'empty string' or "" property array.
     *
     * <p><code>
     * .1=one<br/>
     * 2=two<br/>
     * </code>
     */
    protected void createArrayProperties()
    {
        // Scan through all defined properties.
        for (Object o : keySet())
        {
            String key = (String) o;
            String value = super.getProperty(key);

            // Split the property key into everything before the last '.' and after it.
            int lastDotIndex = key.lastIndexOf('.');
            String keyEnding = key.substring(lastDotIndex + 1, key.length());
            String keyStart = key.substring(0, (lastDotIndex == -1) ? 0 : lastDotIndex);

            // Check if the property key ends in an integer, in which case it is an array property.
            int index = 0;

            try
            {
                index = Integer.parseInt(keyEnding);
            }
            // The ending is not an integer so its not an array.
            catch (NumberFormatException e)
            {
                // Scan the next property.
                continue;
            }

            // Check if an array property already exists for this base name and create one if not.
            ArrayList propArray = (ArrayList) arrayProperties.get(keyStart);

            if (propArray == null)
            {
                propArray = new ArrayList();
                arrayProperties.put(keyStart, propArray);
            }

            // Add the new property value to the array property for the index.
            propArray.set(index, value);
        }
    }
}