001/*
002 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003 *
004 * Copyright (c) 2005 Sun Microsystems Inc. All Rights Reserved
005 *
006 * The contents of this file are subject to the terms
007 * of the Common Development and Distribution License
008 * (the License). You may not use this file except in
009 * compliance with the License.
010 *
011 * You can obtain a copy of the License at
012 * https://opensso.dev.java.net/public/CDDLv1.0.html or
013 * opensso/legal/CDDLv1.0.txt
014 * See the License for the specific language governing
015 * permission and limitations under the License.
016 *
017 * When distributing Covered Code, include this CDDL
018 * Header Notice in each file and include the License file
019 * at opensso/legal/CDDLv1.0.txt.
020 * If applicable, add the following below the CDDL Header,
021 * with the fields enclosed by brackets [] replaced by
022 * your own identifying information:
023 * "Portions Copyrighted [year] [name of copyright owner]"
024 *
025 * Portions Copyrighted 2010-2016 ForgeRock AS.
026 */
027package com.iplanet.am.util;
028
029import static org.forgerock.openam.utils.CollectionUtils.asSet;
030
031import com.iplanet.sso.SSOToken;
032import com.sun.identity.common.AttributeStruct;
033import com.sun.identity.common.PropertiesFinder;
034import com.sun.identity.common.configuration.ConfigurationListener;
035import com.sun.identity.common.configuration.ConfigurationObserver;
036import com.sun.identity.common.configuration.ServerConfiguration;
037import com.sun.identity.security.AdminTokenAction;
038import com.sun.identity.shared.Constants;
039import com.sun.identity.sm.SMSEntry;
040
041import java.io.FileInputStream;
042import java.io.IOException;
043import java.io.PrintWriter;
044import java.io.StringWriter;
045import java.net.InetAddress;
046import java.security.AccessController;
047import java.util.Collections;
048import java.util.HashMap;
049import java.util.HashSet;
050import java.util.Map;
051import java.util.MissingResourceException;
052import java.util.Properties;
053import java.util.ResourceBundle;
054import java.util.Set;
055import java.util.concurrent.atomic.AtomicReference;
056import javax.annotation.Nullable;
057
058import org.forgerock.guava.common.base.Predicate;
059import org.forgerock.guava.common.collect.ImmutableMap;
060import org.forgerock.guava.common.collect.Maps;
061import org.forgerock.openam.cts.api.CoreTokenConstants;
062import org.forgerock.openam.utils.StringUtils;
063
064/**
065 * This class provides functionality that allows single-point-of-access to all
066 * related system properties.
067 * <p>
068 * The system properties can be set in couple of ways: programmatically by
069 * calling the <code>initializeProperties</code> method, or can be statically
070 * loaded at startup from a file named: 
071 * <code>AMConfig.[class,properties]</code>.
072 * Setting the properties through the API takes precedence and will replace the
073 * properties loaded via file. For statically loading the properties via a file,
074 * this class tries to first find a class, <code>AMConfig.class</code>, and
075 * then a file, <code>AMConfig.properties</code> in the CLASSPATH accessible
076 * to this code. The <code>AMConfig.class</code> takes precedence over the
077 * flat file <code>AMConfig.properties</code>.
078 * <p>
079 * If multiple servers are running, each may have their own configuration file.
080 * The naming convention for such scenarios is
081 * <code>AMConfig-&lt;serverName></code>.
082 * @supported.all.api
083 */
084public class SystemProperties {
085
086    /**
087     * Runtime flag to be set, in order to override the path of the
088     * configuration file.
089     */
090    public static final String CONFIG_PATH = "com.iplanet.services.configpath";
091
092    /**
093     * Default name of the configuration file.
094     */
095    public static final String CONFIG_FILE_NAME = "serverconfig.xml";
096
097    /**
098     * New configuration file extension
099     */
100    public static final String PROPERTIES = "properties";
101
102    public static final String NEWCONFDIR = "NEW_CONF_DIR";
103
104    private static final String SERVER_NAME_PROPERTY = "server.name";
105
106    private static final String CONFIG_NAME_PROPERTY = "amconfig";
107
108    private static final String AMCONFIG_FILE_NAME = "AMConfig";
109
110    /** Regular expression pattern for a sequence of 1 or more white space characters. */
111    private static final String WHITESPACE = "\\s+";
112
113    private static final Map<String, AttributeStruct> ATTRIBUTE_MAP = initAttributeMapping();
114
115    private static final int TAG_START = '%';
116
117    /**
118     * Maps from tags to the system properties that they should be replaced with. System property values containing
119     * these tags will be replaced with the actual values of these properties by {@link #get(String)}.
120     */
121    private static final Map<String, String> TAG_SWAP_PROPERTIES = ImmutableMap.<String, String>builder()
122            .put("%SERVER_PORT%", Constants.AM_SERVER_PORT)
123            .put("%SERVER_URI%", Constants.AM_SERVICES_DEPLOYMENT_DESCRIPTOR)
124            .put("%SERVER_HOST%", Constants.AM_SERVER_HOST)
125            .put("%SERVER_PROTO%", Constants.AM_SERVER_PROTOCOL)
126            .put("%BASE_DIR%", CONFIG_PATH)
127            .put("%SESSION_ROOT_SUFFIX%", CoreTokenConstants.SYS_PROPERTY_SESSION_HA_REPOSITORY_ROOT_SUFFIX)
128            .put("%SESSION_STORE_TYPE%", CoreTokenConstants.SYS_PROPERTY_SESSION_HA_REPOSITORY_TYPE)
129            .build();
130
131    private static final boolean SITEMONITOR_DISABLED;
132
133    /**
134     * Reference to the current properties map and tagswap values.
135     */
136    private static final AtomicReference<PropertiesHolder> propertiesHolderRef =
137            new AtomicReference<>(new PropertiesHolder());
138
139    private static String initError = null;
140    private static String initSecondaryError = null;
141    private static String instanceName = null;
142
143    /*
144     * Initialization to load the properties file for config information before
145     * anything else starts.
146     */
147    static {
148        try {
149            // Load properties from file
150            String serverName = System.getProperty(SERVER_NAME_PROPERTY);
151            String configName = System.getProperty(CONFIG_NAME_PROPERTY, AMCONFIG_FILE_NAME);
152            String fname = null;
153            if (serverName != null) {
154                serverName = serverName.replace('.', '_');
155                fname = configName + "-" + serverName;
156            } else {
157                fname = configName;
158            }
159            initializeProperties(fname);
160            PropertiesHolder props = propertiesHolderRef.get();
161
162            // Get the location of the new configuration file in case
163            // of single war deployment
164            try {
165                String newConfigFileLoc = props.getProperty(Constants.AM_NEW_CONFIGFILE_PATH);
166
167                if (!StringUtils.isEmpty(newConfigFileLoc) && !NEWCONFDIR.equals(newConfigFileLoc)) {
168                    String hostName = InetAddress.getLocalHost().getHostName().toLowerCase();
169                    String serverURI = props.getProperty(Constants.AM_SERVICES_DEPLOYMENT_DESCRIPTOR).replace('/', '_')
170                                            .toLowerCase();
171
172                    String fileName = newConfigFileLoc + "/" + AMCONFIG_FILE_NAME + serverURI + hostName +
173                            props.getProperty(Constants.AM_SERVER_PORT) + "." + PROPERTIES;
174
175                    try {
176                        props = loadProperties(props, fileName);
177                    } catch (IOException ioe) {
178                        try {
179                            props = loadProperties(props, newConfigFileLoc + "/" + AMCONFIG_FILE_NAME + "." +
180                                    PROPERTIES);
181                        } catch (IOException ioe2) {
182                            saveException(ioe2);
183                        }
184                    }
185                    propertiesHolderRef.set(props);
186                }
187            } catch (Exception ex) {
188                saveException(ex);
189            }
190        } catch (MissingResourceException e) {
191            // Can't print the message to debug due to dependency
192            // Save it as a String and provide when requested.
193            StringWriter sw = new StringWriter();
194            e.printStackTrace(new PrintWriter(sw));
195            initError = sw.toString();
196        }
197        SITEMONITOR_DISABLED = Boolean.parseBoolean(getProp(Constants.SITEMONITOR_DISABLED, "false"));
198    }
199
200    private static PropertiesHolder loadProperties(PropertiesHolder props, String file) throws IOException {
201        try (FileInputStream fis = new FileInputStream(file)) {
202            Properties temp = new Properties();
203            temp.load(fis);
204            return props.putAll(temp);
205        }
206    }
207
208    /**
209     * Helper function to handle associated exceptions during initialization of
210     * properties using external properties file in a single war deployment.
211     */
212    static void saveException(Exception ex) {
213        // Save it as a String and provide when requested.
214        StringWriter sw = new StringWriter();
215        ex.printStackTrace(new PrintWriter(sw));
216        initSecondaryError = sw.toString();
217    }
218
219    /**
220     * This method lets you query for a system property whose value is same as
221     * <code>String</code> key. The method first tries to read the property
222     * from java.lang.System followed by a lookup in the config file.
223     *
224     * @param key
225     *            type <code>String</code>, the key whose value one is
226     *            looking for.
227     * @return the value if the key exists; otherwise returns <code>null</code>
228     */
229    public static String get(String key) {
230
231        String answer = null;
232
233        // look up values in SMS services only if in server mode.
234        if (isServerMode() || SITEMONITOR_DISABLED) {
235            AttributeStruct ast = ATTRIBUTE_MAP.get(key);
236            if (ast != null) {
237                answer = PropertiesFinder.getProperty(key, ast);
238            }
239        }
240
241        if (answer == null) {
242            answer = getProp(key);
243
244            final Map<String, String> tagswapValues = propertiesHolderRef.get().tagSwapValues;
245            if (answer != null && tagswapValues != null && answer.indexOf(TAG_START) != -1) {
246                for (Map.Entry<String, String> tagSwapEntry : tagswapValues.entrySet()) {
247                    String k = tagSwapEntry.getKey();
248                    String val = tagSwapEntry.getValue();
249
250                    if (k.equals("%SERVER_URI%")) {
251                        if (!StringUtils.isEmpty(val)) {
252                            if (val.charAt(0) == '/') {
253                                answer = answer.replaceAll("/%SERVER_URI%", val);
254                                String lessSlash = val.substring(1);
255                                answer = answer.replaceAll("%SERVER_URI%", lessSlash);
256                            } else {
257                                answer = answer.replaceAll(k, val);
258                            }
259                        }
260                    } else {
261                        answer = answer.replaceAll(k, val);
262                    }
263                }
264
265                if (answer.contains("%ROOT_SUFFIX%")) {
266                    answer = answer.replaceAll("%ROOT_SUFFIX%", SMSEntry.getAMSdkBaseDN());
267                }
268            }
269        }
270
271        return answer;
272    }
273
274    private static String getProp(String key, String def) {
275        String value = getProp(key);
276        return ((value == null) ? def : value);
277    }
278
279    private static String getProp(String key) {
280        String answer = System.getProperty(key);
281        if (answer == null) {
282            answer = propertiesHolderRef.get().getProperty(key);
283        }
284        return answer;
285    }
286
287    /**
288     * This method lets you query for a system property whose value is same as
289     * <code>String</code> key.
290     *
291     * @param key the key whose value one is looking for.
292     * @param def the default value if the key does not exist.
293     * @return the value if the key exists; otherwise returns default value.
294     */
295    public static String get(String key, String def) {
296        String value = get(key);
297        return value == null ? def : value;
298    }
299
300    /**
301     * Returns the property value as a boolean
302     *
303     * @param key the key whose value one is looking for.
304     * @return the boolean value if the key exists; otherwise returns false
305     */
306    public static boolean getAsBoolean(String key) {
307        String value = get(key);
308        return Boolean.parseBoolean(value);
309    }
310
311    /**
312     * Returns the property value as a boolean
313     *
314     * @param key the property name.
315     * @param defaultValue value if key is not found.
316     * @return the boolean value if the key exists; otherwise the default value
317     */
318    public static boolean getAsBoolean(String key, boolean defaultValue) {
319        String value = get(key);
320
321        if (value == null) {
322            return defaultValue;
323        }
324
325        return Boolean.parseBoolean(value);
326    }
327
328    /**
329     * @param key The System Property key to lookup.
330     * @param defaultValue If the property was not set, or could not be parsed to an int.
331     * @return Either the defaultValue, or the numeric value assigned to the System Property.
332     */
333    public static int getAsInt(String key, int defaultValue) {
334        String value = get(key);
335
336        if (value == null) {
337            return defaultValue;
338        }
339        try {
340            return Integer.parseInt(value);
341        } catch (NumberFormatException e) {
342            return defaultValue;
343        }
344    }
345
346    /**
347     * @param key The System Property key to lookup.
348     * @param defaultValue If the property was not set, or could not be parsed to a long.
349     * @return Either the defaultValue, or the numeric value assigned to the System Property.
350     */
351    public static long getAsLong(String key, long defaultValue) {
352        String value = get(key);
353
354        if (value == null) {
355            return defaultValue;
356        }
357        try {
358            return Long.parseLong(value);
359        } catch (NumberFormatException e) {
360            return defaultValue;
361        }
362    }
363
364    /**
365     * Parses a system property as a set of strings by splitting the value on the given delimiter expression.
366     *
367     * @param key The System Property key to lookup.
368     * @param delimiterRegex The regular expression to use to split the value into elements in the set.
369     * @param defaultValue The default set to return if the property does not exist.
370     * @return the value of the property parsed as a set of strings.
371     */
372    public static Set<String> getAsSet(String key, String delimiterRegex, Set<String> defaultValue) {
373        String value = get(key);
374        if (value == null || value.trim().isEmpty()) {
375            return defaultValue;
376        }
377        return asSet(value.split(delimiterRegex));
378    }
379
380    /**
381     * Parses a system property as a set of strings by splitting the value on the given delimiter expression.
382     *
383     * @param key The System Property key to lookup.
384     * @param delimiterRegex The regular expression to use to split the value into elements in the set.
385     * @return the value of the property parsed as a set of strings or an empty set if no match is found.
386     */
387    public static Set<String> getAsSet(String key, String delimiterRegex) {
388        return getAsSet(key, delimiterRegex, Collections.<String>emptySet());
389    }
390
391    /**
392     * Parses a system property as a set of strings by splitting the value on white space characters.
393     *
394     * @param key The System Property key to lookup.
395     * @return the value of the property parsed as a set of strings or an empty set if no match is found.
396     */
397    public static Set<String> getAsSet(String key) {
398        return getAsSet(key, WHITESPACE);
399    }
400
401    /**
402     * Returns all the properties defined and their values. This is a defensive copy of the properties and so updates
403     * to the returned object will not be reflected in the actual properties used by OpenAM.
404     *
405     * @return Properties object with a copy of all the key value pairs.
406     */
407    public static Properties getProperties() {
408        Properties properties = new Properties();
409        properties.putAll(propertiesHolderRef.get().properties);
410        return properties;
411    }
412
413    /**
414     * This method lets you get all the properties defined and their values. The
415     * method first tries to load the properties from java.lang.System followed
416     * by a lookup in the config file.
417     *
418     * @return Properties object with all the key value pairs.
419     *
420     */
421    public static Properties getAll() {
422        Properties properties = new Properties();
423        properties.putAll(propertiesHolderRef.get().properties);
424        // Iterate over the System Properties & add them in result obj
425        properties.putAll(System.getProperties());
426        return properties;
427    }
428
429    /**
430     * This method lets you query for all the platform properties defined and
431     * their values. Returns a Properties object with all the key value pairs.
432     *
433     * @deprecated use <code>getAll()</code>
434     *
435     * @return the platform properties
436     */
437    public static Properties getPlatform() {
438        return getAll();
439    }
440
441    /**
442     * Initializes properties bundle from the <code>file<code> 
443     * passed.
444     *
445     * @param file type <code>String</code>, file name for the resource bundle
446     * @exception MissingResourceException
447     */
448    public static void initializeProperties(String file) throws MissingResourceException {
449        Properties props = new Properties();
450        ResourceBundle bundle = ResourceBundle.getBundle(file);
451        // Copy the properties to props
452        for (String key : bundle.keySet()) {
453            props.put(key, bundle.getString(key));
454        }
455        initializeProperties(props, false, false);
456    }
457
458    public static void initializeProperties(Properties properties) {
459        initializeProperties(properties, false);
460    }
461
462    /**
463     * Initializes the properties to be used by OpenAM. Ideally this
464     * must be called first before any other method is called within OpenAM.
465     * This method provides a programmatic way to set the properties, and will
466     * override similar properties if loaded for a properties file.
467     *
468     * @param properties properties for OpenAM
469     * @param reset <code>true</code> to reset existing properties.
470     */
471    public static void initializeProperties(Properties properties, boolean reset) {
472        initializeProperties(properties, reset, false);
473    }
474
475    /**
476     * Initializes the properties to be used by OpenAM. Ideally this
477     * must be called first before any other method is called within OpenAM.
478     * This method provides a programmatic way to set the properties, and will
479     * override similar properties if loaded for a properties file.
480     *
481     * @param properties properties for OpenAM.
482     * @param reset <code>true</code> to reset existing properties.
483     * @param withDefaults <code>true</code> to include default properties.
484     */
485    public static void initializeProperties(Properties properties, boolean reset, boolean withDefaults) {
486        Properties defaultProp = null;
487        if (withDefaults) {
488            SSOToken appToken = AccessController.doPrivileged(AdminTokenAction.getInstance());
489            defaultProp = ServerConfiguration.getDefaults(appToken);
490        }
491
492        PropertiesHolder oldProps;
493        PropertiesHolder newProps;
494        do {
495            oldProps = propertiesHolderRef.get();
496            final Properties combined = new Properties();
497            if (defaultProp != null) {
498                combined.putAll(defaultProp);
499            }
500
501            if (!reset) {
502                combined.putAll(oldProps.properties);
503            }
504
505            combined.putAll(properties);
506
507            newProps = new PropertiesHolder(Maps.fromProperties(combined));
508        } while (!propertiesHolderRef.compareAndSet(oldProps, newProps));
509    }
510
511    /**
512     * Initializes a property to be used by OpenAM. Ideally this
513     * must be called first before any other method is called within OpenAM.
514     * This method provides a programmatic way to set a specific property, and
515     * will override similar property if loaded for a properties file.
516     *
517     * @param propertyName property name.
518     * @param propertyValue property value.
519     */
520    public static void initializeProperties(String propertyName, String propertyValue) {
521        Properties newProps = new Properties();
522        newProps.put(propertyName, propertyValue);
523        initializeProperties(newProps, false, false);
524    }
525
526    /**
527     * Returns a counter for last modification. The counter is incremented if
528     * the properties are changed by calling the following method
529     * <code>initializeProperties</code>. This is a convenience method for
530     * applications to track changes to OpenAM properties.
531     *
532     * @return counter of the last modification
533     */
534    public static long lastModified() {
535        return propertiesHolderRef.get().lastModified;
536    }
537
538    /**
539     * Returns error messages during initialization, else <code>null</code>.
540     *
541     * @return error messages during initialization
542     */
543    public static String getInitializationError() {
544        return initError;
545    }
546
547    /**
548     * Returns error messages during initialization using the single war
549     * deployment, else <code>null</code>.
550     *
551     * @return error messages during initialization of OpenAM as single war
552     */
553    public static String getSecondaryInitializationError() {
554        return initSecondaryError;
555    }
556
557    /**
558     * Sets the server instance name of which properties are retrieved
559     * to initialized this object.
560     *
561     * @param name Server instance name.
562     */
563    public static void setServerInstanceName(String name) {
564        instanceName = name;
565    }
566
567    /**
568     * Returns the server instance name of which properties are retrieved
569     * to initialized this object.
570     *
571     * @return Server instance name.
572     */
573    public static String getServerInstanceName() {
574        return instanceName;
575    }
576
577    /**
578     * Returns <code>true</code> if instance is running in server mode.
579     *
580     * @return <code>true</code> if instance is running in server mode.
581     */
582    public static boolean isServerMode() {
583        return IsServerModeHolder.isServerMode;
584    }
585
586    /**
587     * Returns the property name to service attribute schema name mapping.
588     *
589     * @return Property name to service attribute schema name mapping.
590     */
591    public static Map getAttributeMap() {
592        return ATTRIBUTE_MAP;
593    }
594
595    private static Map<String, AttributeStruct> initAttributeMapping() {
596        final Map<String, AttributeStruct> attributeMapping = new HashMap<>();
597        try {
598            ResourceBundle rb = ResourceBundle.getBundle("serverAttributeMap");
599            for (String propertyName : rb.keySet()) {
600                attributeMapping.put(propertyName, new AttributeStruct(rb.getString(propertyName)));
601            }
602        } catch (MissingResourceException mse) {
603            // No Resource Bundle Found, Continue.
604            // Could be in Test Mode.
605        }
606        return Collections.unmodifiableMap(attributeMapping);
607    }
608
609
610    /**
611     * Lazy initialisation holder idiom for server mode flag as this is read frequently but never changes.
612     */
613    private static final class IsServerModeHolder {
614        // use getProp and not get method to avoid infinite loop
615        private static final boolean isServerMode = Boolean.parseBoolean(getProp(Constants.SERVER_MODE, "false"));
616    }
617
618    /**
619     * A singleton enum for the configuration listeners, which will be lazily initialized on first use. The code
620     * here cannot be added to the {@code SystemProperties} class initialization as it would create a cyclic
621     * dependency on the static initialization of {@code ConfigurationObserver}.
622     */
623    private enum Listeners {
624        INSTANCE;
625
626        private final Map<String, ServicePropertiesConfigurationListener> servicePropertiesListeners;
627        private final ServicePropertiesConfigurationListener platformServicePropertiesListener;
628
629        Listeners() {
630            servicePropertiesListeners = new HashMap<>();
631            platformServicePropertiesListener = new ServicePropertiesConfigurationListener();
632
633            Map<String, Set<String>> services = new HashMap<>();
634            for (Map.Entry<String, AttributeStruct> property : ATTRIBUTE_MAP.entrySet()) {
635                String serviceName = property.getValue().getServiceName();
636                if (!services.containsKey(serviceName)) {
637                    services.put(serviceName, new HashSet<String>());
638                }
639                services.get(serviceName).add(property.getKey());
640            }
641
642            ConfigurationObserver configurationObserver = ConfigurationObserver.getInstance();
643
644            for (final Map.Entry<String, Set<String>> service : services.entrySet()) {
645                if (!Constants.SVC_NAME_PLATFORM.equals(service.getKey())) {
646                    Set<String> properties = service.getValue();
647                    ServicePropertiesConfigurationListener listener =
648                            new ServicePropertiesConfigurationListener(properties);
649                    for (String property : properties) {
650                        servicePropertiesListeners.put(property, listener);
651                    }
652                    configurationObserver.addServiceListener(listener, new Predicate<String>() {
653                        @Override
654                        public boolean apply(@Nullable String s) {
655                            return s != null && s.equals(service.getKey());
656                        }
657                    });
658                }
659            }
660
661            configurationObserver.addServiceListener(platformServicePropertiesListener, new Predicate<String>() {
662                @Override
663                public boolean apply(@Nullable String s) {
664                    return Constants.SVC_NAME_PLATFORM.equals(s);
665                }
666            });
667        }
668    }
669
670    /**
671     * A listener for the properties that are provided by a single service. Property values are cached so that
672     * property listeners are only notified when the property(-ies) they are observing have changed.
673     */
674    private static final class ServicePropertiesConfigurationListener implements ConfigurationListener {
675
676        private final Map<String, Set<ConfigurationListener>> propertyListeners = new HashMap<>();
677        private final Map<String, String> propertyValues = new HashMap<>();
678
679        private ServicePropertiesConfigurationListener(Set<String> propertyNames) {
680            for (String propertyName : propertyNames) {
681                registerPropertyName(propertyName);
682            }
683        }
684
685        private void registerPropertyName(String propertyName) {
686            propertyListeners.put(propertyName, Collections.synchronizedSet(new HashSet<ConfigurationListener>()));
687        }
688
689        private ServicePropertiesConfigurationListener() {
690            // nothing to see here
691        }
692
693        @Override
694        public void notifyChanges() {
695            Set<ConfigurationListener> affectedListeners = new HashSet<>();
696            for (Map.Entry<String, Set<ConfigurationListener>> propertyListeners : this.propertyListeners.entrySet()) {
697                String propertyName = propertyListeners.getKey();
698                String value = get(propertyName);
699                String oldValue = propertyValues.get(propertyName);
700                if (value != null && !value.equals(oldValue) || value == null && oldValue != null) {
701                    Set<ConfigurationListener> listeners = propertyListeners.getValue();
702                    for (ConfigurationListener listener : listeners) {
703                        affectedListeners.add(listener);
704                    }
705                    propertyValues.put(propertyName, value);
706                }
707            }
708            for (ConfigurationListener listener : affectedListeners) {
709                listener.notifyChanges();
710            }
711        }
712    }
713
714    /**
715     * Listen for runtime changes to a system property value. Only values that are stored in the SMS will
716     * be changed at runtime. See {@code serverdefaults.properties}, {@code amPlatform.xml} and
717     * {@code serverAttributeMap.properties}.
718     *
719     * @param listener The listener to call when one of the provided properties has changed.
720     * @param properties The list of properties that should be observed. A change in any one of these properties
721     *                   will cause the listener to be notified.
722     */
723    public static void observe(ConfigurationListener listener, String... properties) {
724        for (String property : properties) {
725            ServicePropertiesConfigurationListener serviceListener =
726                    Listeners.INSTANCE.servicePropertiesListeners.get(property);
727            if (serviceListener == null) {
728                serviceListener = Listeners.INSTANCE.platformServicePropertiesListener;
729            }
730            synchronized (serviceListener) {
731                if (!serviceListener.propertyListeners.containsKey(property)) {
732                    serviceListener.registerPropertyName(property);
733                }
734                serviceListener.propertyListeners.get(property).add(listener);
735            }
736        }
737    }
738
739    /**
740     * Holds the current properties map together with the tagswap values and last updated timestamp to allow atomic
741     * updates of all three as one unit without locking. This is an immutable structure that is intended to be used
742     * with an AtomicReference.
743     */
744    private static final class PropertiesHolder {
745        private final Map<String, String> properties;
746        private final Map<String, String> tagSwapValues;
747        private final long lastModified;
748
749        PropertiesHolder() {
750            this.properties = Collections.emptyMap();
751            this.tagSwapValues = Collections.emptyMap();
752            this.lastModified = System.currentTimeMillis();
753        }
754
755        PropertiesHolder(final Map<String, String> properties) {
756            Map<String, String> tagSwapMap = new HashMap<>();
757            for (Map.Entry<String, String> tagSwapEntry : TAG_SWAP_PROPERTIES.entrySet()) {
758                String tag = tagSwapEntry.getKey();
759                String propertyName = tagSwapEntry.getValue();
760                String val = System.getProperty(propertyName);
761                if (val == null) {
762                    val = properties.get(propertyName);
763                }
764                tagSwapMap.put(tag, val);
765            }
766            this.properties = Collections.unmodifiableMap(properties);
767            this.tagSwapValues = Collections.unmodifiableMap(tagSwapMap);
768            this.lastModified = System.currentTimeMillis();
769        }
770
771        String getProperty(String name) {
772            return properties.get(name);
773        }
774
775        PropertiesHolder putAll(Properties newProperties) {
776            return new PropertiesHolder(Maps.fromProperties(newProperties));
777        }
778    }
779}