Forráskód Böngészése

implement zip config serializers

Jason Rivard 5 éve
szülő
commit
d043de7091
32 módosított fájl, 1608 hozzáadás és 990 törlés
  1. 2 260
      server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java
  2. 4 25
      server/src/main/java/password/pwm/config/stored/ConfigurationReader.java
  3. 20 0
      server/src/main/java/password/pwm/config/stored/StoredConfigItemKey.java
  4. 40 0
      server/src/main/java/password/pwm/config/stored/StoredConfigSerializer.java
  5. 0 50
      server/src/main/java/password/pwm/config/stored/StoredConfigXmlConstants.java
  6. 936 0
      server/src/main/java/password/pwm/config/stored/StoredConfigXmlSerializer.java
  7. 319 0
      server/src/main/java/password/pwm/config/stored/StoredConfigZipJsonSerializer.java
  8. 204 0
      server/src/main/java/password/pwm/config/stored/StoredConfigZipXmlSerializer.java
  9. 11 608
      server/src/main/java/password/pwm/config/stored/StoredConfigurationFactory.java
  10. 2 2
      server/src/main/java/password/pwm/config/value/AbstractValue.java
  11. 5 5
      server/src/main/java/password/pwm/config/value/ActionValue.java
  12. 2 2
      server/src/main/java/password/pwm/config/value/BooleanValue.java
  13. 2 2
      server/src/main/java/password/pwm/config/value/FileValue.java
  14. 3 3
      server/src/main/java/password/pwm/config/value/LocalizedStringValue.java
  15. 2 2
      server/src/main/java/password/pwm/config/value/NamedSecretValue.java
  16. 2 2
      server/src/main/java/password/pwm/config/value/NumericValue.java
  17. 2 2
      server/src/main/java/password/pwm/config/value/PasswordValue.java
  18. 4 4
      server/src/main/java/password/pwm/config/value/PrivateKeyValue.java
  19. 2 2
      server/src/main/java/password/pwm/config/value/RemoteWebServiceValue.java
  20. 2 2
      server/src/main/java/password/pwm/config/value/StringArrayValue.java
  21. 2 2
      server/src/main/java/password/pwm/config/value/StringValue.java
  22. 2 2
      server/src/main/java/password/pwm/config/value/UserPermissionValue.java
  23. 14 5
      server/src/main/java/password/pwm/config/value/VerificationMethodValue.java
  24. 2 2
      server/src/main/java/password/pwm/config/value/X509CertificateValue.java
  25. 1 1
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideUtils.java
  26. 1 1
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java
  27. 1 1
      server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  28. 1 1
      server/src/main/java/password/pwm/util/cli/commands/ConfigNewCommand.java
  29. 1 1
      server/src/main/java/password/pwm/util/cli/commands/ImportPropertyConfigCommand.java
  30. 16 0
      server/src/main/java/password/pwm/util/java/JavaHelper.java
  31. 2 2
      server/src/test/java/password/pwm/config/PwmSettingTest.java
  32. 1 1
      server/src/test/java/password/pwm/config/stored/ConfigurationCleanerTest.java

+ 2 - 260
server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java

@@ -29,20 +29,11 @@ import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.config.option.RecoveryMinLifetimeOption;
 import password.pwm.config.option.WebServiceUsage;
 import password.pwm.config.value.OptionListValue;
-import password.pwm.config.value.StoredValueEncoder;
-import password.pwm.config.value.StringArrayValue;
 import password.pwm.config.value.StringValue;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.PwmExceptionLoggingConsumer;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.java.XmlDocument;
-import password.pwm.util.java.XmlElement;
-import password.pwm.util.java.XmlFactory;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmSecurityKey;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -55,14 +46,7 @@ class ConfigurationCleaner
     private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigurationCleaner.class );
 
 
-    private static final List<PwmExceptionLoggingConsumer<XmlDocument>> XML_PRE_PROCESSORS = Collections.unmodifiableList( Arrays.asList(
-            new MigratePreValueXmlElements(),
-            new MigrateOldPropertyFormat(),
-            new AppPropertyOverrideMigration(),
-            new ProfileNonProfiledSettings(),
-            new MigrateDeprecatedProperties(),
-            new UpdatePropertiesWithoutType()
-    ) );
+
 
     private static final List<PwmExceptionLoggingConsumer<StoredConfigurationModifier>> STORED_CONFIG_POST_PROCESSORS = Collections.unmodifiableList( Arrays.asList(
             new UpdateDeprecatedAdComplexitySettings(),
@@ -71,13 +55,6 @@ class ConfigurationCleaner
     ) );
 
 
-    static void preProcessXml(
-            final XmlDocument document
-    )
-    {
-        XML_PRE_PROCESSORS.forEach( ( c ) -> PwmExceptionLoggingConsumer.wrapConsumer( c ).accept( document ) );
-    }
-
 
     static void postProcessStoredConfig(
             final StoredConfigurationModifier storedConfiguration
@@ -86,238 +63,7 @@ class ConfigurationCleaner
         STORED_CONFIG_POST_PROCESSORS.forEach( aClass -> PwmExceptionLoggingConsumer.wrapConsumer( aClass ).accept( storedConfiguration ) );
     }
 
-    private static class MigratePreValueXmlElements implements PwmExceptionLoggingConsumer<XmlDocument>
-    {
-        @Override
-        public void accept( final XmlDocument xmlDocument )
-        {
-            if ( readDocVersion( xmlDocument ) >= 4 )
-            {
-                return;
-            }
-
-            final List<XmlElement> settingElements = xmlDocument.evaluateXpathToElements( "//"
-                    + StoredConfigXmlConstants.XML_ELEMENT_SETTING );
-            for ( final XmlElement settingElement : settingElements )
-            {
-                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
-                final Optional<XmlElement> defaultElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT );
-                if ( valueElement.isPresent() && defaultElement.isPresent() )
-                {
-                    final String textValue = settingElement.getTextTrim();
-                    if ( !StringUtil.isEmpty( textValue ) )
-                    {
-                        final XmlElement newValueElement = XmlFactory.getFactory().newElement( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
-                        newValueElement.addText( textValue );
-                        settingElement.addContent( newValueElement );
-                        final String key = settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY );
-                        LOGGER.info( () -> "migrating pre-xml 'value' tag format to use value element for key: " + key );
-                    }
-                }
-            }
-        }
-    }
-
-    private static class MigrateOldPropertyFormat implements PwmExceptionLoggingConsumer<XmlDocument>
-    {
-        @Override
-        public void accept( final XmlDocument xmlDocument )
-        {
-            // read correct (new) //properties[@type="config"]
-            final String configPropertiesXpath = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
-                    + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + "=\"" + StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG + "\"]";
-            final Optional<XmlElement> configPropertiesElement = xmlDocument.evaluateXpathToElement( configPropertiesXpath );
-
-            // read list of old //properties[not (@type)]/property
-            final String nonAttributedPropertyXpath = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
-                    + "[not (@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + ")]/" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTY;
-            final List<XmlElement> nonAttributedProperties = xmlDocument.evaluateXpathToElements( nonAttributedPropertyXpath );
-
-            if ( configPropertiesElement.isPresent() && nonAttributedProperties != null )
-            {
-                for ( final XmlElement element : nonAttributedProperties )
-                {
-                    element.detach();
-                    configPropertiesElement.get().addContent( element );
-                }
-            }
-
-            // remove old //properties[not (@type] element
-            final String oldPropertiesXpath = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
-                    + "[not (@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + ")]";
-            final List<XmlElement> oldPropertiesElements = xmlDocument.evaluateXpathToElements( oldPropertiesXpath );
-            if ( oldPropertiesElements != null )
-            {
-                for ( final XmlElement element : oldPropertiesElements )
-                {
-                    element.detach();
-                }
-            }
-        }
-    }
-
-    static class ProfileNonProfiledSettings implements PwmExceptionLoggingConsumer<XmlDocument>
-    {
-        @Override
-        public void accept( final XmlDocument xmlDocument )
-        {
-            final StoredConfigurationFactory.XmlInputDocumentReader reader = new StoredConfigurationFactory.XmlInputDocumentReader( xmlDocument );
-            for ( final PwmSetting setting : PwmSetting.values() )
-            {
-                if ( setting.getCategory().hasProfiles() )
-                {
-                    reader.xpathForSetting( setting, null ).ifPresent( existingSettingElement ->
-                    {
-                        final List<String> profileStringDefinitions = new ArrayList<>();
-                        {
-                            final List<String> configuredProfiles = reader.profilesForSetting( setting );
-                            if ( !JavaHelper.isEmpty( configuredProfiles ) )
-                            {
-                                profileStringDefinitions.addAll( configuredProfiles );
-                            }
-                        }
-
-                        if ( profileStringDefinitions.isEmpty() )
-                        {
-                            profileStringDefinitions.add( PwmConstants.PROFILE_ID_DEFAULT );
-                        }
-
-                        for ( final String destProfile : profileStringDefinitions )
-                        {
-                            LOGGER.info( () -> "moving setting " + setting.getKey() + " without profile attribute to profile \"" + destProfile + "\"." );
-                            {
-                                //existingSettingElement.detach();
-                                final XmlElement newSettingElement = existingSettingElement.copy();
-                                newSettingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE, destProfile );
-
-                                final XmlElement settingsElement = reader.xpathForSettings();
-                                settingsElement.addContent( newSettingElement );
-                            }
-                        }
-                    } );
-                }
-            }
-        }
-    }
-
-    private static class MigrateDeprecatedProperties implements PwmExceptionLoggingConsumer<XmlDocument>
-    {
-        @Override
-        public void accept( final XmlDocument xmlDocument ) throws PwmUnrecoverableException
-        {
-            {
-                final String xpathString = "//property[@key=\"" + ConfigurationProperty.LDAP_TEMPLATE.getKey() + "\"]";
-                final List<XmlElement> propertyElement = xmlDocument.evaluateXpathToElements( xpathString );
-                if ( propertyElement != null && !propertyElement.isEmpty() )
-                {
-                    final String value = propertyElement.get( 0 ).getText();
-                    propertyElement.get( 0 ).detach();
-                    attachStringSettingElement( xmlDocument, PwmSetting.TEMPLATE_LDAP, value );
-
-                }
-            }
-            {
-                final String xpathString = "//property[@key=\"" + ConfigurationProperty.NOTES.getKey() + "\"]";
-                final List<XmlElement> propertyElement = xmlDocument.evaluateXpathToElements( xpathString );
-                if ( propertyElement != null && !propertyElement.isEmpty() )
-                {
-                    final String value = propertyElement.get( 0 ).getText();
-                    propertyElement.get( 0 ).detach();
-                    attachStringSettingElement( xmlDocument, PwmSetting.NOTES, value );
-                }
-            }
-        }
-
-        private static void attachStringSettingElement(
-                final XmlDocument xmlDocument,
-                final PwmSetting pwmSetting,
-                final String stringValue
-        )
-                throws PwmUnrecoverableException
-        {
-            final StoredConfigurationFactory.XmlInputDocumentReader inputDocumentReader = new StoredConfigurationFactory.XmlInputDocumentReader( xmlDocument );
-
-            final PwmSecurityKey pwmSecurityKey = inputDocumentReader.getKey();
-
-            final XmlElement settingElement = StoredConfigurationFactory.XmlOutputHandler.makeSettingXmlElement(
-                    null,
-                    pwmSetting,
-                    null,
-                    new StringValue( stringValue ),
-                    XmlOutputProcessData.builder().storedValueEncoderMode( StoredValueEncoder.Mode.PLAIN ).pwmSecurityKey( pwmSecurityKey ).build() );
-            final Optional<XmlElement> settingsElement = xmlDocument.getRootElement().getChild( StoredConfigXmlConstants.XML_ELEMENT_SETTING );
-            settingsElement.ifPresent( xmlElement -> xmlElement.addContent( settingElement ) );
-        }
-    }
-
-    private static class UpdatePropertiesWithoutType implements PwmExceptionLoggingConsumer<XmlDocument>
-    {
-        @Override
-        public void accept( final XmlDocument xmlDocument )
-        {
-            final String xpathString = "//properties[not(@type)]";
-            final List<XmlElement> propertiesElements = xmlDocument.evaluateXpathToElements( xpathString );
-            for ( final XmlElement propertiesElement : propertiesElements )
-            {
-                propertiesElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE, StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG );
-            }
-        }
-    }
-
-    private static class AppPropertyOverrideMigration implements PwmExceptionLoggingConsumer<XmlDocument>
-    {
-        @Override
-        public void accept( final XmlDocument xmlDocument ) throws PwmUnrecoverableException
-        {
-            final StoredConfigurationFactory.XmlInputDocumentReader documentReader = new StoredConfigurationFactory.XmlInputDocumentReader( xmlDocument );
-            final List<XmlElement> appPropertiesElements = documentReader.xpathForAppProperties();
-            for ( final XmlElement element : appPropertiesElements )
-            {
-                final List<XmlElement> properties = element.getChildren();
-                for ( final XmlElement 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;
-
-                        final List<String> existingValues = new ArrayList<>();
-                        {
-                            final Optional<StoredConfigData.ValueAndMetaCarrier> valueAndMetaTuple =  documentReader.readSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null );
-                            valueAndMetaTuple.ifPresent( ( t ) -> existingValues.addAll( ( List<String> ) t.getValue().toNativeObject() ) );
-                        }
-                        existingValues.add( newValue );
-                        rewriteAppPropertySettingElement( xmlDocument, existingValues );
-                    }
-                }
-                element.detach();
-            }
-        }
-
-        private static void rewriteAppPropertySettingElement( final XmlDocument xmlDocument, final List<String> newValues )
-                throws PwmUnrecoverableException
-        {
-            final StoredConfigurationFactory.XmlInputDocumentReader inputDocumentReader = new StoredConfigurationFactory.XmlInputDocumentReader( xmlDocument );
-
-            {
-                final Optional<XmlElement> existingAppPropertySetting = inputDocumentReader.xpathForSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null );
-                existingAppPropertySetting.ifPresent( XmlElement::detach );
-            }
 
-            final PwmSecurityKey pwmSecurityKey = inputDocumentReader.getKey();
-
-            final XmlElement settingElement = StoredConfigurationFactory.XmlOutputHandler.makeSettingXmlElement(
-                    null,
-                    PwmSetting.APP_PROPERTY_OVERRIDES,
-                    null,
-                    new StringArrayValue( newValues ),
-                    XmlOutputProcessData.builder().storedValueEncoderMode( StoredValueEncoder.Mode.PLAIN ).pwmSecurityKey( pwmSecurityKey ).build() );
-            final Optional<XmlElement> settingsElement = xmlDocument.getRootElement().getChild( StoredConfigXmlConstants.XML_ELEMENT_SETTING );
-            settingsElement.ifPresent( ( s ) -> s.addContent( settingElement ) );
-        }
-    }
 
     private static class UpdateDeprecatedAdComplexitySettings implements PwmExceptionLoggingConsumer<StoredConfigurationModifier>
     {
@@ -407,9 +153,5 @@ class ConfigurationCleaner
         }
     }
 
-    private static int readDocVersion( final XmlDocument xmlDocument )
-    {
-        final String xmlVersionStr = xmlDocument.getRootElement().getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_XML_VERSION );
-        return JavaHelper.silentParseInt( xmlVersionStr, 0 );
-    }
+
 }

+ 4 - 25
server/src/main/java/password/pwm/config/stored/ConfigurationReader.java

@@ -131,27 +131,6 @@ public class ConfigurationReader
 
         final Instant startTime = Instant.now();
 
-        /*
-        try
-        {
-            final InputStream theFileData = Files.newInputStream( configFile.toPath() );
-            final StoredConfiguration storedConfiguration = StoredConfigurationFactory.fromXml( theFileData );
-
-            System.out.println( TimeDuration.compactFromCurrent( startTime ) );
-
-
-            //final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            final FileOutputStream fos = new FileOutputStream( new File( "/tmp/NEWCFG" ) );
-            StoredConfigurationFactory.toXml( storedConfiguration, fos );
-
-            //System.out.println( new String( baos.toByteArray(), "UTF-8" )  );
-        }
-        catch ( final Exception e )
-        {
-            e.printStackTrace(  );
-        }
-        */
-
         final InputStream theFileData;
         try
         {
@@ -172,9 +151,9 @@ public class ConfigurationReader
         final StoredConfiguration storedConfiguration;
         try
         {
-            storedConfiguration = StoredConfigurationFactory.fromXml( theFileData );
+            storedConfiguration = StoredConfigurationFactory.input( theFileData );
         }
-        catch ( final PwmUnrecoverableException e )
+        catch ( final Exception e )
         {
             final String errorMsg = "unable to parse configuration file: " + e.getMessage();
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
@@ -328,7 +307,7 @@ public class ConfigurationReader
 
         try ( FileOutputStream fileOutputStream = new FileOutputStream( tempWriteFile, false ) )
         {
-            StoredConfigurationFactory.toXml( storedConfiguration, fileOutputStream );
+            StoredConfigurationFactory.output( storedConfiguration, fileOutputStream );
         }
 
         LOGGER.info( () -> "saved configuration in " + TimeDuration.compactFromCurrent( saveFileStartTime ) );
@@ -358,7 +337,7 @@ public class ConfigurationReader
             FileSystemUtility.rotateBackups( backupFile, backupRotations );
             try ( FileOutputStream fileOutputStream = new FileOutputStream( backupFile, false ) )
             {
-                StoredConfigurationFactory.toXml( storedConfiguration, fileOutputStream );
+                StoredConfigurationFactory.output( storedConfiguration, fileOutputStream );
             }
         }
     }

+ 20 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigItemKey.java

@@ -22,6 +22,7 @@ package password.pwm.config.stored;
 
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingSyntax;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.i18n.LocaleHelper;
@@ -231,4 +232,23 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
     {
         return getLabel( PwmConstants.DEFAULT_LOCALE ).compareTo( o.toString() );
     }
+
+    public PwmSettingSyntax getSyntax()
+    {
+        switch ( getRecordType() )
+        {
+            case SETTING:
+                return toPwmSetting().getSyntax();
+
+            case PROPERTY:
+                return PwmSettingSyntax.STRING;
+
+            case LOCALE_BUNDLE:
+                return PwmSettingSyntax.LOCALIZED_STRING_ARRAY;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( getRecordType() );
+                throw new IllegalStateException();
+        }
+    }
 }

+ 40 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigSerializer.java

@@ -0,0 +1,40 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+import password.pwm.error.PwmUnrecoverableException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public interface StoredConfigSerializer
+{
+    StoredConfiguration readInput( InputStream inputStream )
+            throws PwmUnrecoverableException, IOException;
+
+    void writeOutput(
+            StoredConfiguration storedConfiguration,
+            OutputStream outputStream,
+            StoredConfigurationFactory.OutputSettings outputSettings
+    )
+            throws PwmUnrecoverableException, IOException;
+}

+ 0 - 50
server/src/main/java/password/pwm/config/stored/StoredConfigXmlConstants.java

@@ -1,50 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2019 The PWM Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package password.pwm.config.stored;
-
-public class StoredConfigXmlConstants
-{
-    public static final String XML_ATTRIBUTE_TYPE = "type";
-    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_ELEMENT_LOCALEBUNDLE = "localeBundle";
-    public static final String XML_ELEMENT_LABEL = "label";
-    public static final String XML_ELEMENT_VALUE = "value";
-
-    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";
-    public static final String XML_ATTRIBUTE_BUNDLE = "bundle";
-    public static final String XML_ATTRIBUTE_XML_VERSION = "xmlVersion";
-    public static final String XML_ATTRRIBUTE_PWM_BUILD = "pwmBuild";
-    public static final String XML_ATTRIBUTE_PWM_VERSION = "pwmVersion";
-    public static final String XML_ATTRIBUTE_LOCALE = "locale";
-}

+ 936 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigXmlSerializer.java

@@ -0,0 +1,936 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+import password.pwm.PwmConstants;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingFlag;
+import password.pwm.config.PwmSettingSyntax;
+import password.pwm.config.PwmSettingTemplate;
+import password.pwm.config.PwmSettingTemplateSet;
+import password.pwm.config.StoredValue;
+import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.StoredValueEncoder;
+import password.pwm.config.value.StringArrayValue;
+import password.pwm.config.value.StringValue;
+import password.pwm.config.value.ValueFactory;
+import password.pwm.error.PwmException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.PwmLocaleBundle;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.LazySupplier;
+import password.pwm.util.java.PwmExceptionLoggingConsumer;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.XmlDocument;
+import password.pwm.util.java.XmlElement;
+import password.pwm.util.java.XmlFactory;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.secure.PwmSecurityKey;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ResourceBundle;
+import java.util.Set;
+
+public class StoredConfigXmlSerializer implements StoredConfigSerializer
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigXmlSerializer.class );
+
+    private static final String XML_FORMAT_VERSION = "5";
+
+    @Override
+    public StoredConfiguration readInput( final InputStream inputStream )
+            throws PwmUnrecoverableException
+    {
+        final XmlFactory xmlFactory = XmlFactory.getFactory();
+        final XmlDocument xmlDocument = xmlFactory.parseXml( inputStream );
+        XmlCleaner.preProcessXml( xmlDocument );
+
+        final XmlInputDocumentReader xmlInputDocumentReader = new XmlInputDocumentReader( xmlDocument );
+        final StoredConfigData storedConfigData = xmlInputDocumentReader.getStoredConfigData();
+        return new StoredConfigurationImpl( storedConfigData );
+    }
+
+    @Override
+    public void writeOutput(
+            final StoredConfiguration storedConfiguration,
+            final OutputStream outputStream,
+            final StoredConfigurationFactory.OutputSettings outputSettings
+    )
+            throws PwmUnrecoverableException, IOException
+    {
+        final XmlFactory xmlFactory = XmlFactory.getFactory();
+        final XmlDocument xmlDocument = xmlFactory.newDocument( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_ROOT );
+
+        XmlOutputHandler.makeXmlOutput( storedConfiguration, xmlDocument.getRootElement(), outputSettings );
+
+        xmlFactory.outputDocument( xmlDocument, outputStream );
+    }
+
+    static class XmlInputDocumentReader
+    {
+        private final XmlDocument document;
+
+        XmlInputDocumentReader( final XmlDocument document )
+        {
+            this.document = document;
+        }
+
+        StoredConfigData getStoredConfigData()
+        {
+            final String createTime = readCreateTime();
+            final Instant modifyTime = readModifyTime();
+
+            final List<StoredConfigData.ValueAndMetaCarrier> values = new ArrayList<>();
+            values.addAll( readProperties() );
+            values.addAll( readSettings() );
+            values.addAll( readLocaleBundles() );
+            return StoredConfigData.builder()
+                    .createTime( createTime )
+                    .modifyTime( modifyTime )
+                    .metaDatas( StoredConfigData.carrierAsMetaDataMap( values ) )
+                    .storedValues( StoredConfigData.carrierAsStoredValueMap( values ) )
+                    .build();
+        }
+
+        private Collection<StoredConfigData.ValueAndMetaCarrier> readProperties()
+        {
+            final List<StoredConfigData.ValueAndMetaCarrier> valueAndMetaWrapper = new ArrayList<>();
+            for ( final ConfigurationProperty configurationProperty : ConfigurationProperty.values() )
+            {
+                final Optional<XmlElement> propertyElement = xpathForConfigProperty( configurationProperty );
+                if ( propertyElement.isPresent() && !StringUtil.isEmpty( propertyElement.get().getText() ) )
+                {
+                    final StoredConfigItemKey key = StoredConfigItemKey.fromConfigurationProperty( configurationProperty );
+                    final StoredValue storedValue = new StringValue( propertyElement.get().getText() );
+                    final ValueMetaData metaData = readMetaDataFromXmlElement( key, propertyElement.get() ).orElse( null );
+                    valueAndMetaWrapper.add( new StoredConfigData.ValueAndMetaCarrier( key, storedValue, metaData ) );
+                }
+            }
+            return valueAndMetaWrapper;
+        }
+
+        private Collection<StoredConfigData.ValueAndMetaCarrier> readSettings()
+        {
+            final List<StoredConfigData.ValueAndMetaCarrier> returnList = new ArrayList<>();
+            for ( final PwmSetting pwmSetting : PwmSetting.values() )
+            {
+                if ( !pwmSetting.getCategory().hasProfiles() )
+                {
+                    readSetting( pwmSetting, null ).ifPresent( returnList::add );
+                }
+            }
+
+            for ( final PwmSetting pwmSetting : PwmSetting.values() )
+            {
+                if ( pwmSetting.getCategory().hasProfiles() )
+                {
+                    final List<String> profileIDs = profilesForSetting( pwmSetting );
+                    for ( final String profileID : profileIDs )
+                    {
+                        readSetting( pwmSetting, profileID ).ifPresent( returnList::add );
+                    }
+                }
+            }
+            return returnList;
+        }
+
+        Optional<StoredConfigData.ValueAndMetaCarrier> readSetting( final PwmSetting setting, final String profileID )
+        {
+            final Optional<XmlElement> settingElement = xpathForSetting( setting, profileID );
+
+            if ( !settingElement.isPresent() )
+            {
+                return Optional.empty();
+            }
+
+            if ( settingElement.get().getChild( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT ).isPresent() )
+            {
+                return Optional.empty();
+            }
+
+            try
+            {
+                final StoredValue storedValue = ValueFactory.fromXmlValues( setting, settingElement.get(), getKey() );
+                final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+                final ValueMetaData metaData = readMetaDataFromXmlElement( key, settingElement.get() ).orElse( null );
+                return Optional.of( new StoredConfigData.ValueAndMetaCarrier( key, storedValue, metaData ) );
+            }
+            catch ( final PwmException e )
+            {
+                final String errorMsg = "unexpected error reading setting '" + setting.getKey() + "' profile '" + profileID + "', error: " + e.getMessage();
+                throw new IllegalStateException( errorMsg );
+            }
+        }
+
+        List<String> profilesForSetting( final PwmSetting pwmSetting )
+        {
+            if ( !pwmSetting.getCategory().hasProfiles() && pwmSetting.getSyntax() != PwmSettingSyntax.PROFILE )
+            {
+                return Collections.emptyList();
+            }
+
+            final PwmSetting profileSetting;
+            if ( pwmSetting.getSyntax() == PwmSettingSyntax.PROFILE )
+            {
+                profileSetting = pwmSetting;
+            }
+            else
+            {
+                profileSetting = pwmSetting.getCategory().getProfileSetting().orElseThrow( IllegalStateException::new );
+            }
+
+            final StoredValue effectiveValue;
+            {
+                final Optional<StoredConfigData.ValueAndMetaCarrier> configuredValue = readSetting( profileSetting, null );
+                if ( configuredValue.isPresent() )
+                {
+                    effectiveValue = configuredValue.get().getValue();
+                }
+                else
+                {
+                    effectiveValue = profileSetting.getDefaultValue( templateSetSupplier.get() );
+                }
+            }
+
+            final List<String> settingValues = ( List<String> ) effectiveValue.toNativeObject();
+            final LinkedList<String> profiles = new LinkedList<>( settingValues );
+            profiles.removeIf( StringUtil::isEmpty );
+            return Collections.unmodifiableList( profiles );
+        }
+
+
+        public PwmSecurityKey getKey() throws PwmUnrecoverableException
+        {
+            final XmlElement rootElement = document.getRootElement();
+            final String createTimeString = rootElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_CREATE_TIME );
+            return new PwmSecurityKey( createTimeString + "StoredConfiguration" );
+        }
+
+        String readCreateTime()
+        {
+            final XmlElement rootElement = document.getRootElement();
+            final String createTimeString = rootElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_CREATE_TIME );
+            if ( StringUtil.isEmpty( createTimeString ) )
+            {
+                throw new IllegalStateException( "missing createTime timestamp" );
+            }
+            else
+            {
+                return createTimeString;
+            }
+        }
+
+        Instant readModifyTime()
+        {
+            final XmlElement rootElement = document.getRootElement();
+            final String modifyTimeString = rootElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME );
+            if ( !StringUtil.isEmpty( modifyTimeString ) )
+            {
+                try
+                {
+                    return JavaHelper.parseIsoToInstant( modifyTimeString );
+                }
+                catch ( final Exception e )
+                {
+                    LOGGER.error( () -> "error parsing root last modified timestamp: " + e.getMessage() );
+                }
+            }
+
+            return null;
+        }
+
+        private final LazySupplier<PwmSettingTemplateSet> templateSetSupplier = new LazySupplier<>( () ->
+        {
+            final Set<PwmSettingTemplate> templates = new HashSet<>();
+            templates.add( readTemplateValue( PwmSetting.TEMPLATE_LDAP ) );
+            templates.add( readTemplateValue( PwmSetting.TEMPLATE_STORAGE ) );
+            templates.add( readTemplateValue( PwmSetting.DB_VENDOR_TEMPLATE ) );
+            return new PwmSettingTemplateSet( templates );
+        } );
+
+        private PwmSettingTemplate readTemplateValue( final PwmSetting pwmSetting )
+        {
+            final Optional<XmlElement> settingElement = xpathForSetting( pwmSetting, null );
+            if ( settingElement.isPresent() )
+            {
+                try
+                {
+                    final String strValue = ( String ) ValueFactory.fromXmlValues( pwmSetting, settingElement.get(), null ).toNativeObject();
+                    return JavaHelper.readEnumFromString( PwmSettingTemplate.class, null, strValue );
+                }
+                catch ( final IllegalStateException e )
+                {
+                    LOGGER.error( () -> "error reading template", e );
+                }
+            }
+            return null;
+        }
+
+        private Collection<StoredConfigData.ValueAndMetaCarrier> readLocaleBundles()
+        {
+            final List<StoredConfigData.ValueAndMetaCarrier> returnWrapper = new ArrayList<>();
+
+            for ( final XmlElement localeBundleElement : xpathForLocaleBundles() )
+            {
+                final String bundleName = localeBundleElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_BUNDLE );
+                final Optional<PwmLocaleBundle> pwmLocaleBundle = PwmLocaleBundle.forKey( bundleName );
+                pwmLocaleBundle.ifPresent( ( bundle ) ->
+                {
+                    final String key = localeBundleElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY );
+                    if ( bundle.getDisplayKeys().contains( key ) )
+                    {
+                        final Map<String, String> bundleMap = new LinkedHashMap<>();
+                        for ( final XmlElement valueElement : localeBundleElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE ) )
+                        {
+                            final String localeStrValue = valueElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE );
+                            bundleMap.put( localeStrValue == null ? "" : localeStrValue, valueElement.getText() );
+                        }
+                        if ( !bundleMap.isEmpty() )
+                        {
+                            final StoredConfigItemKey storedConfigItemKey = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle.get(), key );
+                            final StoredValue storedValue = new LocalizedStringValue( bundleMap );
+                            final ValueMetaData metaData = readMetaDataFromXmlElement( storedConfigItemKey, localeBundleElement ).orElse( null );
+                            returnWrapper.add( new StoredConfigData.ValueAndMetaCarrier( storedConfigItemKey, storedValue, metaData ) );
+                        }
+                    }
+                } );
+            }
+            return Collections.unmodifiableList( returnWrapper );
+        }
+
+        private Optional<ValueMetaData> readMetaDataFromXmlElement( final StoredConfigItemKey key, final XmlElement xmlElement )
+        {
+            Instant instant = null;
+            {
+                final String modifyTimeValue = xmlElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME );
+                if ( !StringUtil.isEmpty( modifyTimeValue ) )
+                {
+                    try
+                    {
+                        instant = JavaHelper.parseIsoToInstant( modifyTimeValue );
+                    }
+                    catch ( final DateTimeParseException e )
+                    {
+                        e.printStackTrace();
+                    }
+                }
+            }
+
+            UserIdentity userIdentity = null;
+            {
+                final String modifyUserValue = xmlElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_USER );
+                if ( !StringUtil.isEmpty( modifyUserValue ) )
+                {
+                    try
+                    {
+                        userIdentity = UserIdentity.fromDelimitedKey( modifyUserValue );
+                    }
+                    catch ( final DateTimeParseException | PwmUnrecoverableException e )
+                    {
+                        LOGGER.trace( () -> "unable to parse userIdentity metadata for key " + key.toString() );
+                    }
+                }
+            }
+
+            if ( instant != null || userIdentity != null )
+            {
+                return Optional.of( new ValueMetaData( instant, userIdentity ) );
+            }
+
+            return Optional.empty();
+        }
+
+        List<XmlElement> xpathForLocaleBundles()
+        {
+            final String xpathString = "//localeBundle";
+            return document.evaluateXpathToElements( xpathString );
+        }
+
+        XmlElement xpathForSettings()
+        {
+            return document.getRootElement().getChild( StoredConfigXmlConstants.XML_ELEMENT_SETTINGS )
+                    .orElseThrow( () -> new IllegalStateException( "configuration xml document missing 'settings' element" ) );
+        }
+
+        Optional<XmlElement> xpathForSetting( final PwmSetting setting, final String profileID )
+        {
+            final String xpathString;
+            if ( profileID == null || profileID.length() < 1 )
+            {
+                xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_SETTING + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_KEY
+                        + "=\"" + setting.getKey()
+                        + "\"][(not (@" + StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE + ")) or @"
+                        + StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE + "=\"\"]";
+            }
+            else
+            {
+                xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_SETTING + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_KEY
+                        + "=\"" + setting.getKey()
+                        + "\"][@" + StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE + "=\"" + profileID + "\"]";
+            }
+
+            return document.evaluateXpathToElement( xpathString );
+        }
+
+        Optional<XmlElement> xpathForConfigProperty( final ConfigurationProperty configProperty )
+        {
+            final String xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE
+                    + "=\"" + StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG + "\"]/"
+                    + StoredConfigXmlConstants.XML_ELEMENT_PROPERTY + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_KEY + "=\"" + configProperty.getKey() + "\"]";
+            return document.evaluateXpathToElement( xpathString );
+        }
+
+        List<XmlElement> xpathForAppProperties( )
+        {
+            final String xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
+                    + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + "=\"" + StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_APP + "\"]";
+            return document.evaluateXpathToElements( xpathString );
+        }
+    }
+
+    static class XmlOutputHandler
+    {
+        static void makeXmlOutput( final StoredConfiguration storedConfiguration, final XmlElement rootElement, final StoredConfigurationFactory.OutputSettings outputSettings )
+                throws PwmUnrecoverableException
+        {
+            decorateRootElement( rootElement, storedConfiguration );
+
+            rootElement.addContent( makePropertiesElement( storedConfiguration ) );
+
+            rootElement.addContent( makeSettingsXmlElement( storedConfiguration, outputSettings ) );
+
+            rootElement.addContent( XmlOutputHandler.makeLocaleBundleXmlElements( storedConfiguration ) );
+        }
+
+        static void decorateRootElement( final XmlElement rootElement, final StoredConfiguration storedConfiguration )
+        {
+            rootElement.setComment( Collections.singletonList( generateCommentText() ) );
+            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_PWM_VERSION, PwmConstants.BUILD_VERSION );
+            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRRIBUTE_PWM_BUILD, PwmConstants.BUILD_NUMBER );
+            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_XML_VERSION, XML_FORMAT_VERSION );
+
+            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_CREATE_TIME, storedConfiguration.createTime() );
+            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( storedConfiguration.modifyTime() ) );
+        }
+
+        static XmlElement makeSettingsXmlElement(
+                final StoredConfiguration storedConfiguration,
+                final StoredConfigurationFactory.OutputSettings outputSettings
+        )
+                throws PwmUnrecoverableException
+        {
+            final PwmSecurityKey pwmSecurityKey = storedConfiguration.getKey();
+
+            final XmlFactory xmlFactory = XmlFactory.getFactory();
+            final XmlElement settingsElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_SETTINGS );
+
+            final XmlOutputProcessData xmlOutputProcessData = XmlOutputProcessData.builder()
+                    .pwmSecurityKey( pwmSecurityKey )
+                    .storedValueEncoderMode( figureEncoderMode( storedConfiguration, outputSettings ) )
+                    .build();
+
+            for ( final PwmSetting pwmSetting : PwmSetting.sortedByMenuLocation( PwmConstants.DEFAULT_LOCALE ) )
+            {
+                if ( !pwmSetting.getFlags().contains( PwmSettingFlag.Deprecated ) )
+                {
+                    if ( pwmSetting.getCategory().hasProfiles() )
+                    {
+                        for ( final String profileID : storedConfiguration.profilesForSetting( pwmSetting ) )
+                        {
+                            final StoredValue storedValue = storedConfiguration.readSetting( pwmSetting, profileID );
+                            final XmlElement settingElement = makeSettingXmlElement( storedConfiguration, pwmSetting, profileID, storedValue, xmlOutputProcessData );
+                            decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromSetting( pwmSetting, profileID ), settingElement );
+                            settingsElement.addContent( settingElement );
+                        }
+                    }
+                    else
+                    {
+                        final StoredValue storedValue = storedConfiguration.readSetting( pwmSetting, null );
+                        final XmlElement settingElement = makeSettingXmlElement( storedConfiguration, pwmSetting, null, storedValue, xmlOutputProcessData );
+                        decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromSetting( pwmSetting, null ), settingElement );
+                        settingsElement.addContent( settingElement );
+                    }
+                }
+            }
+
+            return settingsElement;
+        }
+
+        static StoredValueEncoder.Mode figureEncoderMode(
+                final StoredConfiguration storedConfiguration,
+                final StoredConfigurationFactory.OutputSettings outputSettings
+        )
+        {
+            if ( outputSettings == null || outputSettings.getMode() == null )
+            {
+                return StoredValueEncoder.Mode.ENCODED;
+            }
+
+            if ( outputSettings.getMode() == StoredConfigurationFactory.OutputSettings.SecureOutputMode.STRIPPED )
+            {
+                return StoredValueEncoder.Mode.STRIPPED;
+            }
+
+            final Optional<String> strValue = storedConfiguration.readConfigProperty( ConfigurationProperty.STORE_PLAINTEXT_VALUES );
+            if ( strValue.isPresent() && Boolean.parseBoolean( strValue.get() ) )
+            {
+                return StoredValueEncoder.Mode.PLAIN;
+            }
+
+            return StoredValueEncoder.Mode.ENCODED;
+        }
+
+
+        static XmlElement makeSettingXmlElement(
+                final StoredConfiguration storedConfiguration,
+                final PwmSetting pwmSetting,
+                final String profileID,
+                final StoredValue storedValue,
+                final XmlOutputProcessData xmlOutputProcessData
+        )
+        {
+            final XmlFactory xmlFactory = XmlFactory.getFactory();
+
+            final XmlElement settingElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_SETTING );
+            settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, pwmSetting.getKey() );
+
+            if ( !StringUtil.isEmpty( profileID ) )
+            {
+                settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE, profileID );
+            }
+
+            settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX, pwmSetting.getSyntax().name() );
+
+            {
+                final XmlElement labelElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_LABEL );
+                labelElement.addText( pwmSetting.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) );
+                settingElement.addContent( labelElement );
+            }
+
+            final List<XmlElement> valueElements = new ArrayList<>(  );
+            if ( storedConfiguration != null && storedConfiguration.isDefaultValue( pwmSetting, profileID ) )
+            {
+                final XmlElement defaultValue = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT );
+                valueElements.add( defaultValue );
+            }
+            else
+            {
+                valueElements.addAll( storedValue.toXmlValues( StoredConfigXmlConstants.XML_ELEMENT_VALUE, xmlOutputProcessData ) );
+            }
+
+            settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION, String.valueOf( storedValue.currentSyntaxVersion() ) );
+            settingElement.addContent( valueElements );
+            return settingElement;
+        }
+
+        private static void decorateElementWithMetaData(
+                final StoredConfiguration storedConfiguration,
+                final StoredConfigItemKey key,
+                final XmlElement xmlElement
+        )
+        {
+            final Optional<ValueMetaData> valueMetaData = ( ( StoredConfiguration ) storedConfiguration ).readMetaData( key );
+
+            if ( valueMetaData.isPresent() )
+            {
+                if ( valueMetaData.get().getUserIdentity() != null )
+                {
+                    xmlElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_USER, valueMetaData.get().getUserIdentity().toDelimitedKey() );
+                }
+
+                if ( valueMetaData.get().getModifyDate() != null )
+                {
+                    xmlElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( valueMetaData.get().getModifyDate() ) );
+                }
+            }
+        }
+
+        private static XmlElement makePropertiesElement( final StoredConfiguration storedConfiguration )
+        {
+            final XmlFactory xmlFactory = XmlFactory.getFactory();
+            final XmlElement propertiesElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES );
+            propertiesElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE, StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG );
+
+            for ( final ConfigurationProperty configurationProperty : ConfigurationProperty.values() )
+            {
+                storedConfiguration.readConfigProperty( configurationProperty ).ifPresent( s ->
+                        {
+                            final XmlElement propertyElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_PROPERTY );
+                            propertyElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, configurationProperty.getKey() );
+                            propertyElement.addText( s );
+                            decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromConfigurationProperty( configurationProperty ), propertyElement );
+                            propertiesElement.addContent( propertyElement );
+                        }
+                );
+            }
+
+            return propertiesElement;
+        }
+
+        private static List<XmlElement> makeLocaleBundleXmlElements( final StoredConfiguration storedConfiguration )
+        {
+            final XmlFactory xmlFactory = XmlFactory.getFactory();
+            final List<XmlElement> returnList = new ArrayList<>();
+            for ( final PwmLocaleBundle pwmLocaleBundle : PwmLocaleBundle.values() )
+            {
+                for ( final String key : pwmLocaleBundle.getDisplayKeys() )
+                {
+                    final Map<String, String> localeBundle = storedConfiguration.readLocaleBundleMap( pwmLocaleBundle, key );
+                    if ( !JavaHelper.isEmpty( localeBundle ) )
+                    {
+                        final XmlElement localeBundleElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_LOCALEBUNDLE );
+                        localeBundleElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_BUNDLE, pwmLocaleBundle.getKey() );
+                        localeBundleElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, key );
+
+                        final Map<String, String> localeBundleMap = storedConfiguration.readLocaleBundleMap( pwmLocaleBundle, key );
+                        for ( final Map.Entry<String, String> entry : localeBundleMap.entrySet() )
+                        {
+                            final XmlElement valueElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                            if ( !StringUtil.isEmpty( entry.getKey() ) )
+                            {
+                                valueElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE, entry.getKey() );
+                            }
+                            valueElement.addText( entry.getValue() );
+                            localeBundleElement.addContent( valueElement );
+                        }
+
+                        decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, key ), localeBundleElement );
+                        returnList.add( localeBundleElement );
+                    }
+                }
+            }
+            return Collections.unmodifiableList( returnList );
+        }
+
+        private static String generateCommentText()
+        {
+            final String resourceText = ResourceBundle.getBundle( StoredConfigurationFactory.class.getName() ).getString( "configCommentText" );
+            return MacroMachine.forStatic().expandMacros( resourceText );
+        }
+    }
+
+    public static class StoredConfigXmlConstants
+    {
+        public static final String XML_ATTRIBUTE_TYPE = "type";
+        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_ELEMENT_LOCALEBUNDLE = "localeBundle";
+        public static final String XML_ELEMENT_LABEL = "label";
+        public static final String XML_ELEMENT_VALUE = "value";
+
+        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";
+        public static final String XML_ATTRIBUTE_BUNDLE = "bundle";
+        public static final String XML_ATTRIBUTE_XML_VERSION = "xmlVersion";
+        public static final String XML_ATTRRIBUTE_PWM_BUILD = "pwmBuild";
+        public static final String XML_ATTRIBUTE_PWM_VERSION = "pwmVersion";
+        public static final String XML_ATTRIBUTE_LOCALE = "locale";
+    }
+
+    static class XmlCleaner
+    {
+        private static final List<PwmExceptionLoggingConsumer<XmlDocument>> XML_PRE_PROCESSORS = Collections.unmodifiableList( Arrays.asList(
+                new MigratePreValueXmlElements(),
+                new MigrateOldPropertyFormat(),
+                new AppPropertyOverrideMigration(),
+                new ProfileNonProfiledSettings(),
+                new MigrateDeprecatedProperties(),
+                new UpdatePropertiesWithoutType()
+        ) );
+
+        static void preProcessXml(
+                final XmlDocument document
+        )
+        {
+            XML_PRE_PROCESSORS.forEach( ( c ) -> PwmExceptionLoggingConsumer.wrapConsumer( c ).accept( document ) );
+        }
+
+        private static class MigratePreValueXmlElements implements PwmExceptionLoggingConsumer<XmlDocument>
+        {
+            @Override
+            public void accept( final XmlDocument xmlDocument )
+            {
+                if ( readDocVersion( xmlDocument ) >= 4 )
+                {
+                    return;
+                }
+
+                final List<XmlElement> settingElements = xmlDocument.evaluateXpathToElements( "//"
+                        + StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_SETTING );
+                for ( final XmlElement settingElement : settingElements )
+                {
+                    final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                    final Optional<XmlElement> defaultElement = settingElement.getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_DEFAULT );
+                    if ( valueElement.isPresent() && defaultElement.isPresent() )
+                    {
+                        final String textValue = settingElement.getTextTrim();
+                        if ( !StringUtil.isEmpty( textValue ) )
+                        {
+                            final XmlElement newValueElement = XmlFactory.getFactory().newElement( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                            newValueElement.addText( textValue );
+                            settingElement.addContent( newValueElement );
+                            final String key = settingElement.getAttributeValue( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_KEY );
+                            LOGGER.info( () -> "migrating pre-xml 'value' tag format to use value element for key: " + key );
+                        }
+                    }
+                }
+            }
+        }
+
+        private static class MigrateOldPropertyFormat implements PwmExceptionLoggingConsumer<XmlDocument>
+        {
+            @Override
+            public void accept( final XmlDocument xmlDocument )
+            {
+                // read correct (new) //properties[@type="config"]
+                final String configPropertiesXpath = "//" + StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
+                        + "[@" + StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + "=\""
+                        + StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG + "\"]";
+                final Optional<XmlElement> configPropertiesElement = xmlDocument.evaluateXpathToElement( configPropertiesXpath );
+
+                // read list of old //properties[not (@type)]/property
+                final String nonAttributedPropertyXpath = "//" + StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
+                        + "[not (@" + StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + ")]/"
+                        + StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_PROPERTY;
+                final List<XmlElement> nonAttributedProperties = xmlDocument.evaluateXpathToElements( nonAttributedPropertyXpath );
+
+                if ( configPropertiesElement.isPresent() && nonAttributedProperties != null )
+                {
+                    for ( final XmlElement element : nonAttributedProperties )
+                    {
+                        element.detach();
+                        configPropertiesElement.get().addContent( element );
+                    }
+                }
+
+                // remove old //properties[not (@type] element
+                final String oldPropertiesXpath = "//" + StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
+                        + "[not (@" + StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + ")]";
+                final List<XmlElement> oldPropertiesElements = xmlDocument.evaluateXpathToElements( oldPropertiesXpath );
+                if ( oldPropertiesElements != null )
+                {
+                    for ( final XmlElement element : oldPropertiesElements )
+                    {
+                        element.detach();
+                    }
+                }
+            }
+        }
+
+        static class ProfileNonProfiledSettings implements PwmExceptionLoggingConsumer<XmlDocument>
+        {
+            @Override
+            public void accept( final XmlDocument xmlDocument )
+            {
+                final StoredConfigXmlSerializer.XmlInputDocumentReader reader = new StoredConfigXmlSerializer.XmlInputDocumentReader( xmlDocument );
+                for ( final PwmSetting setting : PwmSetting.values() )
+                {
+                    if ( setting.getCategory().hasProfiles() )
+                    {
+                        reader.xpathForSetting( setting, null ).ifPresent( existingSettingElement ->
+                        {
+                            final List<String> profileStringDefinitions = new ArrayList<>();
+                            {
+                                final List<String> configuredProfiles = reader.profilesForSetting( setting );
+                                if ( !JavaHelper.isEmpty( configuredProfiles ) )
+                                {
+                                    profileStringDefinitions.addAll( configuredProfiles );
+                                }
+                            }
+
+                            if ( profileStringDefinitions.isEmpty() )
+                            {
+                                profileStringDefinitions.add( PwmConstants.PROFILE_ID_DEFAULT );
+                            }
+
+                            for ( final String destProfile : profileStringDefinitions )
+                            {
+                                LOGGER.info( () -> "moving setting " + setting.getKey() + " without profile attribute to profile \"" + destProfile + "\"." );
+                                {
+                                    //existingSettingElement.detach();
+                                    final XmlElement newSettingElement = existingSettingElement.copy();
+                                    newSettingElement.setAttribute( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE, destProfile );
+
+                                    final XmlElement settingsElement = reader.xpathForSettings();
+                                    settingsElement.addContent( newSettingElement );
+                                }
+                            }
+                        } );
+                    }
+                }
+            }
+        }
+
+        private static class MigrateDeprecatedProperties implements PwmExceptionLoggingConsumer<XmlDocument>
+        {
+            @Override
+            public void accept( final XmlDocument xmlDocument ) throws PwmUnrecoverableException
+            {
+                {
+                    final String xpathString = "//property[@key=\"" + ConfigurationProperty.LDAP_TEMPLATE.getKey() + "\"]";
+                    final List<XmlElement> propertyElement = xmlDocument.evaluateXpathToElements( xpathString );
+                    if ( propertyElement != null && !propertyElement.isEmpty() )
+                    {
+                        final String value = propertyElement.get( 0 ).getText();
+                        propertyElement.get( 0 ).detach();
+                        attachStringSettingElement( xmlDocument, PwmSetting.TEMPLATE_LDAP, value );
+
+                    }
+                }
+                {
+                    final String xpathString = "//property[@key=\"" + ConfigurationProperty.NOTES.getKey() + "\"]";
+                    final List<XmlElement> propertyElement = xmlDocument.evaluateXpathToElements( xpathString );
+                    if ( propertyElement != null && !propertyElement.isEmpty() )
+                    {
+                        final String value = propertyElement.get( 0 ).getText();
+                        propertyElement.get( 0 ).detach();
+                        attachStringSettingElement( xmlDocument, PwmSetting.NOTES, value );
+                    }
+                }
+            }
+
+            private static void attachStringSettingElement(
+                    final XmlDocument xmlDocument,
+                    final PwmSetting pwmSetting,
+                    final String stringValue
+            )
+                    throws PwmUnrecoverableException
+            {
+                final StoredConfigXmlSerializer.XmlInputDocumentReader inputDocumentReader = new StoredConfigXmlSerializer.XmlInputDocumentReader( xmlDocument );
+
+                final PwmSecurityKey pwmSecurityKey = inputDocumentReader.getKey();
+
+                final XmlElement settingElement = StoredConfigXmlSerializer.XmlOutputHandler.makeSettingXmlElement(
+                        null,
+                        pwmSetting,
+                        null,
+                        new StringValue( stringValue ),
+                        XmlOutputProcessData.builder().storedValueEncoderMode( StoredValueEncoder.Mode.PLAIN ).pwmSecurityKey( pwmSecurityKey ).build() );
+                final Optional<XmlElement> settingsElement = xmlDocument.getRootElement().getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_SETTING );
+                settingsElement.ifPresent( xmlElement -> xmlElement.addContent( settingElement ) );
+            }
+        }
+
+        private static class UpdatePropertiesWithoutType implements PwmExceptionLoggingConsumer<XmlDocument>
+        {
+            @Override
+            public void accept( final XmlDocument xmlDocument )
+            {
+                final String xpathString = "//properties[not(@type)]";
+                final List<XmlElement> propertiesElements = xmlDocument.evaluateXpathToElements( xpathString );
+                for ( final XmlElement propertiesElement : propertiesElements )
+                {
+                    propertiesElement.setAttribute(
+                            StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE,
+                            StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG );
+                }
+            }
+        }
+
+        private static class AppPropertyOverrideMigration implements PwmExceptionLoggingConsumer<XmlDocument>
+        {
+            @Override
+            public void accept( final XmlDocument xmlDocument ) throws PwmUnrecoverableException
+            {
+                final StoredConfigXmlSerializer.XmlInputDocumentReader documentReader = new StoredConfigXmlSerializer.XmlInputDocumentReader( xmlDocument );
+                final List<XmlElement> appPropertiesElements = documentReader.xpathForAppProperties();
+                for ( final XmlElement element : appPropertiesElements )
+                {
+                    final List<XmlElement> properties = element.getChildren();
+                    for ( final XmlElement 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;
+
+                            final List<String> existingValues = new ArrayList<>();
+                            {
+                                final Optional<StoredConfigData.ValueAndMetaCarrier> valueAndMetaTuple =  documentReader.readSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null );
+                                valueAndMetaTuple.ifPresent( ( t ) -> existingValues.addAll( ( List<String> ) t.getValue().toNativeObject() ) );
+                            }
+                            existingValues.add( newValue );
+                            rewriteAppPropertySettingElement( xmlDocument, existingValues );
+                        }
+                    }
+                    element.detach();
+                }
+            }
+
+            private static void rewriteAppPropertySettingElement( final XmlDocument xmlDocument, final List<String> newValues )
+                    throws PwmUnrecoverableException
+            {
+                final StoredConfigXmlSerializer.XmlInputDocumentReader inputDocumentReader = new StoredConfigXmlSerializer.XmlInputDocumentReader( xmlDocument );
+
+                {
+                    final Optional<XmlElement> existingAppPropertySetting = inputDocumentReader.xpathForSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null );
+                    existingAppPropertySetting.ifPresent( XmlElement::detach );
+                }
+
+                final PwmSecurityKey pwmSecurityKey = inputDocumentReader.getKey();
+
+                final XmlElement settingElement = StoredConfigXmlSerializer.XmlOutputHandler.makeSettingXmlElement(
+                        null,
+                        PwmSetting.APP_PROPERTY_OVERRIDES,
+                        null,
+                        new StringArrayValue( newValues ),
+                        XmlOutputProcessData.builder().storedValueEncoderMode( StoredValueEncoder.Mode.PLAIN ).pwmSecurityKey( pwmSecurityKey ).build() );
+                final Optional<XmlElement> settingsElement = xmlDocument.getRootElement().getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_SETTING );
+                settingsElement.ifPresent( ( s ) -> s.addContent( settingElement ) );
+            }
+        }
+
+        private static int readDocVersion( final XmlDocument xmlDocument )
+        {
+            final String xmlVersionStr = xmlDocument.getRootElement().getAttributeValue( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_XML_VERSION );
+            return JavaHelper.silentParseInt( xmlVersionStr, 0 );
+        }
+    }
+}

+ 319 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigZipJsonSerializer.java

@@ -0,0 +1,319 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+import com.google.gson.reflect.TypeToken;
+import lombok.Value;
+import password.pwm.PwmConstants;
+import password.pwm.config.PwmSettingSyntax;
+import password.pwm.config.StoredValue;
+import password.pwm.config.value.FileValue;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.bean.ImmutableByteArray;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.PwmHashAlgorithm;
+import password.pwm.util.secure.SecureEngine;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigZipJsonSerializer.class );
+
+    private static final String SETTINGS_FILENAME = "settings.json";
+    private static final String META_VALUES_FILENAME = "meta-values.json";
+    private static final String META_FILENAME = "meta.json";
+    private static final String XREF_SUFFIX = ".xref";
+
+    @Override
+    public StoredConfiguration readInput( final InputStream inputStream )
+            throws PwmUnrecoverableException, IOException
+    {
+        final IntermediateRepresentation intermediateRepresentation = readIntermediateRep( inputStream );
+
+        final Map<StoredConfigItemKey, StoredValue> storedValueMap = new HashMap<>();
+        for ( final SerializedValue serializedValue : intermediateRepresentation.getSerializedValues() )
+        {
+            try
+            {
+                final StoredConfigItemKey key = serializedValue.getKey();
+                System.out.println( key.toString() );
+                if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+                {
+                    System.out.println( key.toPwmSetting().getSyntax() );
+                    System.out.println( key.toPwmSetting().getKey() );
+
+                }
+                final PwmSettingSyntax syntax = key.getSyntax();
+                final StoredValue storedValue;
+                if (
+                        StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() )
+                                && key.toPwmSetting().getSyntax().equals( PwmSettingSyntax.FILE )
+                )
+                {
+                    final SerializedFileValue tempValue = JsonUtil.deserialize( serializedValue.getValueData(), SerializedFileValue.class );
+                    final Map<FileValue.FileInformation, FileValue.FileContent> unstrippedMap = new HashMap<>();
+
+                    for ( final Map.Entry<String, FileValue.FileInformation> entry : tempValue.getFileInformation().entrySet() )
+                    {
+                        final String hash = entry.getKey();
+                        final FileValue.FileInformation fileInformation = entry.getValue();
+                        final ImmutableByteArray realContent = intermediateRepresentation.getExRefs().get( hash );
+                        final FileValue.FileContent realFileContent = new FileValue.FileContent( realContent );
+                        unstrippedMap.put( fileInformation, realFileContent );
+                    }
+
+                    storedValue = new FileValue( unstrippedMap );
+                }
+                else
+                {
+                    storedValue = syntax.getFactory().fromJson( serializedValue.getValueData() );
+                }
+                storedValueMap.put( key, storedValue );
+            }
+            catch ( final Exception e )
+            {
+                final String msg = "error reading configuration file: " + e.getMessage();
+                LOGGER.error( () -> msg, e );
+                throw new PwmUnrecoverableException( PwmError.CONFIG_FORMAT_ERROR, msg );
+            }
+        }
+
+        final Map<StoredConfigItemKey, ValueMetaData> valueMetaDataMap = new HashMap<>();
+        for ( final SerializedMetaValue serializedMetaValue : intermediateRepresentation.serializedMetaValues )
+        {
+            valueMetaDataMap.put( serializedMetaValue.getKey(), serializedMetaValue.getValueMetaData() );
+        }
+
+        final StoredConfigData storedConfigData = StoredConfigData.builder()
+                .storedValues( storedValueMap )
+                .metaDatas( valueMetaDataMap )
+                .createTime( intermediateRepresentation.getMetaData().getCreateTime() )
+                .modifyTime( intermediateRepresentation.getMetaData().getModifyTime() )
+                .build();
+
+        return new StoredConfigurationImpl( storedConfigData );
+    }
+
+    private IntermediateRepresentation readIntermediateRep( final InputStream inputStream ) throws IOException
+    {
+        final Map<String, ImmutableByteArray> exRefs = new HashMap<>();
+        final List<SerializedValue> serializedValues = new ArrayList<>();
+        final List<SerializedMetaValue> serializedMetaValues = new ArrayList<>();
+        MetaData metaData = null;
+
+        final ZipInputStream zipInputStream = new ZipInputStream( inputStream );
+        ZipEntry zipEntry;
+        while ( ( zipEntry = zipInputStream.getNextEntry() ) != null )
+        {
+            if ( SETTINGS_FILENAME.equals( zipEntry.getName() ) )
+            {
+                final String stringData = JavaHelper.copy( zipInputStream );
+                final List<SerializedValue> readComponents = JsonUtil.deserialize( stringData, new TypeToken<List<SerializedValue>>()
+                {
+                } );
+                serializedValues.addAll( readComponents );
+            }
+            else if ( META_VALUES_FILENAME.equals( zipEntry.getName() ) )
+            {
+                final String stringData = JavaHelper.copy( zipInputStream );
+                final List<SerializedValue> readMetaValues = JsonUtil.deserialize( stringData, new TypeToken<List<SerializedMetaValue>>()
+                {
+                } );
+                serializedValues.addAll( readMetaValues );
+            }
+            else if ( META_FILENAME.equals( zipEntry.getName() ) )
+            {
+                final String stringData = JavaHelper.copy( zipInputStream );
+                metaData = JsonUtil.deserialize( stringData, MetaData.class );
+            }
+            else if ( zipEntry.getName().endsWith( XREF_SUFFIX ) )
+            {
+                final String hash = zipEntry.getName().substring( 0, zipEntry.getName().length() - XREF_SUFFIX.length() );
+                final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+                JavaHelper.copy( inputStream, byteArrayOutputStream );
+                final byte[] contents = byteArrayOutputStream.toByteArray();
+                exRefs.put( hash, ImmutableByteArray.of( contents ) );
+            }
+        }
+
+        return new IntermediateRepresentation( serializedValues, serializedMetaValues, exRefs, metaData );
+    }
+
+    @Override
+    public void writeOutput(
+            final StoredConfiguration storedConfiguration,
+            final OutputStream outputStream,
+            final StoredConfigurationFactory.OutputSettings outputSettings
+    )
+            throws PwmUnrecoverableException, IOException
+    {
+        final IntermediateRepresentation intermediateRepresentation = makeIntermediateRepresentation( storedConfiguration, outputSettings );
+
+        final ZipOutputStream zipOutputStream = new ZipOutputStream( outputStream );
+
+        {
+            zipOutputStream.putNextEntry( new ZipEntry( SETTINGS_FILENAME ) );
+            JavaHelper.copy( JsonUtil.serializeCollection( intermediateRepresentation.getSerializedValues(), JsonUtil.Flag.PrettyPrint ), zipOutputStream );
+        }
+
+        {
+            zipOutputStream.putNextEntry( new ZipEntry( META_VALUES_FILENAME ) );
+            JavaHelper.copy( JsonUtil.serializeCollection( intermediateRepresentation.getSerializedMetaValues(), JsonUtil.Flag.PrettyPrint ), zipOutputStream );
+        }
+
+        {
+            final MetaData metaData = new MetaData( storedConfiguration.createTime(), storedConfiguration.modifyTime(), "1" );
+            zipOutputStream.putNextEntry( new ZipEntry( META_FILENAME ) );
+            JavaHelper.copy( JsonUtil.serialize( metaData, JsonUtil.Flag.PrettyPrint ), zipOutputStream );
+        }
+
+        for ( final Map.Entry<String, ImmutableByteArray> entry : intermediateRepresentation.getExRefs().entrySet() )
+        {
+            zipOutputStream.putNextEntry( new ZipEntry( entry.getKey() ) );
+            zipOutputStream.write( entry.getValue().copyOf() );
+        }
+
+        zipOutputStream.finish();
+    }
+
+    private IntermediateRepresentation makeIntermediateRepresentation(
+            final StoredConfiguration storedConfiguration,
+            final StoredConfigurationFactory.OutputSettings outputSettings
+    )
+            throws PwmUnrecoverableException, IOException
+    {
+        final Map<String, ImmutableByteArray> exRefs = new LinkedHashMap<>();
+        final List<SerializedValue> serializedValues = new ArrayList<>();
+        for ( final StoredConfigItemKey key : storedConfiguration.modifiedItems() )
+        {
+            final Optional<StoredValue> storedValue = storedConfiguration.readStoredValue( key );
+            if ( storedValue.isPresent() )
+            {
+                final PwmSettingSyntax syntax;
+                final StoredValue value;
+                if (
+                        StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() )
+                                && key.toPwmSetting().getSyntax().equals( PwmSettingSyntax.FILE )
+                )
+                {
+                    final StoredValue fileValue = storedValue.get();
+                    final Map<FileValue.FileInformation, FileValue.FileContent> strippedValues = new HashMap<>( );
+                    final Map<FileValue.FileInformation, FileValue.FileContent> values = ( Map ) fileValue.toNativeObject();
+                    for ( final Map.Entry<FileValue.FileInformation, FileValue.FileContent> entry : values.entrySet() )
+                    {
+                        final FileValue.FileInformation info = entry.getKey();
+                        final FileValue.FileContent content = entry.getValue();
+                        final String hash = hash( content );
+                        strippedValues.put( info, new FileValue.FileContent( ImmutableByteArray.of( hash.getBytes( PwmConstants.DEFAULT_CHARSET ) ) ) );
+                        exRefs.put( hash, content.getContents() );
+                    }
+                    value = new FileValue( strippedValues );
+                    syntax = PwmSettingSyntax.FILE;
+                }
+                else
+                {
+                    value = storedValue.get();
+                    syntax = key.getSyntax();
+                }
+                final String jsonValue = JsonUtil.serialize( ( Serializable ) value.toNativeObject() );
+                final SerializedValue storedComponent = new SerializedValue( key, syntax, jsonValue );
+                serializedValues.add( storedComponent );
+            }
+        }
+
+        final List<SerializedMetaValue> metaValues = new ArrayList<>();
+        for ( final StoredConfigItemKey key : storedConfiguration.modifiedItems() )
+        {
+            final Optional<ValueMetaData> valueMetaData = storedConfiguration.readMetaData( key );
+            valueMetaData.ifPresent( metaData -> metaValues.add( new SerializedMetaValue( key, metaData ) ) );
+        }
+
+        final MetaData metaData = new MetaData( storedConfiguration.createTime(), storedConfiguration.modifyTime(), "1" );
+        return new IntermediateRepresentation(
+                serializedValues,
+                metaValues,
+                exRefs,
+                metaData );
+    }
+
+    private String hash( final FileValue.FileContent fileContent )
+            throws PwmUnrecoverableException
+    {
+        return SecureEngine.hash( fileContent.getContents().copyOf(), PwmHashAlgorithm.SHA256 );
+    }
+
+
+    @Value
+    private static class IntermediateRepresentation
+    {
+        private List<SerializedValue> serializedValues;
+        private List<SerializedMetaValue> serializedMetaValues;
+        private Map<String, ImmutableByteArray> exRefs;
+        private MetaData metaData;
+    }
+
+    @Value
+    private static class SerializedValue implements Serializable
+    {
+        private StoredConfigItemKey key;
+        private PwmSettingSyntax syntax;
+        private String valueData;
+    }
+
+    @Value
+    private static class SerializedFileValue implements Serializable
+    {
+        private Map<String, FileValue.FileInformation> fileInformation;
+    }
+
+    @Value
+    private static class SerializedMetaValue implements Serializable
+    {
+        private StoredConfigItemKey key;
+        private ValueMetaData valueMetaData;
+    }
+
+    @Value
+    private static class MetaData implements Serializable
+    {
+        private String createTime;
+        private Instant modifyTime;
+        private String version;
+    }
+}

+ 204 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigZipXmlSerializer.java

@@ -0,0 +1,204 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+import org.apache.commons.io.IOUtils;
+import password.pwm.PwmConstants;
+import password.pwm.config.PwmSettingSyntax;
+import password.pwm.config.StoredValue;
+import password.pwm.config.value.FileValue;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.bean.ImmutableByteArray;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.secure.PwmHashAlgorithm;
+import password.pwm.util.secure.SecureEngine;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
+{
+    private static final String SETTINGS_FILENAME = "settings.xml";
+    private static final String XREF_SUFFIX = ".xref";
+
+    @Override
+    public StoredConfiguration readInput( final InputStream inputStream )
+            throws PwmUnrecoverableException, IOException
+    {
+        final ZipInputStream zipInputStream = new ZipInputStream( inputStream );
+
+        final Map<String, ImmutableByteArray> exrefMap = new HashMap<>();
+
+        StoredConfiguration storedConfiguration = null;
+
+        ZipEntry zipEntry;
+        while ( ( zipEntry = zipInputStream.getNextEntry() ) != null )
+        {
+            if ( SETTINGS_FILENAME.equals( zipEntry.getName() ) )
+            {
+                final byte[] xmlBytes = IOUtils.toByteArray( zipInputStream );
+                final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( xmlBytes );
+                storedConfiguration = new StoredConfigXmlSerializer().readInput( byteArrayInputStream );
+            }
+            else if ( zipEntry.getName().endsWith( XREF_SUFFIX ) )
+            {
+                final String hash = zipEntry.getName().substring( 0, zipEntry.getName().length() - XREF_SUFFIX.length() );
+                final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+                JavaHelper.copy( zipInputStream, byteArrayOutputStream );
+                final byte[] contents = byteArrayOutputStream.toByteArray();
+                exrefMap.put( hash, ImmutableByteArray.of( contents ) );
+            }
+            zipInputStream.closeEntry();
+
+        }
+
+        return injectXrefs( exrefMap, StoredConfigurationModifier.newModifier( storedConfiguration ) );
+    }
+
+    @Override
+    public void writeOutput(
+            final StoredConfiguration storedConfiguration,
+            final OutputStream outputStream,
+            final StoredConfigurationFactory.OutputSettings outputSettings
+    )
+            throws PwmUnrecoverableException, IOException
+    {
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+        final Map<String, ImmutableByteArray> exrefMap = extractExRefs( modifier );
+        final StoredConfiguration outputConfig = modifier.newStoredConfiguration();
+
+        final ZipOutputStream zipOutputStream = new ZipOutputStream( outputStream );
+
+        for ( final Map.Entry<String, ImmutableByteArray> entry : exrefMap.entrySet() )
+        {
+            zipOutputStream.putNextEntry( new ZipEntry( entry.getKey() + XREF_SUFFIX ) );
+            zipOutputStream.write( entry.getValue().copyOf() );
+            zipOutputStream.closeEntry();
+        }
+
+        {
+            zipOutputStream.putNextEntry( new ZipEntry( SETTINGS_FILENAME ) );
+            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            new StoredConfigXmlSerializer().writeOutput( outputConfig, byteArrayOutputStream, outputSettings );
+            zipOutputStream.write( byteArrayOutputStream.toByteArray() );
+            zipOutputStream.closeEntry();
+        }
+
+
+        zipOutputStream.finish();
+    }
+
+    private StoredConfiguration injectXrefs(
+            final Map<String, ImmutableByteArray> exRefMap,
+            final StoredConfigurationModifier modifier
+    )
+            throws PwmUnrecoverableException
+    {
+        final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
+        for ( final StoredConfigItemKey key : inputConfig.modifiedItems() )
+        {
+            if (
+                    StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() )
+                            && key.toPwmSetting().getSyntax().equals( PwmSettingSyntax.FILE )
+            )
+            {
+                final Optional<StoredValue> optionalStoredValue = inputConfig.readStoredValue( key );
+                if ( optionalStoredValue.isPresent() )
+                {
+                    final FileValue fileValue = ( FileValue ) optionalStoredValue.get();
+                    final Map<FileValue.FileInformation, FileValue.FileContent> values = ( Map ) fileValue.toNativeObject();
+                    final Map<FileValue.FileInformation, FileValue.FileContent> stripedValues = new HashMap<>();
+
+                    for ( final Map.Entry<FileValue.FileInformation, FileValue.FileContent> entry : values.entrySet() )
+                    {
+                        final FileValue.FileInformation info = entry.getKey();
+                        final FileValue.FileContent content = entry.getValue();
+                        final String hash = new String( content.getContents().copyOf(), PwmConstants.DEFAULT_CHARSET );
+
+                        final ImmutableByteArray realContents = exRefMap.get( hash );
+                        if ( realContents != null )
+                        {
+                            final FileValue.FileContent realFileContent = new FileValue.FileContent( realContents );
+                            stripedValues.put( info, realFileContent );
+                        }
+                    }
+
+                    if ( !stripedValues.isEmpty() )
+                    {
+                        final FileValue strippedFileValue = new FileValue( stripedValues );
+                        modifier.writeSetting( key.toPwmSetting(), key.getProfileID(),  strippedFileValue, null );
+                    }
+                }
+            }
+        }
+        return modifier.newStoredConfiguration();
+    }
+
+    private Map<String, ImmutableByteArray> extractExRefs( final StoredConfigurationModifier modifier ) throws PwmUnrecoverableException
+    {
+        final Map<String, ImmutableByteArray> returnObj = new HashMap<>();
+        final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
+        for ( final StoredConfigItemKey key : inputConfig.modifiedItems() )
+        {
+            if (
+                    StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() )
+                            && key.toPwmSetting().getSyntax().equals( PwmSettingSyntax.FILE )
+            )
+            {
+                final Optional<StoredValue> optionalStoredValue = inputConfig.readStoredValue( key );
+                if ( optionalStoredValue.isPresent() )
+                {
+                    final FileValue fileValue = ( FileValue ) optionalStoredValue.get();
+                    final Map<FileValue.FileInformation, FileValue.FileContent> values = ( Map ) fileValue.toNativeObject();
+                    final Map<FileValue.FileInformation, FileValue.FileContent> stripedValues = new HashMap<>();
+
+                    for ( final Map.Entry<FileValue.FileInformation, FileValue.FileContent> entry : values.entrySet() )
+                    {
+                        final FileValue.FileInformation info = entry.getKey();
+                        final FileValue.FileContent content = entry.getValue();
+                        final String hash = hash( content );
+                        final FileValue.FileContent fileContentWithHash = new FileValue.FileContent( ImmutableByteArray.of( hash.getBytes( PwmConstants.DEFAULT_CHARSET ) ) );
+                        returnObj.put( hash, content.getContents() );
+                        stripedValues.put( info, fileContentWithHash );
+                    }
+                    final FileValue strippedFileValue = new FileValue( stripedValues );
+                    modifier.writeSetting( key.toPwmSetting(), key.getProfileID(),  strippedFileValue, null );
+                }
+            }
+        }
+        return returnObj;
+    }
+
+    private static String hash( final FileValue.FileContent fileContent )
+            throws PwmUnrecoverableException
+    {
+        return SecureEngine.hash( fileContent.getContents().copyOf(), PwmHashAlgorithm.SHA256 );
+    }
+}

+ 11 - 608
server/src/main/java/password/pwm/config/stored/StoredConfigurationFactory.java

@@ -22,54 +22,18 @@ package password.pwm.config.stored;
 
 import lombok.Builder;
 import lombok.Value;
-import password.pwm.PwmConstants;
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.PwmSetting;
-import password.pwm.config.PwmSettingFlag;
-import password.pwm.config.PwmSettingSyntax;
-import password.pwm.config.PwmSettingTemplate;
-import password.pwm.config.PwmSettingTemplateSet;
-import password.pwm.config.StoredValue;
-import password.pwm.config.value.LocalizedStringValue;
-import password.pwm.config.value.StoredValueEncoder;
-import password.pwm.config.value.StringValue;
-import password.pwm.config.value.ValueFactory;
-import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.i18n.PwmLocaleBundle;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.LazySupplier;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.java.XmlDocument;
-import password.pwm.util.java.XmlElement;
-import password.pwm.util.java.XmlFactory;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
-import password.pwm.util.secure.PwmSecurityKey;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.time.Instant;
-import java.time.format.DateTimeParseException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.ResourceBundle;
-import java.util.Set;
 
 public class StoredConfigurationFactory
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationFactory.class );
 
-    private static final String XML_FORMAT_VERSION = "5";
-
+    private static final StoredConfigSerializer SERIALIZER = new StoredConfigXmlSerializer();
 
     public static StoredConfiguration newConfig() throws PwmUnrecoverableException
     {
@@ -88,601 +52,40 @@ public class StoredConfigurationFactory
 
     public static StoredConfigurationModifier newModifiableConfig() throws PwmUnrecoverableException
     {
-        final StoredConfiguration storedConfiguration = new StoredConfigurationImpl(  );
-        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
-
-        StoredConfigurationUtil.initNewRandomSecurityKey( modifier );
-        modifier.writeConfigProperty(
-                ConfigurationProperty.CONFIG_IS_EDITABLE, Boolean.toString( true ) );
-        modifier.writeConfigProperty(
-                ConfigurationProperty.CONFIG_EPOCH, String.valueOf( 0 ) );
-
-
-        return modifier;
+        final StoredConfiguration storedConfiguration = newConfig();
+        return StoredConfigurationModifier.newModifier( storedConfiguration );
     }
 
-    public static StoredConfiguration fromXml( final InputStream inputStream )
-            throws PwmUnrecoverableException
+    public static StoredConfiguration input( final InputStream inputStream )
+            throws PwmUnrecoverableException, IOException
     {
-        final XmlFactory xmlFactory = XmlFactory.getFactory();
-        final XmlDocument xmlDocument = xmlFactory.parseXml( inputStream );
-        ConfigurationCleaner.preProcessXml( xmlDocument );
 
-        final XmlInputDocumentReader xmlInputDocumentReader = new XmlInputDocumentReader( xmlDocument );
-        final StoredConfigData storedConfigData = xmlInputDocumentReader.getStoredConfigData();
-        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier(  new StoredConfigurationImpl( storedConfigData ) );
+        final StoredConfiguration storedConfiguration = SERIALIZER.readInput( inputStream );
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier(  storedConfiguration );
         ConfigurationCleaner.postProcessStoredConfig( modifier );
-
         return modifier.newStoredConfiguration();
     }
 
-    public static void toXml(
+    public static void output(
             final StoredConfiguration storedConfiguration,
             final OutputStream outputStream
     )
             throws PwmUnrecoverableException, IOException
     {
-        toXml( storedConfiguration, outputStream, OutputSettings.builder().build() );
+        output( storedConfiguration, outputStream, StoredConfigurationFactory.OutputSettings.builder().build() );
     }
 
-    public static void toXml(
+    public static void output(
             final StoredConfiguration storedConfiguration,
             final OutputStream outputStream,
             final OutputSettings outputSettings
     )
             throws PwmUnrecoverableException, IOException
     {
-        final XmlFactory xmlFactory = XmlFactory.getFactory();
-        final XmlDocument xmlDocument = xmlFactory.newDocument( StoredConfigXmlConstants.XML_ELEMENT_ROOT );
-
-        XmlOutputHandler.makeXmlOutput( storedConfiguration, xmlDocument.getRootElement(), outputSettings );
-
-        xmlFactory.outputDocument( xmlDocument, outputStream );
-    }
-
-    static class XmlInputDocumentReader
-    {
-        private final XmlDocument document;
-
-        XmlInputDocumentReader( final XmlDocument document )
-        {
-            this.document = document;
-        }
-
-        StoredConfigData getStoredConfigData()
-        {
-            final String createTime = readCreateTime();
-            final Instant modifyTime = readModifyTime();
-
-            final List<StoredConfigData.ValueAndMetaCarrier> values = new ArrayList<>();
-            values.addAll( readProperties() );
-            values.addAll( readSettings() );
-            values.addAll( readLocaleBundles() );
-            return StoredConfigData.builder()
-                    .createTime( createTime )
-                    .modifyTime( modifyTime )
-                    .metaDatas( StoredConfigData.carrierAsMetaDataMap( values ) )
-                    .storedValues( StoredConfigData.carrierAsStoredValueMap( values ) )
-                    .build();
-        }
-
-        private Collection<StoredConfigData.ValueAndMetaCarrier> readProperties()
-        {
-            final List<StoredConfigData.ValueAndMetaCarrier> valueAndMetaWrapper = new ArrayList<>();
-            for ( final ConfigurationProperty configurationProperty : ConfigurationProperty.values() )
-            {
-                final Optional<XmlElement> propertyElement = xpathForConfigProperty( configurationProperty );
-                if ( propertyElement.isPresent() && !StringUtil.isEmpty( propertyElement.get().getText() ) )
-                {
-                    final StoredConfigItemKey key = StoredConfigItemKey.fromConfigurationProperty( configurationProperty );
-                    final StoredValue storedValue = new StringValue( propertyElement.get().getText() );
-                    final ValueMetaData metaData = readMetaDataFromXmlElement( key, propertyElement.get() ).orElse( null );
-                    valueAndMetaWrapper.add( new StoredConfigData.ValueAndMetaCarrier( key, storedValue, metaData ) );
-                }
-            }
-            return valueAndMetaWrapper;
-        }
-
-        private Collection<StoredConfigData.ValueAndMetaCarrier> readSettings()
-        {
-            final List<StoredConfigData.ValueAndMetaCarrier> returnList = new ArrayList<>();
-            for ( final PwmSetting pwmSetting : PwmSetting.values() )
-            {
-                if ( !pwmSetting.getCategory().hasProfiles() )
-                {
-                    readSetting( pwmSetting, null ).ifPresent( returnList::add );
-                }
-            }
-
-            for ( final PwmSetting pwmSetting : PwmSetting.values() )
-            {
-                if ( pwmSetting.getCategory().hasProfiles() )
-                {
-                    final List<String> profileIDs = profilesForSetting( pwmSetting );
-                    for ( final String profileID : profileIDs )
-                    {
-                        readSetting( pwmSetting, profileID ).ifPresent( returnList::add );
-                    }
-                }
-            }
-            return returnList;
-        }
-
-        Optional<StoredConfigData.ValueAndMetaCarrier> readSetting( final PwmSetting setting, final String profileID )
-        {
-            final Optional<XmlElement> settingElement = xpathForSetting( setting, profileID );
-
-            if ( !settingElement.isPresent() )
-            {
-                return Optional.empty();
-            }
-
-            if ( settingElement.get().getChild( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT ).isPresent() )
-            {
-                return Optional.empty();
-            }
-
-            try
-            {
-                final StoredValue storedValue = ValueFactory.fromXmlValues( setting, settingElement.get(), getKey() );
-                final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
-                final ValueMetaData metaData = readMetaDataFromXmlElement( key, settingElement.get() ).orElse( null );
-                return Optional.of( new StoredConfigData.ValueAndMetaCarrier( key, storedValue, metaData ) );
-            }
-            catch ( final PwmException e )
-            {
-                final String errorMsg = "unexpected error reading setting '" + setting.getKey() + "' profile '" + profileID + "', error: " + e.getMessage();
-                throw new IllegalStateException( errorMsg );
-            }
-        }
-
-        List<String> profilesForSetting( final PwmSetting pwmSetting )
-        {
-            if ( !pwmSetting.getCategory().hasProfiles() && pwmSetting.getSyntax() != PwmSettingSyntax.PROFILE )
-            {
-                return Collections.emptyList();
-            }
-
-            final PwmSetting profileSetting;
-            if ( pwmSetting.getSyntax() == PwmSettingSyntax.PROFILE )
-            {
-                profileSetting = pwmSetting;
-            }
-            else
-            {
-                profileSetting = pwmSetting.getCategory().getProfileSetting().orElseThrow( IllegalStateException::new );
-            }
-
-            final StoredValue effectiveValue;
-            {
-                final Optional<StoredConfigData.ValueAndMetaCarrier> configuredValue = readSetting( profileSetting, null );
-                if ( configuredValue.isPresent() )
-                {
-                    effectiveValue = configuredValue.get().getValue();
-                }
-                else
-                {
-                    effectiveValue = profileSetting.getDefaultValue( templateSetSupplier.get() );
-                }
-            }
-
-            final List<String> settingValues = ( List<String> ) effectiveValue.toNativeObject();
-            final LinkedList<String> profiles = new LinkedList<>( settingValues );
-            profiles.removeIf( StringUtil::isEmpty );
-            return Collections.unmodifiableList( profiles );
-        }
-
-
-        public PwmSecurityKey getKey() throws PwmUnrecoverableException
-        {
-            final XmlElement rootElement = document.getRootElement();
-            final String createTimeString = rootElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_CREATE_TIME );
-            return new PwmSecurityKey( createTimeString + "StoredConfiguration" );
-        }
-
-        String readCreateTime()
-        {
-            final XmlElement rootElement = document.getRootElement();
-            final String createTimeString = rootElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_CREATE_TIME );
-            if ( StringUtil.isEmpty( createTimeString ) )
-            {
-                throw new IllegalStateException( "missing createTime timestamp" );
-            }
-            else
-            {
-                return createTimeString;
-            }
-        }
-
-        Instant readModifyTime()
-        {
-            final XmlElement rootElement = document.getRootElement();
-            final String modifyTimeString = rootElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME );
-            if ( !StringUtil.isEmpty( modifyTimeString ) )
-            {
-                try
-                {
-                    return JavaHelper.parseIsoToInstant( modifyTimeString );
-                }
-                catch ( final Exception e )
-                {
-                    LOGGER.error( () -> "error parsing root last modified timestamp: " + e.getMessage() );
-                }
-            }
-
-            return null;
-        }
-
-        private final LazySupplier<PwmSettingTemplateSet> templateSetSupplier = new LazySupplier<>( () ->
-        {
-            final Set<PwmSettingTemplate> templates = new HashSet<>();
-            templates.add( readTemplateValue( PwmSetting.TEMPLATE_LDAP ) );
-            templates.add( readTemplateValue( PwmSetting.TEMPLATE_STORAGE ) );
-            templates.add( readTemplateValue( PwmSetting.DB_VENDOR_TEMPLATE ) );
-            return new PwmSettingTemplateSet( templates );
-        } );
-
-        private PwmSettingTemplate readTemplateValue( final PwmSetting pwmSetting )
-        {
-            final Optional<XmlElement> settingElement = xpathForSetting( pwmSetting, null );
-            if ( settingElement.isPresent() )
-            {
-                try
-                {
-                    final String strValue = ( String ) ValueFactory.fromXmlValues( pwmSetting, settingElement.get(), null ).toNativeObject();
-                    return JavaHelper.readEnumFromString( PwmSettingTemplate.class, null, strValue );
-                }
-                catch ( final IllegalStateException e )
-                {
-                    LOGGER.error( () -> "error reading template", e );
-                }
-            }
-            return null;
-        }
-
-        private Collection<StoredConfigData.ValueAndMetaCarrier> readLocaleBundles()
-        {
-            final List<StoredConfigData.ValueAndMetaCarrier> returnWrapper = new ArrayList<>();
-
-            for ( final XmlElement localeBundleElement : xpathForLocaleBundles() )
-            {
-                final String bundleName = localeBundleElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_BUNDLE );
-                final Optional<PwmLocaleBundle> pwmLocaleBundle = PwmLocaleBundle.forKey( bundleName );
-                pwmLocaleBundle.ifPresent( ( bundle ) ->
-                {
-                    final String key = localeBundleElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY );
-                    if ( bundle.getDisplayKeys().contains( key ) )
-                    {
-                        final Map<String, String> bundleMap = new LinkedHashMap<>();
-                        for ( final XmlElement valueElement : localeBundleElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE ) )
-                        {
-                            final String localeStrValue = valueElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE );
-                            bundleMap.put( localeStrValue == null ? "" : localeStrValue, valueElement.getText() );
-                        }
-                        if ( !bundleMap.isEmpty() )
-                        {
-                            final StoredConfigItemKey storedConfigItemKey = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle.get(), key );
-                            final StoredValue storedValue = new LocalizedStringValue( bundleMap );
-                            final ValueMetaData metaData = readMetaDataFromXmlElement( storedConfigItemKey, localeBundleElement ).orElse( null );
-                            returnWrapper.add( new StoredConfigData.ValueAndMetaCarrier( storedConfigItemKey, storedValue, metaData ) );
-                        }
-                    }
-                } );
-            }
-            return Collections.unmodifiableList( returnWrapper );
-        }
-
-        private Optional<ValueMetaData> readMetaDataFromXmlElement( final StoredConfigItemKey key, final XmlElement xmlElement )
-        {
-            Instant instant = null;
-            {
-                final String modifyTimeValue = xmlElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME );
-                if ( !StringUtil.isEmpty( modifyTimeValue ) )
-                {
-                    try
-                    {
-                        instant = JavaHelper.parseIsoToInstant( modifyTimeValue );
-                    }
-                    catch ( final DateTimeParseException e )
-                    {
-                        e.printStackTrace();
-                    }
-                }
-            }
-
-            UserIdentity userIdentity = null;
-            {
-                final String modifyUserValue = xmlElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_USER );
-                if ( !StringUtil.isEmpty( modifyUserValue ) )
-                {
-                    try
-                    {
-                        userIdentity = UserIdentity.fromDelimitedKey( modifyUserValue );
-                    }
-                    catch ( final DateTimeParseException | PwmUnrecoverableException e )
-                    {
-                        LOGGER.trace( () -> "unable to parse userIdentity metadata for key " + key.toString() );
-                    }
-                }
-            }
-
-            if ( instant != null || userIdentity != null )
-            {
-                return Optional.of( new ValueMetaData( instant, userIdentity ) );
-            }
-
-            return Optional.empty();
-        }
-
-        List<XmlElement> xpathForLocaleBundles()
-        {
-            final String xpathString = "//localeBundle";
-            return document.evaluateXpathToElements( xpathString );
-        }
-
-        XmlElement xpathForSettings()
-        {
-            return document.getRootElement().getChild( StoredConfigXmlConstants.XML_ELEMENT_SETTINGS )
-                    .orElseThrow( () -> new IllegalStateException( "configuration xml document missing 'settings' element" ) );
-        }
-
-        Optional<XmlElement> xpathForSetting( final PwmSetting setting, final String profileID )
-        {
-            final String xpathString;
-            if ( profileID == null || profileID.length() < 1 )
-            {
-                xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_SETTING + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_KEY
-                        + "=\"" + setting.getKey()
-                        + "\"][(not (@" + StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE + ")) or @"
-                        + StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE + "=\"\"]";
-            }
-            else
-            {
-                xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_SETTING + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_KEY
-                        + "=\"" + setting.getKey()
-                        + "\"][@" + StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE + "=\"" + profileID + "\"]";
-            }
-
-            return document.evaluateXpathToElement( xpathString );
-        }
-
-        Optional<XmlElement> xpathForConfigProperty( final ConfigurationProperty configProperty )
-        {
-            final String xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE
-                    + "=\"" + StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG + "\"]/"
-                    + StoredConfigXmlConstants.XML_ELEMENT_PROPERTY + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_KEY + "=\"" + configProperty.getKey() + "\"]";
-            return document.evaluateXpathToElement( xpathString );
-        }
-
-        List<XmlElement> xpathForAppProperties( )
-        {
-            final String xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
-                    + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + "=\"" + StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_APP + "\"]";
-            return document.evaluateXpathToElements( xpathString );
-        }
+        SERIALIZER.writeOutput( storedConfiguration, outputStream, outputSettings );
     }
 
 
-    static class XmlOutputHandler
-    {
-        static void makeXmlOutput( final StoredConfiguration storedConfiguration, final XmlElement rootElement, final OutputSettings outputSettings )
-                throws PwmUnrecoverableException
-        {
-            decorateRootElement( rootElement, storedConfiguration );
-
-            rootElement.addContent( makePropertiesElement( storedConfiguration ) );
-
-            rootElement.addContent( makeSettingsXmlElement( storedConfiguration, outputSettings ) );
-
-            rootElement.addContent( XmlOutputHandler.makeLocaleBundleXmlElements( storedConfiguration ) );
-        }
-
-        static void decorateRootElement( final XmlElement rootElement, final StoredConfiguration storedConfiguration )
-        {
-            rootElement.setComment( Collections.singletonList( generateCommentText() ) );
-            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_PWM_VERSION, PwmConstants.BUILD_VERSION );
-            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRRIBUTE_PWM_BUILD, PwmConstants.BUILD_NUMBER );
-            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_XML_VERSION, XML_FORMAT_VERSION );
-
-            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_CREATE_TIME, storedConfiguration.createTime() );
-            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( storedConfiguration.modifyTime() ) );
-        }
-
-        static XmlElement makeSettingsXmlElement(
-                final StoredConfiguration storedConfiguration,
-                final OutputSettings outputSettings
-        )
-                throws PwmUnrecoverableException
-        {
-            final PwmSecurityKey pwmSecurityKey = storedConfiguration.getKey();
-
-            final XmlFactory xmlFactory = XmlFactory.getFactory();
-            final XmlElement settingsElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_SETTINGS );
-
-            final XmlOutputProcessData xmlOutputProcessData = XmlOutputProcessData.builder()
-                    .pwmSecurityKey( pwmSecurityKey )
-                    .storedValueEncoderMode( figureEncoderMode( storedConfiguration, outputSettings ) )
-                    .build();
-
-            for ( final PwmSetting pwmSetting : PwmSetting.sortedByMenuLocation( PwmConstants.DEFAULT_LOCALE ) )
-            {
-                if ( !pwmSetting.getFlags().contains( PwmSettingFlag.Deprecated ) )
-                {
-                    if ( pwmSetting.getCategory().hasProfiles() )
-                    {
-                        for ( final String profileID : storedConfiguration.profilesForSetting( pwmSetting ) )
-                        {
-                            final StoredValue storedValue = storedConfiguration.readSetting( pwmSetting, profileID );
-                            final XmlElement settingElement = makeSettingXmlElement( storedConfiguration, pwmSetting, profileID, storedValue, xmlOutputProcessData );
-                            decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromSetting( pwmSetting, profileID ), settingElement );
-                            settingsElement.addContent( settingElement );
-                        }
-                    }
-                    else
-                    {
-                        final StoredValue storedValue = storedConfiguration.readSetting( pwmSetting, null );
-                        final XmlElement settingElement = makeSettingXmlElement( storedConfiguration, pwmSetting, null, storedValue, xmlOutputProcessData );
-                        decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromSetting( pwmSetting, null ), settingElement );
-                        settingsElement.addContent( settingElement );
-                    }
-                }
-            }
-
-            return settingsElement;
-        }
-
-        static StoredValueEncoder.Mode figureEncoderMode(
-                final StoredConfiguration storedConfiguration,
-                final OutputSettings outputSettings
-        )
-        {
-            if ( outputSettings == null || outputSettings.getMode() == null )
-            {
-                return StoredValueEncoder.Mode.ENCODED;
-            }
-
-            if ( outputSettings.getMode() == OutputSettings.SecureOutputMode.STRIPPED )
-            {
-                return StoredValueEncoder.Mode.STRIPPED;
-            }
-
-            final Optional<String> strValue = storedConfiguration.readConfigProperty( ConfigurationProperty.STORE_PLAINTEXT_VALUES );
-            if ( strValue.isPresent() && Boolean.parseBoolean( strValue.get() ) )
-            {
-                return StoredValueEncoder.Mode.PLAIN;
-            }
-
-            return StoredValueEncoder.Mode.ENCODED;
-        }
-
-
-        static XmlElement makeSettingXmlElement(
-                final StoredConfiguration storedConfiguration,
-                final PwmSetting pwmSetting,
-                final String profileID,
-                final StoredValue storedValue,
-                final XmlOutputProcessData xmlOutputProcessData
-        )
-        {
-            final XmlFactory xmlFactory = XmlFactory.getFactory();
-
-            final XmlElement settingElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_SETTING );
-            settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, pwmSetting.getKey() );
-
-            if ( !StringUtil.isEmpty( profileID ) )
-            {
-                settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE, profileID );
-            }
-
-            settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX, pwmSetting.getSyntax().name() );
-
-            {
-                final XmlElement labelElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_LABEL );
-                labelElement.addText( pwmSetting.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) );
-                settingElement.addContent( labelElement );
-            }
-
-            final List<XmlElement> valueElements = new ArrayList<>(  );
-            if ( storedConfiguration != null && storedConfiguration.isDefaultValue( pwmSetting, profileID ) )
-            {
-                final XmlElement defaultValue = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT );
-                valueElements.add( defaultValue );
-            }
-            else
-            {
-                valueElements.addAll( storedValue.toXmlValues( StoredConfigXmlConstants.XML_ELEMENT_VALUE, xmlOutputProcessData ) );
-            }
-
-            settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION, String.valueOf( storedValue.currentSyntaxVersion() ) );
-            settingElement.addContent( valueElements );
-            return settingElement;
-        }
-
-        private static void decorateElementWithMetaData(
-                final StoredConfiguration storedConfiguration,
-                final StoredConfigItemKey key,
-                final XmlElement xmlElement
-        )
-        {
-            final Optional<ValueMetaData> valueMetaData = ( ( StoredConfiguration ) storedConfiguration ).readMetaData( key );
-
-            if ( valueMetaData.isPresent() )
-            {
-                if ( valueMetaData.get().getUserIdentity() != null )
-                {
-                    xmlElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_USER, valueMetaData.get().getUserIdentity().toDelimitedKey() );
-                }
-
-                if ( valueMetaData.get().getModifyDate() != null )
-                {
-                    xmlElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( valueMetaData.get().getModifyDate() ) );
-                }
-            }
-        }
-
-        private static XmlElement makePropertiesElement( final StoredConfiguration storedConfiguration )
-        {
-            final XmlFactory xmlFactory = XmlFactory.getFactory();
-            final XmlElement propertiesElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES );
-            propertiesElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE, StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG );
-
-            for ( final ConfigurationProperty configurationProperty : ConfigurationProperty.values() )
-            {
-                storedConfiguration.readConfigProperty( configurationProperty ).ifPresent( s ->
-                        {
-                            final XmlElement propertyElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_PROPERTY );
-                            propertyElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, configurationProperty.getKey() );
-                            propertyElement.addText( s );
-                            decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromConfigurationProperty( configurationProperty ), propertyElement );
-                            propertiesElement.addContent( propertyElement );
-                        }
-                );
-            }
-
-            return propertiesElement;
-        }
-
-        private static List<XmlElement> makeLocaleBundleXmlElements( final StoredConfiguration storedConfiguration )
-        {
-            final XmlFactory xmlFactory = XmlFactory.getFactory();
-            final List<XmlElement> returnList = new ArrayList<>();
-            for ( final PwmLocaleBundle pwmLocaleBundle : PwmLocaleBundle.values() )
-            {
-                for ( final String key : pwmLocaleBundle.getDisplayKeys() )
-                {
-                    final Map<String, String> localeBundle = storedConfiguration.readLocaleBundleMap( pwmLocaleBundle, key );
-                    if ( !JavaHelper.isEmpty( localeBundle ) )
-                    {
-                        final XmlElement localeBundleElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_LOCALEBUNDLE );
-                        localeBundleElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_BUNDLE, pwmLocaleBundle.getKey() );
-                        localeBundleElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, key );
-
-                        final Map<String, String> localeBundleMap = storedConfiguration.readLocaleBundleMap( pwmLocaleBundle, key );
-                        for ( final Map.Entry<String, String> entry : localeBundleMap.entrySet() )
-                        {
-                            final XmlElement valueElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
-                            if ( !StringUtil.isEmpty( entry.getKey() ) )
-                            {
-                                valueElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE, entry.getKey() );
-                            }
-                            valueElement.addText( entry.getValue() );
-                            localeBundleElement.addContent( valueElement );
-                        }
-
-                        decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, key ), localeBundleElement );
-                        returnList.add( localeBundleElement );
-                    }
-                }
-            }
-            return Collections.unmodifiableList( returnList );
-        }
-
-        private static String generateCommentText()
-        {
-            final String resourceText = ResourceBundle.getBundle( StoredConfigurationFactory.class.getName() ).getString( "configCommentText" );
-            return MacroMachine.forStatic().expandMacros( resourceText );
-        }
-    }
 
     @Value
     @Builder

+ 2 - 2
server/src/main/java/password/pwm/config/value/AbstractValue.java

@@ -22,7 +22,7 @@ package password.pwm.config.value;
 
 import password.pwm.PwmConstants;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JsonUtil;
@@ -82,7 +82,7 @@ public abstract class AbstractValue implements StoredValue
                     .pwmSecurityKey( testingKey )
                     .storedValueEncoderMode( StoredValueEncoder.Mode.PLAIN )
                     .build();
-            final List<XmlElement> xmlValues = storedValue.toXmlValues( StoredConfigXmlConstants.XML_ELEMENT_VALUE, xmlOutputProcessData );
+            final List<XmlElement> xmlValues = storedValue.toXmlValues( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE, xmlOutputProcessData );
             final XmlDocument document = XmlFactory.getFactory().newDocument( "root" );
             document.getRootElement().addContent( xmlValues );
             final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

+ 5 - 5
server/src/main/java/password/pwm/config/value/ActionValue.java

@@ -25,7 +25,7 @@ import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.error.PwmOperationalException;
@@ -99,8 +99,8 @@ public class ActionValue extends AbstractValue implements StoredValue
                 final List<ActionConfiguration> values = new ArrayList<>();
 
                 final boolean oldType = PwmSettingSyntax.STRING_ARRAY.toString().equals(
-                        settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX ) );
-                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                        settingElement.getAttributeValue( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX ) );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 for ( final XmlElement loopValueElement : valueElements )
                 {
                     final String stringValue = loopValueElement.getText();
@@ -110,7 +110,7 @@ public class ActionValue extends AbstractValue implements StoredValue
                         {
                             if ( oldType )
                             {
-                                if ( loopValueElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE ) == null )
+                                if ( loopValueElement.getAttributeValue( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE ) == null )
                                 {
                                     final ActionConfiguration.ActionConfigurationOldVersion1 oldVersion1 = ActionConfiguration.ActionConfigurationOldVersion1
                                             .parseOldConfigString( stringValue );
@@ -395,7 +395,7 @@ public class ActionValue extends AbstractValue implements StoredValue
 
     private static int figureCurrentStoredSyntax( final XmlElement settingElement )
     {
-        final String storedSyntaxVersionString = settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION );
+        final String storedSyntaxVersionString = settingElement.getAttributeValue( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION );
         if ( !StringUtil.isEmpty( storedSyntaxVersionString ) )
         {
             try

+ 2 - 2
server/src/main/java/password/pwm/config/value/BooleanValue.java

@@ -23,7 +23,7 @@ package password.pwm.config.value;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.i18n.Display;
 import password.pwm.util.java.JsonUtil;
@@ -58,7 +58,7 @@ public class BooleanValue implements StoredValue
 
             public BooleanValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             {
-                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 if ( valueElement.isPresent() )
                 {
                     final String value = valueElement.get().getTextTrim();

+ 2 - 2
server/src/main/java/password/pwm/config/value/FileValue.java

@@ -25,7 +25,7 @@ import lombok.Value;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ImmutableByteArray;
@@ -108,7 +108,7 @@ public class FileValue extends AbstractValue implements StoredValue
 
             public FileValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             {
-                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final Map<FileInformation, FileContent> values = new LinkedHashMap<>();
                 for ( final XmlElement loopValueElement : valueElements )
                 {

+ 3 - 3
server/src/main/java/password/pwm/config/value/LocalizedStringValue.java

@@ -23,7 +23,7 @@ package password.pwm.config.value;
 import com.google.gson.reflect.TypeToken;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
@@ -71,11 +71,11 @@ public class LocalizedStringValue extends AbstractValue implements StoredValue
 
             public LocalizedStringValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
-                final List<XmlElement> elements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final List<XmlElement> elements = settingElement.getChildren( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final Map<String, String> values = new TreeMap<>();
                 for ( final XmlElement loopValueElement : elements )
                 {
-                    final String localeString = loopValueElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE );
+                    final String localeString = loopValueElement.getAttributeValue( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE );
                     final String value = loopValueElement.getText();
                     values.put( localeString == null ? "" : localeString, value );
                 }

+ 2 - 2
server/src/main/java/password/pwm/config/value/NamedSecretValue.java

@@ -24,7 +24,7 @@ import com.google.gson.reflect.TypeToken;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.NamedSecretData;
 import password.pwm.error.ErrorInformation;
@@ -101,7 +101,7 @@ public class NamedSecretValue implements StoredValue
                     throws PwmOperationalException, PwmUnrecoverableException
             {
                 final Map<String, NamedSecretData> values = new LinkedHashMap<>();
-                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
 
                 try
                 {

+ 2 - 2
server/src/main/java/password/pwm/config/value/NumericValue.java

@@ -23,7 +23,7 @@ package password.pwm.config.value;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingProperty;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
@@ -54,7 +54,7 @@ public class NumericValue extends AbstractValue implements StoredValue
 
             public NumericValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             {
-                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 if ( valueElement.isPresent() )
                 {
                     final String value = valueElement.get().getText();

+ 2 - 2
server/src/main/java/password/pwm/config/value/PasswordValue.java

@@ -24,7 +24,7 @@ package password.pwm.config.value;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -90,7 +90,7 @@ public class PasswordValue implements StoredValue
             )
                     throws PwmOperationalException, PwmUnrecoverableException
             {
-                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 if ( valueElement.isPresent() )
                 {
                     final String rawValue = valueElement.get().getText();

+ 4 - 4
server/src/main/java/password/pwm/config/value/PrivateKeyValue.java

@@ -23,7 +23,7 @@ package password.pwm.config.value;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
@@ -61,10 +61,10 @@ public class PrivateKeyValue extends AbstractValue
         {
             public PrivateKeyValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
-                if ( settingElement != null && settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE  ).isPresent() )
+                if ( settingElement != null && settingElement.getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE  ).isPresent() )
                 {
 
-                    final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                    final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                     if ( valueElement.isPresent() )
                     {
                         final List<X509Certificate> certificates = new ArrayList<>();
@@ -156,7 +156,7 @@ public class PrivateKeyValue extends AbstractValue
 
     public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
-        final XmlElement valueElement = XmlFactory.getFactory().newElement( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+        final XmlElement valueElement = XmlFactory.getFactory().newElement( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
         if ( privateKeyCertificate != null )
         {
             try

+ 2 - 2
server/src/main/java/password/pwm/config/value/RemoteWebServiceValue.java

@@ -24,7 +24,7 @@ import com.google.gson.reflect.TypeToken;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.RemoteWebServiceConfiguration;
 import password.pwm.error.PwmOperationalException;
@@ -90,7 +90,7 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
             )
                     throws PwmOperationalException
             {
-                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final List<RemoteWebServiceConfiguration> values = new ArrayList<>();
                 for ( final XmlElement loopValueElement : valueElements )
                 {

+ 2 - 2
server/src/main/java/password/pwm/config/value/StringArrayValue.java

@@ -22,7 +22,7 @@ package password.pwm.config.value;
 
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
@@ -70,7 +70,7 @@ public class StringArrayValue extends AbstractValue implements StoredValue
 
             public StringArrayValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
-                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final List<String> values = new ArrayList<>();
                 for ( final XmlElement loopValueElement : valueElements )
                 {

+ 2 - 2
server/src/main/java/password/pwm/config/value/StringValue.java

@@ -23,7 +23,7 @@ package password.pwm.config.value;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingFlag;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.util.java.JsonUtil;
@@ -64,7 +64,7 @@ public class StringValue extends AbstractValue implements StoredValue
 
             public StringValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
-                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final String value = valueElement.map( XmlElement::getText ).orElse( "" );
                 return new StringValue( value );
             }

+ 2 - 2
server/src/main/java/password/pwm/config/value/UserPermissionValue.java

@@ -24,7 +24,7 @@ import com.google.gson.reflect.TypeToken;
 import org.apache.commons.lang3.StringUtils;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.PwmOperationalException;
@@ -78,7 +78,7 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
                     throws PwmOperationalException
             {
                 final boolean newType = "2".equals(
-                        settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION ) );
+                        settingElement.getAttributeValue( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION ) );
                 final List<XmlElement> valueElements = settingElement.getChildren( "value" );
                 final List<UserPermission> values = new ArrayList<>();
                 for ( final XmlElement loopValueElement : valueElements )

+ 14 - 5
server/src/main/java/password/pwm/config/value/VerificationMethodValue.java

@@ -24,7 +24,7 @@ import lombok.Value;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.option.IdentityVerificationMethod;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.i18n.Display;
@@ -38,6 +38,7 @@ import password.pwm.util.secure.PwmSecurityKey;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -101,14 +102,22 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
 
     public VerificationMethodValue( final VerificationMethodSettings value )
     {
-        this.value = value;
+        this.value = normalizeSettings( normalizeSettings( value ) );
+    }
+
+    private static VerificationMethodSettings normalizeSettings( final VerificationMethodSettings input )
+    {
+        final Map<IdentityVerificationMethod, VerificationMethodValue.VerificationMethodSetting> tempMap = new HashMap<>( input.getMethodSettings() );
+
         for ( final IdentityVerificationMethod recoveryVerificationMethods : IdentityVerificationMethod.availableValues() )
         {
-            if ( !value.methodSettings.containsKey( recoveryVerificationMethods ) )
+            if ( !tempMap.containsKey( recoveryVerificationMethods ) )
             {
-                value.methodSettings.put( recoveryVerificationMethods, new VerificationMethodSetting( EnabledState.disabled ) );
+                tempMap.put( recoveryVerificationMethods, new VerificationMethodSetting( EnabledState.disabled ) );
             }
         }
+
+        return new VerificationMethodSettings( tempMap, input.getMinOptionalRequired() );
     }
 
     public static StoredValueFactory factory( )
@@ -131,7 +140,7 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
             public VerificationMethodValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
                     throws PwmOperationalException
             {
-                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 if ( valueElement.isPresent() )
                 {
                     final String inputStr = valueElement.get().getText();

+ 2 - 2
server/src/main/java/password/pwm/config/value/X509CertificateValue.java

@@ -23,7 +23,7 @@ package password.pwm.config.value;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
@@ -60,7 +60,7 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
             public X509CertificateValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
                 final List<X509Certificate> certificates = new ArrayList<>();
-                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 for ( final XmlElement loopValueElement : valueElements )
                 {
                     final String b64encodedStr = loopValueElement.getText();

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideUtils.java

@@ -237,7 +237,7 @@ public class ConfigGuideUtils
             {
                 try
                 {
-                    final StoredConfiguration storedConfig = StoredConfigurationFactory.fromXml( uploadedFile );
+                    final StoredConfiguration storedConfig = StoredConfigurationFactory.input( uploadedFile );
                     final List<String> configErrors = StoredConfigurationUtil.validateValues( storedConfig );
                     if ( !JavaHelper.isEmpty( configErrors ) )
                     {

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java

@@ -352,7 +352,7 @@ public class ConfigManagerServlet extends AbstractPwmServlet
             final OutputStream responseWriter = resp.getOutputStream();
             resp.setHeader( HttpHeader.ContentDisposition, "attachment;filename=" + PwmConstants.DEFAULT_CONFIG_FILE_FILENAME );
             resp.setContentType( HttpContentType.xml );
-            StoredConfigurationFactory.toXml( storedConfiguration, responseWriter );
+            StoredConfigurationFactory.output( storedConfiguration, responseWriter );
             responseWriter.close();
         }
         catch ( final Exception e )

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -299,7 +299,7 @@ public class DebugItemGenerator
             final StoredConfigurationFactory.OutputSettings outputSettings = StoredConfigurationFactory.OutputSettings.builder()
                     .mode( StoredConfigurationFactory.OutputSettings.SecureOutputMode.STRIPPED )
                     .build();
-            StoredConfigurationFactory.toXml( storedConfiguration, byteArrayOutputStream, outputSettings );
+            StoredConfigurationFactory.output( storedConfiguration, byteArrayOutputStream, outputSettings );
             outputStream.write( byteArrayOutputStream.toByteArray() );        }
     }
 

+ 1 - 1
server/src/main/java/password/pwm/util/cli/commands/ConfigNewCommand.java

@@ -39,7 +39,7 @@ public class ConfigNewCommand extends AbstractCliCommand
 
         try ( FileOutputStream fileOutputStream = new FileOutputStream( outputFile, false ) )
         {
-            StoredConfigurationFactory.toXml( storedConfiguration, fileOutputStream );
+            StoredConfigurationFactory.output( storedConfiguration, fileOutputStream );
         }
         out( "success: created new configuration" );
     }

+ 1 - 1
server/src/main/java/password/pwm/util/cli/commands/ImportPropertyConfigCommand.java

@@ -57,7 +57,7 @@ public class ImportPropertyConfigCommand extends AbstractCliCommand
 
             try ( OutputStream outputStream = new FileOutputStream( configFile ) )
             {
-                StoredConfigurationFactory.toXml( storedConfiguration,  outputStream );
+                StoredConfigurationFactory.output( storedConfiguration,  outputStream );
                 out( "output configuration file " + configFile.getAbsolutePath() );
             }
         }

+ 16 - 0
server/src/main/java/password/pwm/util/java/JavaHelper.java

@@ -30,6 +30,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.http.ContextManager;
 import password.pwm.util.logging.PwmLogger;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -271,6 +272,21 @@ public class JavaHelper
         return IOUtils.copyLarge( input, output, 0, -1, buffer );
     }
 
+    public static String copy( final InputStream input )
+            throws IOException
+    {
+        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        JavaHelper.copy( input, byteArrayOutputStream );
+        return new String( byteArrayOutputStream.toByteArray(), PwmConstants.DEFAULT_CHARSET );
+    }
+
+    public static void copy( final String input, final OutputStream output )
+            throws IOException
+    {
+        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( input.getBytes( PwmConstants.DEFAULT_CHARSET ) );
+        JavaHelper.copy( byteArrayInputStream, output );
+    }
+
     public static long copyWhilePredicate(
             final InputStream input,
             final OutputStream output,

+ 2 - 2
server/src/test/java/password/pwm/config/PwmSettingTest.java

@@ -26,7 +26,7 @@ import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
-import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.StoredConfigXmlSerializer;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.StoredValueEncoder;
 import password.pwm.error.PwmOperationalException;
@@ -68,7 +68,7 @@ public class PwmSettingTest
                 storedValue.toNativeObject();
                 storedValue.toDebugString( PwmConstants.DEFAULT_LOCALE );
                 storedValue.toDebugJsonObject( PwmConstants.DEFAULT_LOCALE );
-                storedValue.toXmlValues( StoredConfigXmlConstants.XML_ELEMENT_VALUE, outputSettings );
+                storedValue.toXmlValues( StoredConfigXmlSerializer.StoredConfigXmlConstants.XML_ELEMENT_VALUE, outputSettings );
                 storedValue.validateValue( pwmSetting );
                 Assert.assertNotNull( storedValue.valueHash() );
             }

+ 1 - 1
server/src/test/java/password/pwm/config/stored/ConfigurationCleanerTest.java

@@ -51,7 +51,7 @@ public class ConfigurationCleanerTest
 
         try ( InputStream xmlFile = ConfigurationCleanerTest.class.getResourceAsStream( "ConfigurationCleanerTest.xml" ); )
         {
-            final StoredConfiguration storedConfiguration = StoredConfigurationFactory.fromXml( xmlFile );
+            final StoredConfiguration storedConfiguration = StoredConfigurationFactory.input( xmlFile );
             configuration = new Configuration( storedConfiguration );
         }
     }