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