password.pwm.config.StoredConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for password.pwm.config.StoredConfiguration.java

Source

/*
 * Password Management Servlets (PWM)
 * http://code.google.com/p/pwm/
 *
 * Copyright (c) 2006-2009 Novell, Inc.
 * Copyright (c) 2009-2015 The PWM Project
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package password.pwm.config;

import org.jdom2.*;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;
import password.pwm.AppProperty;
import password.pwm.PwmConstants;
import password.pwm.bean.UserIdentity;
import password.pwm.config.option.ADPolicyComplexity;
import password.pwm.config.value.PasswordValue;
import password.pwm.config.value.StringArrayValue;
import password.pwm.config.value.StringValue;
import password.pwm.config.value.ValueFactory;
import password.pwm.error.*;
import password.pwm.i18n.Config;
import password.pwm.i18n.LocaleHelper;
import password.pwm.i18n.PwmLocaleBundle;
import password.pwm.util.*;
import password.pwm.util.logging.PwmLogger;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.text.ParseException;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author Jason D. Rivard
 */
public class StoredConfiguration implements Serializable {
    // ------------------------------ FIELDS ------------------------------

    public enum ConfigProperty {
        PROPERTY_KEY_CONFIG_IS_EDITABLE("configIsEditable"), PROPERTY_KEY_CONFIG_EPOCH(
                "configEpoch"), PROPERTY_KEY_TEMPLATE("configTemplate"), PROPERTY_KEY_NOTES(
                        "notes"), PROPERTY_KEY_PASSWORD_HASH(
                                "configPasswordHash"), PROPERTY_KEY_SAVE_CONFIG_ON_START("saveConfigOnStart"),;

        private final String key;

        private ConfigProperty(String key) {
            this.key = key;
        }

        public String getKey() {
            return key;
        }
    }

    public static class SettingMetaData implements Serializable {
        private Date modifyDate;
        private UserIdentity userIdentity;

        public Date getModifyDate() {
            return modifyDate;
        }

        public UserIdentity getUserIdentity() {
            return userIdentity;
        }
    }

    private static final PwmLogger LOGGER = PwmLogger.forClass(StoredConfiguration.class);
    private static final String XML_FORMAT_VERSION = "4";

    public static final String XML_ELEMENT_ROOT = "PwmConfiguration";
    public static final String XML_ELEMENT_PROPERTIES = "properties";
    public static final String XML_ELEMENT_PROPERTY = "property";
    public static final String XML_ELEMENT_SETTINGS = "settings";
    public static final String XML_ELEMENT_SETTING = "setting";
    public static final String XML_ELEMENT_DEFAULT = "default";

    public static final String XML_ATTRIBUTE_TYPE = "type";
    public static final String XML_ATTRIBUTE_KEY = "key";
    public static final String XML_ATTRIBUTE_SYNTAX = "syntax";
    public static final String XML_ATTRIBUTE_PROFILE = "profile";
    public static final String XML_ATTRIBUTE_VALUE_APP = "app";
    public static final String XML_ATTRIBUTE_VALUE_CONFIG = "config";
    public static final String XML_ATTRIBUTE_CREATE_TIME = "createTime";
    public static final String XML_ATTRIBUTE_MODIFY_TIME = "modifyTime";
    public static final String XML_ATTRIBUTE_MODIFY_USER = "modifyUser";
    public static final String XML_ATTRIBUTE_SYNTAX_VERSION = "syntaxVersion";

    private Document document = new Document(new Element(XML_ELEMENT_ROOT));
    private ChangeLog changeLog = new ChangeLog();

    private boolean locked = false;
    private boolean setting_writeLabels = true;
    private final ReentrantReadWriteLock domModifyLock = new ReentrantReadWriteLock();

    // -------------------------- STATIC METHODS --------------------------

    public static StoredConfiguration newStoredConfiguration() {
        return new StoredConfiguration();
    }

    public static StoredConfiguration copy(final StoredConfiguration input) {
        final StoredConfiguration copy = new StoredConfiguration();
        copy.document = input.document.clone();
        return copy;
    }

    public static StoredConfiguration fromXml(final InputStream xmlData) throws PwmUnrecoverableException {
        final Date startTime = new Date();
        //validateXmlSchema(xmlData);

        final Document inputDocument = XmlUtil.parseXml(xmlData);
        final StoredConfiguration newConfiguration = StoredConfiguration.newStoredConfiguration();

        try {
            newConfiguration.document = inputDocument;
            newConfiguration.createTime(); // verify create time;
            ConfigurationCleaner.cleanup(newConfiguration);
        } catch (Exception e) {
            final String errorMsg = "error reading configuration file format, error=" + e.getMessage();
            final ErrorInformation errorInfo = new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR, null,
                    new String[] { errorMsg });
            throw new PwmUnrecoverableException(errorInfo);
        }

        checkIfXmlRequiresUpdate(newConfiguration);
        final TimeDuration totalDuration = TimeDuration.fromCurrent(startTime);
        LOGGER.debug("successfully loaded configuration (" + totalDuration.asCompactString() + ")");
        return newConfiguration;
    }

    /**
     * Loop through all settings to see if setting value has flag {@link StoredValue#requiresStoredUpdate()} set to true.
     * If so, then call {@link #writeSetting(PwmSetting, StoredValue, password.pwm.bean.UserIdentity)} or {@link #writeSetting(PwmSetting, String, StoredValue, password.pwm.bean.UserIdentity)}
     * for that value so that the xml dom can be updated.
     * @param storedConfiguration stored configuration to check
     */
    private static void checkIfXmlRequiresUpdate(final StoredConfiguration storedConfiguration) {
        for (final PwmSetting setting : PwmSetting.values()) {
            if (setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles()) {
                final StoredValue value = storedConfiguration.readSetting(setting);
                if (value.requiresStoredUpdate()) {
                    storedConfiguration.writeSetting(setting, value, null);
                }
            }
        }

        for (final PwmSettingCategory category : PwmSettingCategory.values()) {
            if (category.hasProfiles()) {
                for (final String profileID : storedConfiguration
                        .profilesForSetting(category.getProfileSetting())) {
                    for (final PwmSetting profileSetting : category.getSettings()) {
                        final StoredValue value = storedConfiguration.readSetting(profileSetting, profileID);
                        if (value.requiresStoredUpdate()) {
                            storedConfiguration.writeSetting(profileSetting, profileID, value, null);
                        }
                    }
                }
            }
        }
    }

    public void resetAllPasswordValues(final String comment) {
        for (final Iterator<SettingValueRecord> settingValueRecordIterator = new StoredValueIterator(
                false); settingValueRecordIterator.hasNext();) {
            final SettingValueRecord settingValueRecord = settingValueRecordIterator.next();
            if (settingValueRecord.getSetting().getSyntax() == PwmSettingSyntax.PASSWORD) {
                this.resetSetting(settingValueRecord.getSetting(), settingValueRecord.getProfile(), null);
                if (comment != null && !comment.isEmpty()) {
                    final XPathExpression xp = XPathBuilder.xpathForSetting(settingValueRecord.getSetting(),
                            settingValueRecord.getProfile());
                    final Element settingElement = (Element) xp.evaluateFirst(document);
                    if (settingElement != null) {
                        settingElement.addContent(new Comment(comment));
                    }
                }
            }
        }
    }

    public StoredConfiguration() {
        ConfigurationCleaner.cleanup(this);
        final String createTime = PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date());
        document.getRootElement().setAttribute(XML_ATTRIBUTE_CREATE_TIME, createTime);
    }

    public String readConfigProperty(final ConfigProperty propertyName) {
        final XPathExpression xp = XPathBuilder.xpathForConfigProperty(propertyName);
        final Element propertyElement = (Element) xp.evaluateFirst(document);
        return propertyElement == null ? null : propertyElement.getText();
    }

    public void writeConfigProperty(final ConfigProperty propertyName, final String value) {
        domModifyLock.writeLock().lock();
        try {

            final XPathExpression xp = XPathBuilder.xpathForConfigProperty(propertyName);
            final List<Element> propertyElements = xp.evaluate(document);
            for (final Element propertyElement : propertyElements) {
                propertyElement.detach();
            }

            final Element propertyElement = new Element(XML_ELEMENT_PROPERTY);
            propertyElement.setAttribute(new Attribute(XML_ATTRIBUTE_KEY, propertyName.getKey()));
            propertyElement.setContent(new Text(value));

            if (null == XPathBuilder.xpathForConfigProperties().evaluateFirst(document)) {
                Element configProperties = new Element(XML_ELEMENT_PROPERTIES);
                configProperties.setAttribute(new Attribute(XML_ATTRIBUTE_TYPE, XML_ATTRIBUTE_VALUE_CONFIG));
                document.getRootElement().addContent(configProperties);
            }

            final XPathExpression xp2 = XPathBuilder.xpathForConfigProperties();
            final Element propertiesElement = (Element) xp2.evaluateFirst(document);
            propertyElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME,
                    PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
            propertiesElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME,
                    PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
            propertiesElement.addContent(propertyElement);
        } finally {
            domModifyLock.writeLock().unlock();
        }
    }

    public void lock() {
        locked = true;
    }

    public Map<String, String> readLocaleBundleMap(final String bundleName, final String keyName) {
        domModifyLock.readLock().lock();
        try {
            final XPathExpression xp = XPathBuilder.xpathForLocaleBundleSetting(bundleName, keyName);
            final Element localeBundleElement = (Element) xp.evaluateFirst(document);
            if (localeBundleElement != null) {
                final Map<String, String> bundleMap = new LinkedHashMap<>();
                for (final Element valueElement : localeBundleElement.getChildren("value")) {
                    final String localeStrValue = valueElement.getAttributeValue("locale");
                    bundleMap.put(localeStrValue == null ? "" : localeStrValue, valueElement.getText());
                }
                if (!bundleMap.isEmpty()) {
                    return bundleMap;
                }
            }
        } finally {
            domModifyLock.readLock().unlock();
        }
        return Collections.emptyMap();
    }

    public Map<String, Object> toOutputMap(final Locale locale) {
        final List<Map<String, String>> settingData = new ArrayList<>();
        for (final StoredConfiguration.SettingValueRecord settingValueRecord : this.modifiedSettings()) {
            final Map<String, String> recordMap = new HashMap<>();
            recordMap.put("label", settingValueRecord.getSetting().getLabel(locale));
            if (settingValueRecord.getProfile() != null) {
                recordMap.put("profile", settingValueRecord.getProfile());
            }
            if (settingValueRecord.getStoredValue() != null) {
                recordMap.put("value", settingValueRecord.getStoredValue().toDebugString(true, locale));
            }
            final SettingMetaData settingMetaData = readSettingMetadata(settingValueRecord.getSetting(),
                    settingValueRecord.getProfile());
            if (settingMetaData != null) {
                if (settingMetaData.getModifyDate() != null) {
                    recordMap.put("modifyTime",
                            PwmConstants.DEFAULT_DATETIME_FORMAT.format(settingMetaData.getModifyDate()));
                }
                if (settingMetaData.getUserIdentity() != null) {
                    recordMap.put("modifyUser", settingMetaData.getUserIdentity().toDisplayString());
                }
            }
            settingData.add(recordMap);
        }

        final HashMap<String, Object> outputObj = new HashMap<>();
        outputObj.put("settings", settingData);
        outputObj.put("template", this.getTemplate().toString());

        return Collections.unmodifiableMap(outputObj);
    }

    public void resetLocaleBundleMap(final String bundleName, final String keyName) {
        preModifyActions();
        domModifyLock.writeLock().lock();
        try {
            final XPathExpression xp = XPathBuilder.xpathForLocaleBundleSetting(bundleName, keyName);
            final List<Element> oldBundleElements = xp.evaluate(document);
            if (oldBundleElements != null) {
                for (final Element element : oldBundleElements) {
                    element.detach();
                }
            }
        } finally {
            domModifyLock.writeLock().unlock();
        }
    }

    public void resetSetting(final PwmSetting setting, final UserIdentity userIdentity) {
        resetSetting(setting, null, userIdentity);
    }

    public void resetSetting(final PwmSetting setting, final String profileID, final UserIdentity userIdentity) {
        changeLog.updateChangeLog(setting, profileID, defaultValue(setting, this.getTemplate()));
        domModifyLock.writeLock().lock();
        preModifyActions();
        try {
            final Element settingElement = createOrGetSettingElement(document, setting, profileID);
            settingElement.removeContent();
            settingElement.addContent(new Element(XML_ELEMENT_DEFAULT));
            updateMetaData(settingElement, userIdentity);
        } finally {
            domModifyLock.writeLock().unlock();
        }
    }

    public boolean isDefaultValue(final PwmSetting setting) {
        return isDefaultValue(setting, null);
    }

    public boolean isDefaultValue(final PwmSetting setting, final String profileID) {
        domModifyLock.readLock().lock();
        try {
            final StoredValue currentValue = readSetting(setting, profileID);
            if (setting.getSyntax() == PwmSettingSyntax.PASSWORD) {
                return currentValue == null || currentValue.toNativeObject() == null;
            }
            final StoredValue defaultValue = defaultValue(setting, this.getTemplate());
            final String currentJsonValue = JsonUtil.serialize((Serializable) currentValue.toNativeObject());
            final String defaultJsonValue = JsonUtil.serialize((Serializable) defaultValue.toNativeObject());
            return defaultJsonValue.equalsIgnoreCase(currentJsonValue);
        } finally {
            domModifyLock.readLock().unlock();
        }
    }

    private static StoredValue defaultValue(final PwmSetting pwmSetting, final PwmSetting.Template template) {
        try {
            return pwmSetting.getDefaultValue(template);
        } catch (PwmException e) {
            final String errorMsg = "error reading default value for setting " + pwmSetting.toString() + ", error: "
                    + e.getErrorInformation().toDebugStr();
            LOGGER.error(errorMsg, e);
            throw new IllegalStateException(errorMsg);
        }
    }

    public PwmSetting.Template getTemplate() {
        final String propertyValue = readConfigProperty(ConfigProperty.PROPERTY_KEY_TEMPLATE);
        try {
            return PwmSetting.Template.valueOf(propertyValue);
        } catch (IllegalArgumentException e) {
            return PwmSetting.Template.DEFAULT;
        } catch (NullPointerException e) {
            return PwmSetting.Template.DEFAULT;
        }
    }

    public void setTemplate(PwmSetting.Template template) {
        writeConfigProperty(ConfigProperty.PROPERTY_KEY_TEMPLATE, template.toString());
    }

    public String toString() {
        return toString(false);
    }

    public String toString(final PwmSetting setting, final String profileID) {
        final StoredValue storedValue = readSetting(setting, profileID);
        return setting.getKey() + "=" + storedValue.toDebugString(false, null);
    }

    List<SettingValueRecord> modifiedSettings() {
        final List<SettingValueRecord> returnObj = new ArrayList<>();
        domModifyLock.readLock().lock();
        try {
            for (final PwmSetting setting : PwmSetting.values()) {
                if (setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles()) {
                    if (!isDefaultValue(setting, null)) {
                        final StoredValue value = readSetting(setting);
                        if (value != null) {
                            returnObj.add(new SettingValueRecord(setting, null, value));
                        }
                    }
                }
            }

            for (final PwmSettingCategory category : PwmSettingCategory.values()) {
                if (category.hasProfiles()) {
                    for (final String profileID : this.profilesForSetting(category.getProfileSetting())) {
                        for (final PwmSetting profileSetting : category.getSettings()) {
                            if (!isDefaultValue(profileSetting, profileID)) {
                                final StoredValue value = readSetting(profileSetting, profileID);
                                if (value != null) {
                                    returnObj.add(new SettingValueRecord(profileSetting, profileID, value));

                                }
                            }
                        }
                    }
                }
            }

            return returnObj;
        } finally {
            domModifyLock.readLock().unlock();
        }
    }

    public String toString(final boolean linebreaks) {
        domModifyLock.readLock().lock();
        try {
            final TreeMap<String, Object> outputObject = new TreeMap<>();

            for (final PwmSetting setting : PwmSetting.values()) {
                if (setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles()) {
                    if (!isDefaultValue(setting, null)) {
                        final StoredValue value = readSetting(setting);
                        outputObject.put(setting.getKey(), value.toDebugString(false, null));
                    }
                }
            }

            for (final PwmSettingCategory category : PwmSettingCategory.values()) {
                if (category.hasProfiles()) {
                    final TreeMap<String, Object> profiles = new TreeMap<>();
                    for (final String profileID : this.profilesForSetting(category.getProfileSetting())) {
                        final TreeMap<String, String> profileObject = new TreeMap<>();
                        for (final PwmSetting profileSetting : category.getSettings()) {
                            if (!isDefaultValue(profileSetting, profileID)) {
                                final StoredValue value = readSetting(profileSetting, profileID);
                                profileObject.put(profileSetting.getKey(), value.toDebugString(false, null));
                            }
                        }
                        profiles.put(profileID, profileObject);
                    }
                    outputObject.put(category.getProfileSetting().getKey(), profiles);
                }
            }

            return linebreaks ? JsonUtil.serialize(outputObject, JsonUtil.Flag.PrettyPrint)
                    : JsonUtil.serialize(outputObject);
        } finally {
            domModifyLock.readLock().unlock();
        }
    }

    public void toXml(final OutputStream outputStream) throws IOException, PwmUnrecoverableException {
        ConfigurationCleaner.updateMandatoryElements(document);
        XmlUtil.outputDocument(document, outputStream);
    }

    public List<String> profilesForSetting(final PwmSetting pwmSetting) {
        if (!pwmSetting.getCategory().hasProfiles() && pwmSetting.getSyntax() != PwmSettingSyntax.PROFILE) {
            throw new IllegalArgumentException(
                    "cannot build profile list for non-profile setting " + pwmSetting.toString());
        }

        final PwmSetting profileSetting;
        if (pwmSetting.getSyntax() == PwmSettingSyntax.PROFILE) {
            profileSetting = pwmSetting;
        } else {
            profileSetting = pwmSetting.getCategory().getProfileSetting();
        }

        final List<String> settingValues = (List<String>) readSetting(profileSetting).toNativeObject();
        final LinkedList<String> profiles = new LinkedList<>();
        profiles.addAll(settingValues);
        for (Iterator<String> iterator = profiles.iterator(); iterator.hasNext();) {
            final String profile = iterator.next();
            if (profile == null || profile.length() < 1) {
                iterator.remove();
            }
        }
        return Collections.unmodifiableList(profiles);
    }

    public List<String> validateValues() {
        final long startTime = System.currentTimeMillis();
        final List<String> errorStrings = new ArrayList<>();

        for (final PwmSetting loopSetting : PwmSetting.values()) {
            final StringBuilder errorPrefix = new StringBuilder();
            errorPrefix.append(loopSetting.getCategory().getLabel(PwmConstants.DEFAULT_LOCALE));
            errorPrefix.append("-");
            errorPrefix.append(loopSetting.getLabel(PwmConstants.DEFAULT_LOCALE));

            if (loopSetting.getCategory().hasProfiles()) {
                errorPrefix.append("-");
                for (final String profile : profilesForSetting(loopSetting)) {
                    final String errorAppend = profile;
                    final StoredValue loopValue = readSetting(loopSetting, profile);

                    try {
                        final List<String> errors = loopValue.validateValue(loopSetting);
                        for (final String loopError : errors) {
                            errorStrings.add(errorPrefix + errorAppend + " " + loopError);
                        }
                    } catch (Exception e) {
                        LOGGER.error("unexpected error during validate value for " + errorPrefix + errorAppend
                                + ", error: " + e.getMessage(), e);
                    }
                }
            } else {
                errorPrefix.append(" ");
                final StoredValue loopValue = readSetting(loopSetting);

                try {
                    final List<String> errors = loopValue.validateValue(loopSetting);
                    for (final String loopError : errors) {
                        errorStrings.add(errorPrefix + loopError);
                    }
                } catch (Exception e) {
                    LOGGER.error("unexpected error during validate value for " + errorPrefix + ", error: "
                            + e.getMessage(), e);
                }
            }
        }

        LOGGER.trace("StoredConfiguration validator completed in "
                + TimeDuration.fromCurrent(startTime).asCompactString());
        return errorStrings;
    }

    public SettingMetaData readSettingMetadata(final PwmSetting setting, final String profileID) {
        final XPathExpression xp = XPathBuilder.xpathForSetting(setting, profileID);
        final Element settingElement = (Element) xp.evaluateFirst(document);

        if (settingElement == null) {
            return null;
        }

        final SettingMetaData metaData = new SettingMetaData();
        try {
            if (settingElement.getAttributeValue(XML_ATTRIBUTE_MODIFY_TIME) != null) {
                metaData.modifyDate = PwmConstants.DEFAULT_DATETIME_FORMAT
                        .parse(settingElement.getAttributeValue(XML_ATTRIBUTE_MODIFY_TIME));
            }
        } catch (Exception e) {
            LOGGER.error("can't read modifyDate for setting " + setting.getKey() + ", profile " + profileID
                    + ", error: " + e.getMessage());
        }
        try {
            if (settingElement.getAttributeValue(XML_ATTRIBUTE_MODIFY_USER) != null) {
                metaData.userIdentity = UserIdentity
                        .fromDelimitedKey(settingElement.getAttributeValue(XML_ATTRIBUTE_MODIFY_USER));
            }
        } catch (Exception e) {
            LOGGER.error("can't read userIdentity for setting " + setting.getKey() + ", profile " + profileID
                    + ", error: " + e.getMessage());
        }
        return metaData;
    }

    public List<ConfigRecordID> search(final String searchTerm, final Locale locale) {
        if (searchTerm == null) {
            return Collections.emptyList();
        }

        final LinkedHashSet<ConfigRecordID> returnSet = new LinkedHashSet<>();
        boolean firstIter = true;
        for (final String searchWord : searchTerm.split(" ")) {
            final LinkedHashSet<ConfigRecordID> loopResults = new LinkedHashSet<>();
            for (final PwmSetting loopSetting : PwmSetting.values()) {
                if (loopSetting.getCategory().hasProfiles()) {
                    for (final String profile : profilesForSetting(loopSetting)) {
                        final StoredValue loopValue = readSetting(loopSetting, profile);
                        if (matchSetting(loopSetting, loopValue, searchWord, locale)) {
                            loopResults.add(
                                    new ConfigRecordID(ConfigRecordID.RecordType.SETTING, loopSetting, profile));
                        }
                    }
                } else {
                    final StoredValue loopValue = readSetting(loopSetting);
                    if (matchSetting(loopSetting, loopValue, searchWord, locale)) {
                        loopResults.add(new ConfigRecordID(ConfigRecordID.RecordType.SETTING, loopSetting, null));
                    }
                }
            }
            if (firstIter) {
                returnSet.addAll(loopResults);
            } else {
                returnSet.retainAll(loopResults);
            }
            firstIter = false;
        }

        return new ArrayList<>(returnSet);
    }

    private boolean matchSetting(final PwmSetting setting, final StoredValue value, final String searchTerm,
            final Locale locale) {
        if (setting.isHidden() || setting.getCategory().isHidden()) {
            return false;
        }
        {
            final String key = setting.getKey();
            if (key.toLowerCase().contains(searchTerm.toLowerCase())) {
                return true;
            }
        }
        {
            final String label = setting.getLabel(locale);
            if (label.toLowerCase().contains(searchTerm.toLowerCase())) {
                return true;
            }
        }
        {
            final String descr = setting.getDescription(locale);
            if (descr.toLowerCase().contains(searchTerm.toLowerCase())) {
                return true;
            }
        }
        {
            final String menuLocationString = setting.toMenuLocationDebug(null, locale);
            if (menuLocationString.toLowerCase().contains(searchTerm.toLowerCase())) {
                return true;
            }
        }

        if (setting.isConfidential()) {
            return false;
        }
        {
            final String valueDebug = value.toDebugString(true, locale);
            if (valueDebug.toLowerCase().contains(searchTerm.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    public StoredValue readSetting(final PwmSetting setting) {
        return readSetting(setting, null);
    }

    public StoredValue readSetting(final PwmSetting setting, final String profileID) {
        if (profileID == null && setting.getCategory().hasProfiles()) {
            throw new IllegalArgumentException(
                    "reading of setting " + setting.getKey() + " requires a non-null profileID");
        }
        if (profileID != null && !setting.getCategory().hasProfiles()) {
            throw new IllegalStateException(
                    "cannot read setting key " + setting.getKey() + " with non-null profileID");
        }
        domModifyLock.readLock().lock();
        try {
            final XPathExpression xp = XPathBuilder.xpathForSetting(setting, profileID);
            final Element settingElement = (Element) xp.evaluateFirst(document);

            if (settingElement == null) {
                return defaultValue(setting, getTemplate());
            }

            if (settingElement.getChild(XML_ELEMENT_DEFAULT) != null) {
                return defaultValue(setting, getTemplate());
            }

            try {
                return ValueFactory.fromXmlValues(setting, settingElement, getKey());
            } catch (PwmException e) {
                final String errorMsg = "unexpected error reading setting '" + setting.getKey() + "' profile '"
                        + profileID + "', error: " + e.getMessage();
                throw new IllegalStateException(errorMsg);
            }
        } finally {
            domModifyLock.readLock().unlock();
        }
    }

    public void writeLocaleBundleMap(final String bundleName, final String keyName,
            final Map<String, String> localeMap) {
        ResourceBundle theBundle = null;
        for (final PwmLocaleBundle bundle : PwmLocaleBundle.values()) {
            if (bundle.getTheClass().getName().equals(bundleName)) {
                theBundle = ResourceBundle.getBundle(bundleName);
            }
        }

        if (theBundle == null) {
            LOGGER.info("ignoring unknown locale bundle for bundle=" + bundleName + ", key=" + keyName);
            return;
        }

        if (theBundle.getString(keyName) == null) {
            LOGGER.info("ignoring unknown key for bundle=" + bundleName + ", key=" + keyName);
            return;
        }

        resetLocaleBundleMap(bundleName, keyName);
        if (localeMap == null || localeMap.isEmpty()) {
            LOGGER.info("cleared locale bundle map for bundle=" + bundleName + ", key=" + keyName);
            return;
        }

        preModifyActions();
        changeLog.updateChangeLog(bundleName, keyName, localeMap);
        try {
            domModifyLock.writeLock().lock();
            final Element localeBundleElement = new Element("localeBundle");
            localeBundleElement.setAttribute("bundle", bundleName);
            localeBundleElement.setAttribute("key", keyName);
            for (final String locale : localeMap.keySet()) {
                final Element valueElement = new Element("value");
                if (locale != null && locale.length() > 0) {
                    valueElement.setAttribute("locale", locale);
                }
                valueElement.setContent(new CDATA(localeMap.get(locale)));
                localeBundleElement.addContent(valueElement);
            }
            localeBundleElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME,
                    PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
            document.getRootElement().addContent(localeBundleElement);
        } finally {
            domModifyLock.writeLock().unlock();
        }
    }

    public void writeSetting(final PwmSetting setting, final StoredValue value, final UserIdentity userIdentity) {
        writeSetting(setting, null, value, userIdentity);
    }

    public void writeSetting(final PwmSetting setting, final String profileID, final StoredValue value,
            final UserIdentity userIdentity) {
        if (profileID == null && setting.getCategory().hasProfiles()) {
            throw new IllegalArgumentException(
                    "reading of setting " + setting.getKey() + " requires a non-null profileID");
        }
        if (profileID != null && !setting.getCategory().hasProfiles()) {
            throw new IllegalArgumentException("cannot specify profile for non-profile setting");
        }

        preModifyActions();
        changeLog.updateChangeLog(setting, profileID, value);
        domModifyLock.writeLock().lock();
        try {
            final Element settingElement = createOrGetSettingElement(document, setting, profileID);
            settingElement.removeContent();
            settingElement.setAttribute(XML_ATTRIBUTE_SYNTAX, setting.getSyntax().toString());
            settingElement.setAttribute(XML_ATTRIBUTE_SYNTAX_VERSION,
                    Integer.toString(value.currentSyntaxVersion()));

            if (setting_writeLabels) {
                final Element labelElement = new Element("label");
                labelElement.addContent(setting.getLabel(PwmConstants.DEFAULT_LOCALE));
                settingElement.addContent(labelElement);
            }

            if (setting.getSyntax() == PwmSettingSyntax.PASSWORD) {
                final List<Element> valueElements = ((PasswordValue) value).toXmlValues("value", getKey());
                settingElement
                        .addContent(new Comment("Note: This value is encrypted and can not be edited directly."));
                settingElement
                        .addContent(new Comment("Please use the Configuration Manager GUI to modify this value."));
                settingElement.addContent(valueElements);
            } else {
                settingElement.addContent(value.toXmlValues("value"));
            }

            updateMetaData(settingElement, userIdentity);
        } finally {
            domModifyLock.writeLock().unlock();
        }
    }

    public String settingChecksum() throws PwmUnrecoverableException {
        final Date startTime = new Date();

        final List<SettingValueRecord> modifiedSettings = modifiedSettings();
        final StringBuilder sb = new StringBuilder();
        sb.append("PwmSettingsChecksum");
        for (SettingValueRecord settingValueRecord : modifiedSettings) {
            final StoredValue storedValue = settingValueRecord.getStoredValue();
            sb.append(storedValue.valueHash());
        }

        final String result = SecureHelper.hash(sb.toString());
        LOGGER.trace("computed setting checksum in " + TimeDuration.fromCurrent(startTime).asCompactString());
        return result;
    }

    private void preModifyActions() {
        if (locked) {
            throw new UnsupportedOperationException("StoredConfiguration is locked and cannot be modified");
        }
        document.getRootElement().setAttribute(XML_ATTRIBUTE_MODIFY_TIME,
                PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
    }

    // -------------------------- INNER CLASSES --------------------------

    public void setPassword(final String password) throws PwmOperationalException {
        if (password == null || password.isEmpty()) {
            throw new PwmOperationalException(new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR, null,
                    new String[] { "can not set blank password" }));
        }
        final String trimmedPassword = password.trim();
        if (trimmedPassword.length() < 1) {
            throw new PwmOperationalException(new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR, null,
                    new String[] { "can not set blank password" }));
        }

        final String salt = BCrypt.gensalt();
        final String passwordHash = BCrypt.hashpw(password, salt);
        this.writeConfigProperty(ConfigProperty.PROPERTY_KEY_PASSWORD_HASH, passwordHash);
    }

    public boolean verifyPassword(final String password) {
        if (!hasPassword()) {
            return false;
        }
        final String passwordHash = this.readConfigProperty(ConfigProperty.PROPERTY_KEY_PASSWORD_HASH);
        return BCrypt.checkpw(password, passwordHash);
    }

    public boolean hasPassword() {
        final String passwordHash = this.readConfigProperty(ConfigProperty.PROPERTY_KEY_PASSWORD_HASH);
        return passwordHash != null && passwordHash.length() > 0;
    }

    private static abstract class XPathBuilder {
        private static XPathExpression xpathForLocaleBundleSetting(final String bundleName, final String keyName) {
            final XPathFactory xpfac = XPathFactory.instance();
            final String xpathString;
            xpathString = "//localeBundle[@bundle=\"" + bundleName + "\"][@key=\"" + keyName + "\"]";
            return xpfac.compile(xpathString);
        }

        private static XPathExpression xpathForSetting(final PwmSetting setting, final String profileID) {
            final XPathFactory xpfac = XPathFactory.instance();
            final String xpathString;
            if (profileID == null || profileID.length() < 1) {
                xpathString = "//setting[@key=\"" + setting.getKey() + "\"][(not (@profile)) or @profile=\"\"]";
            } else {
                xpathString = "//setting[@key=\"" + setting.getKey() + "\"][@profile=\"" + profileID + "\"]";
            }

            return xpfac.compile(xpathString);
        }

        private static XPathExpression xpathForAppProperty(final AppProperty appProperty) {
            final XPathFactory xpfac = XPathFactory.instance();
            final String xpathString;
            xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\""
                    + XML_ATTRIBUTE_VALUE_APP + "\"]/" + XML_ELEMENT_PROPERTY + "[@" + XML_ATTRIBUTE_KEY + "=\""
                    + appProperty.getKey() + "\"]";
            return xpfac.compile(xpathString);
        }

        private static XPathExpression xpathForAppProperties() {
            final XPathFactory xpfac = XPathFactory.instance();
            final String xpathString;
            xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\""
                    + XML_ATTRIBUTE_VALUE_APP + "\"]";
            return xpfac.compile(xpathString);
        }

        private static XPathExpression xpathForConfigProperty(final ConfigProperty configProperty) {
            final XPathFactory xpfac = XPathFactory.instance();
            final String xpathString;
            xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\""
                    + XML_ATTRIBUTE_VALUE_CONFIG + "\"]/" + XML_ELEMENT_PROPERTY + "[@" + XML_ATTRIBUTE_KEY + "=\""
                    + configProperty.getKey() + "\"]";
            return xpfac.compile(xpathString);
        }

        private static XPathExpression xpathForConfigProperties() {
            final XPathFactory xpfac = XPathFactory.instance();
            final String xpathString;
            xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\""
                    + XML_ATTRIBUTE_VALUE_CONFIG + "\"]";
            return xpfac.compile(xpathString);
        }
    }

    private static class ConfigurationCleaner {
        private static void cleanup(final StoredConfiguration configuration) {
            updateProperitiesWithoutType(configuration);
            updateMandatoryElements(configuration.document);
            profilizeNonProfiledSettings(configuration);
            stripOrphanedProfileSettings(configuration);
            migrateAppProperties(configuration);
            updateDeprecatedSettings(configuration);
        }

        private static void updateMandatoryElements(final Document document) {
            final Element rootElement = document.getRootElement();

            {
                final XPathExpression commentXPath = XPathFactory.instance().compile("//comment()[1]");
                final Comment existingComment = (Comment) commentXPath.evaluateFirst(rootElement);
                if (existingComment != null) {
                    existingComment.detach();
                }
                final Comment comment = new Comment(generateCommentText());
                rootElement.addContent(0, comment);
            }

            rootElement.setAttribute("pwmVersion", PwmConstants.BUILD_VERSION);
            rootElement.setAttribute("pwmBuild", PwmConstants.BUILD_NUMBER);
            rootElement.setAttribute("pwmBuildType", PwmConstants.BUILD_TYPE);
            rootElement.setAttribute("xmlVersion", XML_FORMAT_VERSION);

            { // migrate old properties

                // read correct (new) //properties[@type="config"]
                final XPathExpression configPropertiesXpath = XPathFactory.instance()
                        .compile("//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\""
                                + XML_ATTRIBUTE_VALUE_CONFIG + "\"]");
                final Element configPropertiesElement = (Element) configPropertiesXpath.evaluateFirst(rootElement);

                // read list of old //properties[not (@type)]/property
                final XPathExpression nonAttributedProperty = XPathFactory.instance().compile("//"
                        + XML_ELEMENT_PROPERTIES + "[not (@" + XML_ATTRIBUTE_TYPE + ")]/" + XML_ELEMENT_PROPERTY);
                final List<Element> nonAttributedProperties = nonAttributedProperty.evaluate(rootElement);

                if (configPropertiesElement != null && nonAttributedProperties != null) {
                    for (Element element : nonAttributedProperties) {
                        element.detach();
                        configPropertiesElement.addContent(element);
                    }
                }

                // remove old //properties[not (@type] element
                final XPathExpression oldPropertiesXpath = XPathFactory.instance()
                        .compile("//" + XML_ELEMENT_PROPERTIES + "[not (@" + XML_ATTRIBUTE_TYPE + ")]");
                final List<Element> oldPropertiesElements = oldPropertiesXpath.evaluate(rootElement);
                if (oldPropertiesElements != null) {
                    for (Element element : oldPropertiesElements) {
                        element.detach();
                    }
                }
            }
        }

        private static String generateCommentText() {
            final StringBuilder commentText = new StringBuilder();
            commentText.append("\t\t").append(" ").append("\n");
            commentText.append("\t\t").append("This configuration file has been auto-generated by the ")
                    .append(PwmConstants.PWM_APP_NAME).append(" password self service application.").append("\n");
            commentText.append("\t\t").append("").append("\n");
            commentText.append("\t\t").append(
                    "WARNING: This configuration file contains sensitive security information, please handle with care!")
                    .append("\n");
            commentText.append("\t\t").append("").append("\n");
            commentText.append("\t\t").append(
                    "WARNING: If a server is currently running using this configuration file, it will be restarted")
                    .append("\n");
            commentText.append("\t\t")
                    .append("         and the configuration updated immediately when it is modified.").append("\n");
            commentText.append("\t\t").append("").append("\n");
            commentText.append("\t\t").append(
                    "NOTICE: This file is encoded as UTF-8.  Do not save or edit this file with an editor that does not")
                    .append("\n");
            commentText.append("\t\t").append("        support UTF-8 encoding.").append("\n");
            commentText.append("\t\t").append("").append("\n");
            commentText.append("\t\t").append(
                    "If unable to edit using the application ConfigurationEditor web UI, the following options are available.")
                    .append("\n");
            commentText.append("\t\t").append("   or 1. Edit this file directly by hand.").append("\n");
            commentText.append("\t\t").append(
                    "   or 2. Unlock the configuration by setting the property 'configIsEditable' to 'true' in this file.  This will ")
                    .append("\n");
            commentText.append("\t\t").append(
                    "         allow access to the ConfigurationEditor web UI without having to authenticate to an LDAP server first.")
                    .append("\n");
            commentText.append("\t\t")
                    .append("   or 3. Unlock the configuration by using the the command line utility. ")
                    .append("\n");
            commentText.append("\t\t").append("").append("\n");
            return commentText.toString();
        }

        private static void profilizeNonProfiledSettings(final StoredConfiguration storedConfiguration) {
            final String NEW_PROFILE_NAME = "default";
            final Document document = storedConfiguration.document;
            for (final PwmSetting setting : PwmSetting.values()) {
                if (setting.getCategory().hasProfiles()) {

                    final XPathExpression xp = XPathBuilder.xpathForSetting(setting, null);
                    final Element settingElement = (Element) xp.evaluateFirst(document);
                    if (settingElement != null) {
                        LOGGER.info("moving setting " + setting.getKey()
                                + " without profile attribute to profile \"" + NEW_PROFILE_NAME + "\".");
                        // change setting to "default" profile.
                        settingElement.setAttribute(XML_ATTRIBUTE_PROFILE, NEW_PROFILE_NAME);

                        final PwmSetting profileSetting = setting.getCategory().getProfileSetting();
                        final List<String> profileStringDefinitions = new ArrayList<>();
                        {
                            final StringArrayValue profileDefinitions = (StringArrayValue) storedConfiguration
                                    .readSetting(profileSetting);
                            if (profileDefinitions != null) {
                                if (profileDefinitions.toNativeObject() != null) {
                                    profileStringDefinitions.addAll(profileDefinitions.toNativeObject());
                                }
                            }
                        }

                        if (!profileStringDefinitions.contains(NEW_PROFILE_NAME)) {
                            profileStringDefinitions.add(NEW_PROFILE_NAME);
                            storedConfiguration.writeSetting(profileSetting,
                                    new StringArrayValue(profileStringDefinitions), null);
                        }
                    }
                }
            }
        }

        private static void updateProperitiesWithoutType(final StoredConfiguration storedConfiguration) {
            final Document document = storedConfiguration.document;
            final String xpathString = "//properties[not(@type)]";
            final XPathFactory xpfac = XPathFactory.instance();
            final XPathExpression xp = xpfac.compile(xpathString);
            final List<Element> propertiesElements = (List<Element>) xp.evaluate(document);
            for (final Element propertiesElement : propertiesElements) {
                propertiesElement.setAttribute(XML_ATTRIBUTE_TYPE, XML_ATTRIBUTE_VALUE_CONFIG);
            }
        }

        private static void stripOrphanedProfileSettings(final StoredConfiguration storedConfiguration) {
            final Document document = storedConfiguration.document;
            final XPathFactory xpfac = XPathFactory.instance();
            for (final PwmSetting setting : PwmSetting.values()) {
                if (setting.getCategory().hasProfiles()) {
                    final List<String> validProfiles = storedConfiguration.profilesForSetting(setting);
                    final String xpathString = "//setting[@key=\"" + setting.getKey() + "\"]";
                    final XPathExpression xp = xpfac.compile(xpathString);
                    final List<Element> settingElements = (List<Element>) xp.evaluate(document);
                    for (final Element settingElement : settingElements) {
                        final String profileID = settingElement.getAttributeValue(XML_ATTRIBUTE_PROFILE);
                        if (profileID != null) {
                            if (!validProfiles.contains(profileID)) {
                                LOGGER.info("removing setting " + setting.getKey() + " with profile \"" + profileID
                                        + "\", profile is not a valid profile");
                                settingElement.detach();
                            }
                        }
                    }
                }
            }
        }

        private static void migrateAppProperties(final StoredConfiguration storedConfiguration) {
            final Document document = storedConfiguration.document;
            final XPathExpression xPathExpression = XPathBuilder.xpathForAppProperties();
            final List<Element> appPropertiesElements = (List<Element>) xPathExpression.evaluate(document);
            for (final Element element : appPropertiesElements) {
                final List<Element> properties = element.getChildren();
                for (final Element property : properties) {
                    final String key = property.getAttributeValue("key");
                    final String value = property.getText();
                    if (key != null && !key.isEmpty() && value != null && !value.isEmpty()) {
                        LOGGER.info("migrating app-property config element '" + key + "' to setting "
                                + PwmSetting.APP_PROPERTY_OVERRIDES.getKey());
                        final String newValue = key + "=" + value;
                        List<String> existingValues = (List<String>) storedConfiguration
                                .readSetting(PwmSetting.APP_PROPERTY_OVERRIDES).toNativeObject();
                        if (existingValues == null) {
                            existingValues = new ArrayList<>();
                        }
                        existingValues = new ArrayList<>(existingValues);
                        existingValues.add(newValue);
                        storedConfiguration.writeSetting(PwmSetting.APP_PROPERTY_OVERRIDES,
                                new StringArrayValue(existingValues), null);
                    }
                }
                element.detach();
            }
        }

        private static void updateDeprecatedSettings(final StoredConfiguration storedConfiguration) {
            final UserIdentity actor = new UserIdentity("UpgradeProcessor", null);
            for (final String profileID : storedConfiguration
                    .profilesForSetting(PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY)) {
                if (!storedConfiguration.isDefaultValue(PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID)) {
                    boolean ad2003Enabled = (boolean) storedConfiguration
                            .readSetting(PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY).toNativeObject();
                    final StoredValue value;
                    if (ad2003Enabled) {
                        value = new StringValue(ADPolicyComplexity.AD2003.toString());
                    } else {
                        value = new StringValue(ADPolicyComplexity.NONE.toString());
                    }
                    LOGGER.warn("converting deprecated non-default setting "
                            + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY.getKey() + "/" + profileID
                            + " to replacement setting " + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL
                            + ", value=" + value.toNativeObject().toString());
                    storedConfiguration.writeSetting(PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL, profileID,
                            value, actor);
                    storedConfiguration.resetSetting(PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID, actor);
                }
            }

            /*
            {
            if (!storedConfiguration.isDefaultValue(PwmSetting.CHALLENGE_REQUIRE_RESPONSES)) {
                final StoredValue configValue = storedConfiguration.readSetting(PwmSetting.RECOVERY_VERIFICATION_METHODS, "default");
                final VerificationMethodValue.VerificationMethodSettings existingSettings = (VerificationMethodValue.VerificationMethodSettings)configValue.toNativeObject();
                final Map<RecoveryVerificationMethod,VerificationMethodValue.VerificationMethodSetting> newMethods = new HashMap<>();
                newMethods.putAll(existingSettings.getMethodSettings());
                VerificationMethodValue.VerificationMethodSetting setting = new VerificationMethodValue.VerificationMethodSetting(VerificationMethodValue.EnabledState.disabled);
                newMethods.put(RecoveryVerificationMethod.CHALLENGE_RESPONSES,setting);
                final VerificationMethodValue.VerificationMethodSettings newSettings = new VerificationMethodValue.VerificationMethodSettings(
                        newMethods,
                        existingSettings.getMinOptionalRequired()
                );
                storedConfiguration.writeSetting(PwmSetting.RECOVERY_VERIFICATION_METHODS, "default", new VerificationMethodValue(newSettings), actor);
            }
            }
                
            {
            if (!storedConfiguration.isDefaultValue(PwmSetting.FORGOTTEN_PASSWORD_REQUIRE_OTP)) {
                final StoredValue configValue = storedConfiguration.readSetting(PwmSetting.RECOVERY_VERIFICATION_METHODS, "default");
                final VerificationMethodValue.VerificationMethodSettings existingSettings = (VerificationMethodValue.VerificationMethodSettings)configValue.toNativeObject();
                final Map<RecoveryVerificationMethod,VerificationMethodValue.VerificationMethodSetting> newMethods = new HashMap<>();
                newMethods.putAll(existingSettings.getMethodSettings());
                VerificationMethodValue.VerificationMethodSetting setting = new VerificationMethodValue.VerificationMethodSetting(VerificationMethodValue.EnabledState.required);
                newMethods.put(RecoveryVerificationMethod.CHALLENGE_RESPONSES,setting);
                final VerificationMethodValue.VerificationMethodSettings newSettings = new VerificationMethodValue.VerificationMethodSettings(
                        newMethods,
                        existingSettings.getMinOptionalRequired()
                );
                storedConfiguration.writeSetting(PwmSetting.FORGOTTEN_PASSWORD_REQUIRE_OTP, "default", new VerificationMethodValue(newSettings), actor);
            }
            }
            */
        }
    }

    public static class ConfigRecordID implements Serializable {
        private RecordType recordType;
        private Object recordID;
        private String profileID;

        public enum RecordType {
            SETTING, LOCALE_BUNDLE,
        }

        public ConfigRecordID(RecordType recordType, Object recordID, String profileID) {
            this.recordType = recordType;
            this.recordID = recordID;
            this.profileID = profileID;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            ConfigRecordID that = (ConfigRecordID) o;

            if (profileID != null ? !profileID.equals(that.profileID) : that.profileID != null)
                return false;
            if (recordID != null ? !recordID.equals(that.recordID) : that.recordID != null)
                return false;
            if (recordType != that.recordType)
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = recordType != null ? recordType.hashCode() : 0;
            result = 31 * result + (recordID != null ? recordID.hashCode() : 0);
            result = 31 * result + (profileID != null ? profileID.hashCode() : 0);
            return result;
        }

        public RecordType getRecordType() {
            return recordType;
        }

        public Object getRecordID() {
            return recordID;
        }

        public String getProfileID() {
            return profileID;
        }
    }

    public String changeLogAsDebugString(final Locale locale, final boolean asHtml) {
        return changeLog.changeLogAsDebugString(locale, asHtml);
    }

    public String getKey() {
        return createTime() + StoredConfiguration.class.getSimpleName();
    }

    public boolean isModified() {
        return changeLog.isModified();
    }

    private class ChangeLog implements Serializable {
        /* values contain the _original_ toJson version of the value. */
        private Map<ConfigRecordID, String> changeLog = new LinkedHashMap<>();

        public boolean isModified() {
            return !changeLog.isEmpty();
        }

        public String changeLogAsDebugString(final Locale locale, boolean asHtml) {
            final Map<String, String> outputMap = new TreeMap<>();
            final String SEPARATOR = LocaleHelper.getLocalizedMessage(locale,
                    Config.Display_SettingNavigationSeparator, null);

            for (final ConfigRecordID configRecordID : changeLog.keySet()) {
                switch (configRecordID.recordType) {
                case SETTING: {
                    final StoredValue currentValue = readSetting((PwmSetting) configRecordID.recordID,
                            configRecordID.profileID);
                    final PwmSetting pwmSetting = (PwmSetting) configRecordID.recordID;
                    final String keyName = pwmSetting.toMenuLocationDebug(configRecordID.getProfileID(), locale);
                    final String debugValue = currentValue.toDebugString(asHtml, locale);
                    outputMap.put(keyName, debugValue);
                }
                    break;

                case LOCALE_BUNDLE: {
                    final String key = (String) configRecordID.recordID;
                    final String bundleName = key.split("!")[0];
                    final String keys = key.split("!")[1];
                    final Map<String, String> currentValue = readLocaleBundleMap(bundleName, keys);
                    final String debugValue = JsonUtil.serializeMap(currentValue, JsonUtil.Flag.PrettyPrint);
                    outputMap.put("LocaleBundle" + SEPARATOR + bundleName + " " + keys, debugValue);
                }
                    break;
                }
            }
            final StringBuilder output = new StringBuilder();
            if (outputMap.isEmpty()) {
                output.append("No setting changes.");
            } else {
                for (final String keyName : outputMap.keySet()) {
                    final String value = outputMap.get(keyName);
                    if (asHtml) {
                        output.append("<div class=\"changeLogKey\">");
                        output.append(keyName);
                        output.append("</div><div class=\"changeLogValue\">");
                        output.append(StringUtil.escapeHtml(value));
                        output.append("</div>");
                    } else {
                        output.append(keyName);
                        output.append("\n");
                        output.append(" Value: ");
                        output.append(value);
                        output.append("\n");
                    }
                }
            }
            return output.toString();
        }

        public void updateChangeLog(final String bundleName, final String keyName,
                final Map<String, String> localeValueMap) {
            final String key = bundleName + "!" + keyName;
            final Map<String, String> currentValue = readLocaleBundleMap(bundleName, keyName);
            final String currentJsonValue = JsonUtil.serializeMap(currentValue);
            final String newJsonValue = JsonUtil.serializeMap(localeValueMap);
            final ConfigRecordID configRecordID = new ConfigRecordID(ConfigRecordID.RecordType.LOCALE_BUNDLE, key,
                    null);
            updateChangeLog(configRecordID, currentJsonValue, newJsonValue);
        }

        public void updateChangeLog(final PwmSetting setting, final String profileID, final StoredValue newValue) {
            final StoredValue currentValue = readSetting(setting, profileID);
            final String currentJsonValue = JsonUtil.serialize(currentValue);
            final String newJsonValue = JsonUtil.serialize(newValue);
            final ConfigRecordID configRecordID = new ConfigRecordID(ConfigRecordID.RecordType.SETTING, setting,
                    profileID);
            updateChangeLog(configRecordID, currentJsonValue, newJsonValue);
        }

        public void updateChangeLog(final ConfigRecordID configRecordID, final String currentValueString,
                final String newValueString) {
            if (changeLog.containsKey(configRecordID)) {
                final String currentRecord = changeLog.get(configRecordID);

                if (currentRecord == null && newValueString == null) {
                    changeLog.remove(configRecordID);
                } else if (currentRecord != null && currentRecord.equals(newValueString)) {
                    changeLog.remove(configRecordID);
                }
            } else {
                changeLog.put(configRecordID, currentValueString);
            }
        }
    }

    public static void validateXmlSchema(final String xmlDocument) throws PwmUnrecoverableException {
        return;
        /*
        try {
        final InputStream xsdInputStream = PwmSetting.class.getClassLoader().getResourceAsStream("password/pwm/config/StoredConfiguration.xsd");
        final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        final Schema schema = factory.newSchema(new StreamSource(xsdInputStream));
        Validator validator = schema.newValidator();
        validator.validate(new StreamSource(new StringReader(xmlDocument)));
        } catch (Exception e) {
        final String errorMsg = "error while validating setting file schema definition: " + e.getMessage();
        throw new PwmUnrecoverableException(new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,errorMsg));
        }
        */
    }

    private static void updateMetaData(final Element settingElement, final UserIdentity userIdentity) {
        final Element settingsElement = settingElement.getDocument().getRootElement()
                .getChild(XML_ELEMENT_SETTINGS);
        settingElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME,
                PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
        settingsElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME,
                PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
        settingElement.removeAttribute(XML_ATTRIBUTE_MODIFY_USER);
        settingsElement.removeAttribute(XML_ATTRIBUTE_MODIFY_USER);
        if (userIdentity != null) {
            settingElement.setAttribute(XML_ATTRIBUTE_MODIFY_USER, userIdentity.toDelimitedKey());
            settingsElement.setAttribute(XML_ATTRIBUTE_MODIFY_USER, userIdentity.toDelimitedKey());
        }
    }

    private static Element createOrGetSettingElement(final Document document, final PwmSetting setting,
            final String profileID) {
        final XPathExpression xp = XPathBuilder.xpathForSetting(setting, profileID);
        final Element existingSettingElement = (Element) xp.evaluateFirst(document);
        if (existingSettingElement != null) {
            return existingSettingElement;
        }

        final Element settingElement = new Element(XML_ELEMENT_SETTING);
        settingElement.setAttribute(XML_ATTRIBUTE_KEY, setting.getKey());
        settingElement.setAttribute(XML_ATTRIBUTE_SYNTAX, setting.getSyntax().toString());
        if (profileID != null && profileID.length() > 0) {
            settingElement.setAttribute(XML_ATTRIBUTE_PROFILE, profileID);
        }

        Element settingsElement = document.getRootElement().getChild(XML_ELEMENT_SETTINGS);
        if (settingsElement == null) {
            settingsElement = new Element(XML_ELEMENT_SETTINGS);
            document.getRootElement().addContent(settingsElement);
        }
        settingsElement.addContent(settingElement);

        return settingElement;
    }

    static class SettingValueRecord implements Serializable {
        private PwmSetting setting;
        private String profile;
        private StoredValue storedValue;

        public SettingValueRecord(PwmSetting setting, String profile, StoredValue storedValue) {
            this.setting = setting;
            this.profile = profile;
            this.storedValue = storedValue;
        }

        public PwmSetting getSetting() {
            return setting;
        }

        public String getProfile() {
            return profile;
        }

        public StoredValue getStoredValue() {
            return storedValue;
        }
    }

    class StoredValueIterator implements Iterator<StoredConfiguration.SettingValueRecord> {

        private Queue<SettingValueRecord> settingQueue = new LinkedList<>();

        public StoredValueIterator(boolean includeDefaults) {
            for (final PwmSetting setting : PwmSetting.values()) {
                if (setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles()) {
                    if (includeDefaults || !isDefaultValue(setting)) {
                        SettingValueRecord settingValueRecord = new SettingValueRecord(setting, null, null);
                        settingQueue.add(settingValueRecord);
                    }
                }
            }

            for (final PwmSettingCategory category : PwmSettingCategory.values()) {
                if (category.hasProfiles()) {
                    for (final String profileID : profilesForSetting(category.getProfileSetting())) {
                        for (final PwmSetting setting : category.getSettings()) {
                            if (includeDefaults || !isDefaultValue(setting, profileID)) {
                                SettingValueRecord settingValueRecord = new SettingValueRecord(setting, profileID,
                                        null);
                                settingQueue.add(settingValueRecord);
                            }
                        }
                    }
                }
            }
        }

        @Override
        public boolean hasNext() {
            return !settingQueue.isEmpty();
        }

        @Override
        public SettingValueRecord next() {
            StoredConfiguration.SettingValueRecord settingValueRecord = settingQueue.poll();
            return new SettingValueRecord(settingValueRecord.getSetting(), settingValueRecord.getProfile(),
                    readSetting(settingValueRecord.getSetting(), settingValueRecord.getProfile()));
        }

        @Override
        public void remove() {

        }
    }

    private String createTime() {
        final Element rootElement = document.getRootElement();
        final String createTimeString = rootElement.getAttributeValue(XML_ATTRIBUTE_CREATE_TIME);
        if (createTimeString == null || createTimeString.isEmpty()) {
            throw new IllegalStateException("missing createTime timestamp");
        }
        return createTimeString;
    }

    public Date modifyTime() {
        final Element rootElement = document.getRootElement();
        final String modifyTimeString = rootElement.getAttributeValue(XML_ATTRIBUTE_MODIFY_TIME);
        if (modifyTimeString != null) {
            try {
                return PwmConstants.DEFAULT_DATETIME_FORMAT.parse(modifyTimeString);
            } catch (ParseException e) {
                LOGGER.error("error parsing root last modified timestamp: " + e.getMessage());
            }
        }
        return null;
    }

    public void initNewRandomSecurityKey() throws PwmUnrecoverableException {
        if (!isDefaultValue(PwmSetting.PWM_SECURITY_KEY)) {
            return;
        }

        writeSetting(PwmSetting.PWM_SECURITY_KEY,
                new PasswordValue(new PasswordData(PwmRandom.getInstance().alphaNumericString(1024))), null);

        LOGGER.debug("initialized new random security key");
    }

}