Ver Fonte

Merge branch 'master' into patch-1

Jason Rivard há 5 anos atrás
pai
commit
333395e752
100 ficheiros alterados com 3671 adições e 3803 exclusões
  1. 6 2
      build/checkstyle.xml
  2. 7 7
      client/package.json
  3. 3 3
      data-service/src/main/java/password/pwm/receiver/FtpDataIngestor.java
  4. 2 2
      data-service/src/main/java/password/pwm/receiver/PwmReceiverApp.java
  5. 1 1
      data-service/src/main/java/password/pwm/receiver/Storage.java
  6. 2 2
      data-service/src/main/java/password/pwm/receiver/TelemetryRestReceiver.java
  7. 1 1
      onejar/pom.xml
  8. 5 5
      onejar/src/main/java/password/pwm/onejar/ArgumentParser.java
  9. 3 3
      onejar/src/main/java/password/pwm/onejar/OnejarMain.java
  10. 3 3
      onejar/src/main/java/password/pwm/onejar/TomcatOnejarRunner.java
  11. 5 5
      pom.xml
  12. 6 6
      pwm-cr/src/main/java/password/pwm/cr/ChaiXmlResponseSetSerializer.java
  13. 1 0
      pwm-cr/src/main/java/password/pwm/cr/CrUtils.java
  14. 2 2
      pwm-cr/src/main/java/password/pwm/cr/hash/HashFactory.java
  15. 1 1
      pwm-cr/src/main/java/password/pwm/cr/hash/PBKDF2HashMachine.java
  16. 3 2
      pwm-cr/src/main/java/password/pwm/cr/hash/TypicalHashMachine.java
  17. 1 0
      pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSet1Test.java
  18. 1 1
      pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSetReaderTest.java
  19. 1 1
      rest-test-service/src/main/java/password/pwm/resttest/RestTestUtilities.java
  20. 1 0
      server/pom.xml
  21. 19 3
      server/src/main/java/password/pwm/PwmAboutProperty.java
  22. 51 42
      server/src/main/java/password/pwm/PwmApplication.java
  23. 2 2
      server/src/main/java/password/pwm/PwmApplicationMode.java
  24. 3 6
      server/src/main/java/password/pwm/PwmConstants.java
  25. 10 9
      server/src/main/java/password/pwm/PwmEnvironment.java
  26. 5 19
      server/src/main/java/password/pwm/bean/PrivateKeyCertificate.java
  27. 3 3
      server/src/main/java/password/pwm/bean/UserIdentity.java
  28. 64 94
      server/src/main/java/password/pwm/config/Configuration.java
  29. 228 232
      server/src/main/java/password/pwm/config/PwmSetting.java
  30. 4 3
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  31. 1 0
      server/src/main/java/password/pwm/config/PwmSettingFlag.java
  32. 2 1
      server/src/main/java/password/pwm/config/PwmSettingProperty.java
  33. 1 1
      server/src/main/java/password/pwm/config/PwmSettingSyntax.java
  34. 39 32
      server/src/main/java/password/pwm/config/PwmSettingXml.java
  35. 2 2
      server/src/main/java/password/pwm/config/SettingUIFunction.java
  36. 3 5
      server/src/main/java/password/pwm/config/StoredValue.java
  37. 15 9
      server/src/main/java/password/pwm/config/function/AbstractUriCertImportFunction.java
  38. 13 7
      server/src/main/java/password/pwm/config/function/ActionCertImportFunction.java
  39. 4 4
      server/src/main/java/password/pwm/config/function/LdapCertImportFunction.java
  40. 7 5
      server/src/main/java/password/pwm/config/function/OAuthCertImportFunction.java
  41. 12 11
      server/src/main/java/password/pwm/config/function/RemoteWebServiceCertImportFunction.java
  42. 6 8
      server/src/main/java/password/pwm/config/function/SMSGatewayCertImportFunction.java
  43. 4 4
      server/src/main/java/password/pwm/config/function/SmtpCertImportFunction.java
  44. 7 7
      server/src/main/java/password/pwm/config/function/SyslogCertImportFunction.java
  45. 7 6
      server/src/main/java/password/pwm/config/function/UserMatchViewerFunction.java
  46. 3 3
      server/src/main/java/password/pwm/config/profile/ChallengeProfile.java
  47. 1 1
      server/src/main/java/password/pwm/config/profile/LdapProfile.java
  48. 1 1
      server/src/main/java/password/pwm/config/profile/NewUserProfile.java
  49. 8 5
      server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  50. 2 2
      server/src/main/java/password/pwm/config/profile/PwmPasswordRule.java
  51. 0 150
      server/src/main/java/password/pwm/config/stored/ConfigChangeLogImpl.java
  52. 2 15
      server/src/main/java/password/pwm/config/stored/ConfigRestartRequirement.java
  53. 258 200
      server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java
  54. 2 2
      server/src/main/java/password/pwm/config/stored/ConfigurationProperty.java
  55. 138 63
      server/src/main/java/password/pwm/config/stored/ConfigurationReader.java
  56. 70 0
      server/src/main/java/password/pwm/config/stored/StoredConfigData.java
  57. 235 0
      server/src/main/java/password/pwm/config/stored/StoredConfigItemKey.java
  58. 0 67
      server/src/main/java/password/pwm/config/stored/StoredConfigReferenceBean.java
  59. 50 0
      server/src/main/java/password/pwm/config/stored/StoredConfigXmlConstants.java
  60. 18 51
      server/src/main/java/password/pwm/config/stored/StoredConfiguration.java
  61. 671 3
      server/src/main/java/password/pwm/config/stored/StoredConfigurationFactory.java
  62. 124 1418
      server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  63. 291 0
      server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java
  64. 348 37
      server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java
  65. 11 15
      server/src/main/java/password/pwm/config/stored/XmlOutputProcessData.java
  66. 0 99
      server/src/main/java/password/pwm/config/stored/ng/NGStorageEngineImpl.java
  67. 0 202
      server/src/main/java/password/pwm/config/stored/ng/NGStoredConfiguration.java
  68. 0 253
      server/src/main/java/password/pwm/config/stored/ng/NGStoredConfigurationFactory.java
  69. 31 72
      server/src/main/java/password/pwm/config/value/AbstractValue.java
  70. 56 28
      server/src/main/java/password/pwm/config/value/ActionValue.java
  71. 13 13
      server/src/main/java/password/pwm/config/value/BooleanValue.java
  72. 12 6
      server/src/main/java/password/pwm/config/value/ChallengeValue.java
  73. 5 4
      server/src/main/java/password/pwm/config/value/CustomLinkValue.java
  74. 4 3
      server/src/main/java/password/pwm/config/value/EmailValue.java
  75. 50 101
      server/src/main/java/password/pwm/config/value/FileValue.java
  76. 7 14
      server/src/main/java/password/pwm/config/value/FormValue.java
  77. 4 3
      server/src/main/java/password/pwm/config/value/LocalizedStringArrayValue.java
  78. 7 5
      server/src/main/java/password/pwm/config/value/LocalizedStringValue.java
  79. 32 32
      server/src/main/java/password/pwm/config/value/NamedSecretValue.java
  80. 4 3
      server/src/main/java/password/pwm/config/value/NumericArrayValue.java
  81. 15 5
      server/src/main/java/password/pwm/config/value/NumericValue.java
  82. 4 3
      server/src/main/java/password/pwm/config/value/OptionListValue.java
  83. 54 48
      server/src/main/java/password/pwm/config/value/PasswordValue.java
  84. 34 19
      server/src/main/java/password/pwm/config/value/PrivateKeyValue.java
  85. 29 12
      server/src/main/java/password/pwm/config/value/RemoteWebServiceValue.java
  86. 268 0
      server/src/main/java/password/pwm/config/value/StoredValueEncoder.java
  87. 6 4
      server/src/main/java/password/pwm/config/value/StringArrayValue.java
  88. 7 3
      server/src/main/java/password/pwm/config/value/StringValue.java
  89. 13 9
      server/src/main/java/password/pwm/config/value/UserPermissionValue.java
  90. 5 5
      server/src/main/java/password/pwm/config/value/ValueFactory.java
  91. 19 11
      server/src/main/java/password/pwm/config/value/VerificationMethodValue.java
  92. 13 8
      server/src/main/java/password/pwm/config/value/X509CertificateValue.java
  93. 144 0
      server/src/main/java/password/pwm/config/value/data/ActionConfiguration.java
  94. 0 180
      server/src/main/java/password/pwm/config/value/data/ActionConfigurationOldVersion1.java
  95. 4 15
      server/src/main/java/password/pwm/config/value/data/ChallengeItemConfiguration.java
  96. 3 3
      server/src/main/java/password/pwm/config/value/data/CustomLinkConfiguration.java
  97. 18 31
      server/src/main/java/password/pwm/config/value/data/FormConfiguration.java
  98. 2 4
      server/src/main/java/password/pwm/config/value/data/NamedSecretData.java
  99. 4 4
      server/src/main/java/password/pwm/config/value/data/RemoteWebServiceConfiguration.java
  100. 4 6
      server/src/main/java/password/pwm/config/value/data/ShortcutItem.java

+ 6 - 2
build/checkstyle.xml

@@ -269,12 +269,16 @@
         <module name="FallThrough"/>
         <module name="EqualsHashCode"/>
         <module name="ArrayTrailingCommaCheck"/>
-        <module name="FinalLocalVariable"/>
+        <module name="FinalLocalVariable">
+            <property name="validateEnhancedForLoopVariable" value="true"/>
+        </module>
         <module name="MissingSwitchDefault"/>
         <module name="ModifiedControlVariable"/>
         <module name="MultipleVariableDeclarations"/>
         <module name="OneStatementPerLine"/>
-        <module name="FinalParameters"/>
+        <module name="FinalParameters">
+            <property name="tokens" value="METHOD_DEF,CTOR_DEF,LITERAL_CATCH"/>
+        </module>
         <module name="ParameterAssignment"/>
         <module name="SimplifyBooleanReturn"/>
         <module name="StringLiteralEquality"/>

+ 7 - 7
client/package.json

@@ -21,9 +21,9 @@
         "@microfocus/ng-ias": "1.0.1",
         "@microfocus/ux-ias": "1.1.2",
         "@uirouter/angularjs": "1.0.15",
-        "angular": "1.7.8",
+        "angular": "^1.7.9",
         "angular-aria": "1.7.8",
-        "angular-sanitize": "1.7.8",
+        "angular-sanitize": "1.7.9",
         "angular-translate": "2.18.1",
         "core-js": "3.2.1",
         "textangular": "1.5.16"
@@ -38,7 +38,7 @@
         "angular-mocks": "1.6.9",
         "autoprefixer": "8.1.0",
         "copy-webpack-plugin": "4.5.1",
-        "css-loader": "^3.2.0",
+        "css-loader": "^3.2.1",
         "file-loader": "1.1.11",
         "html-loader": "0.5.5",
         "html-webpack-plugin": "3.0.6",
@@ -46,10 +46,10 @@
         "imports-loader": "0.8.0",
         "jasmine": "3.2.0",
         "jasmine-core": "3.2.1",
-        "jshint": "^2.10.2",
+        "jshint": "^2.10.3",
         "jshint-loader": "0.8.4",
         "json-loader": "0.5.7",
-        "karma": "^4.3.0",
+        "karma": "^4.4.1",
         "karma-chrome-launcher": "2.2.0",
         "karma-jasmine": "1.1.2",
         "karma-jasmine-html-reporter": "1.3.1",
@@ -59,7 +59,7 @@
         "karma-webpack": "3.0.5",
         "moment": "2.21.0",
         "ngtemplate-loader": "2.0.1",
-        "node-sass": "^4.12.0",
+        "node-sass": "^4.13.0",
         "phantomjs": "2.1.7",
         "phantomjs-prebuilt": "2.1.16",
         "postcss-loader": "2.1.1",
@@ -76,7 +76,7 @@
         "uglifyjs-webpack-plugin": "1.2.3",
         "url-loader": "1.0.1",
         "webpack": "4.1.1",
-        "webpack-cli": "^3.3.8",
+        "webpack-cli": "^3.3.10",
         "webpack-dev-server": "3.1.14",
         "webpack-merge": "4.1.2",
         "write-file-webpack-plugin": "4.2.0"

+ 3 - 3
data-service/src/main/java/password/pwm/receiver/FtpDataIngestor.java

@@ -74,7 +74,7 @@ class FtpDataIngestor
                     {
                         readFile( ftpClient, fileName, storage );
                     }
-                    catch ( Exception e )
+                    catch ( final Exception e )
                     {
                         app.getStatus().setLastFtpIngest( Instant.now() );
                         final String msg = "error while reading ftp file '" + fileName + "': " + e.getMessage();
@@ -93,7 +93,7 @@ class FtpDataIngestor
             app.getStatus().setLastFtpIngest( Instant.now() );
             app.getStatus().setLastFtpFilesRead( files.size() );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             app.getStatus().setLastFtpIngest( Instant.now() );
             app.getStatus().setLastFtpStatus( "error during ftp scan: " + e.getMessage() );
@@ -130,7 +130,7 @@ class FtpDataIngestor
                 storage.store( bean );
             }
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             final String msg = "error reading ftp file '" + fileName + "', error: " + e.getMessage();
             LOGGER.info( msg );

+ 2 - 2
data-service/src/main/java/password/pwm/receiver/PwmReceiverApp.java

@@ -53,7 +53,7 @@ public class PwmReceiverApp
         {
             settings = Settings.readFromFile( propsFile );
         }
-        catch ( IOException e )
+        catch ( final IOException e )
         {
             final String errorMsg = "can't read configuration: " + JavaHelper.readHostileExceptionMessage( e );
             status.setErrorState( errorMsg );
@@ -65,7 +65,7 @@ public class PwmReceiverApp
         {
             storage = new Storage( settings );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             final String errorMsg = "can't start storage system: " + JavaHelper.readHostileExceptionMessage( e );
             status.setErrorState( errorMsg );

+ 1 - 1
data-service/src/main/java/password/pwm/receiver/Storage.java

@@ -173,7 +173,7 @@ public class Storage
                 }
                 nextValue = string;
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 e.printStackTrace();
                 throw e;

+ 2 - 2
data-service/src/main/java/password/pwm/receiver/TelemetryRestReceiver.java

@@ -58,11 +58,11 @@ public class TelemetryRestReceiver extends HttpServlet
             stoage.store( telemetryPublishBean );
             resp.getWriter().print( RestResultBean.forSuccessMessage( null, null, null, Message.Success_Unknown ).toJson() );
         }
-        catch ( PwmUnrecoverableException e )
+        catch ( final PwmUnrecoverableException e )
         {
             resp.getWriter().print( RestResultBean.fromError( e.getErrorInformation() ).toJson() );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             final RestResultBean restResultBean = RestResultBean.fromError( new ErrorInformation( PwmError.ERROR_INTERNAL, e.getMessage() ) );
             resp.getWriter().print( restResultBean.toJson() );

+ 1 - 1
onejar/pom.xml

@@ -17,7 +17,7 @@
 
     <properties>
         <project.root.basedir>${project.basedir}/..</project.root.basedir>
-        <tomcat.version>9.0.27</tomcat.version>
+        <tomcat.version>9.0.29</tomcat.version>
     </properties>
 
     <build>

+ 5 - 5
onejar/src/main/java/password/pwm/onejar/ArgumentParser.java

@@ -58,7 +58,7 @@ public class ArgumentParser
             {
                 commandLine = new DefaultParser().parse( Argument.asOptions(), args );
             }
-            catch ( ParseException e )
+            catch ( final ParseException e )
             {
                 throw new ArgumentParserException( "unable to parse command line: " + e.getMessage() );
             }
@@ -100,7 +100,7 @@ public class ArgumentParser
                 {
                     onejarConfig = makeTomcatConfig( argumentMap );
                 }
-                catch ( IOException e )
+                catch ( final IOException e )
                 {
                     throw new ArgumentParserException( "error while reading input: " + e.getMessage() );
                 }
@@ -118,7 +118,7 @@ public class ArgumentParser
         {
             props.load( is );
         }
-        catch ( IOException e )
+        catch ( final IOException e )
         {
             throw new ArgumentParserException( "unable to read properties input file: " + e.getMessage() );
         }
@@ -204,7 +204,7 @@ public class ArgumentParser
                     port = Integer.parseInt( argumentMap.get( Argument.port ) );
                     onejarConfig.port( port );
                 }
-                catch ( NumberFormatException e )
+                catch ( final NumberFormatException e )
                 {
                     final String msg = Argument.port.name() + " argument must be numeric";
                     System.out.println( msg );
@@ -228,7 +228,7 @@ public class ArgumentParser
                 final ServerSocket socket = new ServerSocket( port, 100, InetAddress.getByName( localAddress ) );
                 socket.close();
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 throw new ArgumentParserException( "port or address conflict: " + e.getMessage() );
             }

+ 3 - 3
onejar/src/main/java/password/pwm/onejar/OnejarMain.java

@@ -52,7 +52,7 @@ public class OnejarMain
         {
             onejarConfig = argumentParser.parseArguments( args );
         }
-        catch ( ArgumentParserException | OnejarException e )
+        catch ( final ArgumentParserException | OnejarException e )
         {
             output( "error parsing command line: " + e.getMessage() );
         }
@@ -91,7 +91,7 @@ public class OnejarMain
 
             mainMethod.invoke( null, ( Object ) arguments );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             e.printStackTrace( );
         }
@@ -111,7 +111,7 @@ public class OnejarMain
                 runner.startTomcat( onejarConfig );
 
             }
-            catch ( OnejarException | ServletException | IOException e )
+            catch ( final OnejarException | ServletException | IOException e )
             {
                 out( "error starting tomcat: " + e.getMessage() );
             }

+ 3 - 3
onejar/src/main/java/password/pwm/onejar/TomcatOnejarRunner.java

@@ -70,7 +70,7 @@ public class TomcatOnejarRunner
             tlsProperties = this.executeOnejarHelper( onejarConfig );
             out( "keystore generated" );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             throw new OnejarException( "error generating keystore: " + e.getMessage() );
         }
@@ -113,7 +113,7 @@ public class TomcatOnejarRunner
             tomcat.start();
             out( "tomcat started in " + Duration.between( Instant.now(), startTime ).toString() );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             throw new OnejarException( "unable to start tomcat: " + e.getMessage() );
         }
@@ -199,7 +199,7 @@ public class TomcatOnejarRunner
             return attr.getValue( "Implementation-Version-Display" )
                     + "  [" + ServerInfo.getServerInfo() + "]";
         }
-        catch ( IOException e )
+        catch ( final IOException e )
         {
             throw new OnejarException( "error reading internal version info: " + e.getMessage() );
         }

+ 5 - 5
pom.xml

@@ -166,7 +166,7 @@
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
-                        <version>8.25</version>
+                        <version>8.27</version>
                     </dependency>
                 </dependencies>
                 <executions>
@@ -308,13 +308,13 @@
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
-            <version>3.1.0</version>
+            <version>3.2.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
-            <version>3.13.2</version>
+            <version>3.14.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -332,13 +332,13 @@
         <dependency>
             <groupId>org.openjdk.jmh</groupId>
             <artifactId>jmh-core</artifactId>
-            <version>1.21</version>
+            <version>1.22</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.openjdk.jmh</groupId>
             <artifactId>jmh-generator-annprocess</artifactId>
-            <version>1.21</version>
+            <version>1.22</version>
             <scope>test</scope>
         </dependency>
 

+ 6 - 6
pwm-cr/src/main/java/password/pwm/cr/ChaiXmlResponseSetSerializer.java

@@ -142,7 +142,7 @@ public class ChaiXmlResponseSetSerializer
                     {
                         timestamp = CrUtils.parseDateString( timeStr );
                     }
-                    catch ( ParseException e )
+                    catch ( final ParseException e )
                     {
                         throw new IllegalArgumentException( "unexpected error attempting to parse timestamp: " + e.getMessage() );
                     }
@@ -173,14 +173,14 @@ public class ChaiXmlResponseSetSerializer
                                 break;
 
                             default:
-                                throw new IllegalStateException( "unknown response type '" + type + "'" );
+                                throw new IllegalStateException( "unknown response type '" + type + '\'' );
 
                         }
                     }
                 }
             }
         }
-        catch ( JDOMException | IOException | NullPointerException e )
+        catch ( final JDOMException | IOException | NullPointerException e )
         {
             throw new IllegalArgumentException( "error parsing stored response record: " + e.getMessage() );
         }
@@ -228,7 +228,7 @@ public class ChaiXmlResponseSetSerializer
                 return XML_NODE_HELPDESK_RESPONSE;
 
             default:
-                throw new IllegalArgumentException( "unknown type '" + type + "'" );
+                throw new IllegalArgumentException( "unknown type '" + type + '\'' );
         }
     }
 
@@ -279,7 +279,7 @@ public class ChaiXmlResponseSetSerializer
         {
             saltCount = Integer.parseInt( hashCount );
         }
-        catch ( NumberFormatException e )
+        catch ( final NumberFormatException e )
         {
             /* noop */
         }
@@ -306,7 +306,7 @@ public class ChaiXmlResponseSetSerializer
             final byte[] hashedBytes = md.digest( questionText.getBytes( StandardCharsets.UTF_8 ) );
             return net.iharder.Base64.encodeBytes( hashedBytes, Base64.URL_SAFE );
         }
-        catch ( NoSuchAlgorithmException | IOException e )
+        catch ( final NoSuchAlgorithmException | IOException e )
         {
             throw new IllegalStateException( "unable to load SHA1 message digest algorithm: " + e.getMessage() );
         }

+ 1 - 0
pwm-cr/src/main/java/password/pwm/cr/CrUtils.java

@@ -26,6 +26,7 @@ import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.util.TimeZone;
 
+@SuppressWarnings( "checkstyle:MultipleStringLiterals" )
 public class CrUtils
 {
     static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss Z";

+ 2 - 2
pwm-cr/src/main/java/password/pwm/cr/hash/HashFactory.java

@@ -54,7 +54,7 @@ public class HashFactory
         {
             alg = ResponseHashAlgorithm.valueOf( algName );
         }
-        catch ( IllegalArgumentException e )
+        catch ( final IllegalArgumentException e )
         {
             throw new IllegalArgumentException( "unknown format type '" + algName + "'" );
         }
@@ -64,7 +64,7 @@ public class HashFactory
         {
             responseHashMachine = ( ResponseHashMachineSpi ) algClass.newInstance();
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             throw new IllegalStateException( "unexpected error instantiating response hash machine spi class: " + e.getMessage() );
         }

+ 1 - 1
pwm-cr/src/main/java/password/pwm/cr/hash/PBKDF2HashMachine.java

@@ -115,7 +115,7 @@ class PBKDF2HashMachine extends AbstractHashMachine implements ResponseHashMachi
             final byte[] hash = skf.generateSecret( spec ).getEncoded();
             return Base64.encodeBytes( hash );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             throw new IllegalStateException( "unable to perform PBKDF2 hashing operation: " + e.getMessage() );
         }

+ 3 - 2
pwm-cr/src/main/java/password/pwm/cr/hash/TypicalHashMachine.java

@@ -30,6 +30,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
+@SuppressWarnings( "checkstyle:MultipleStringLiterals" )
 public class TypicalHashMachine extends AbstractHashMachine implements ResponseHashMachineSpi
 {
 
@@ -106,7 +107,7 @@ public class TypicalHashMachine extends AbstractHashMachine implements ResponseH
         {
             md = MessageDigest.getInstance( algorithm );
         }
-        catch ( NoSuchAlgorithmException e )
+        catch ( final NoSuchAlgorithmException e )
         {
             throw new IllegalStateException( "unable to load " + algorithm + " message digest algorithm: " + e.getMessage() );
         }
@@ -117,7 +118,7 @@ public class TypicalHashMachine extends AbstractHashMachine implements ResponseH
         {
             hashedBytes = input.getBytes( "UTF-8" );
         }
-        catch ( UnsupportedEncodingException e )
+        catch ( final UnsupportedEncodingException e )
         {
             throw new IllegalStateException( "unsupported UTF8 byte encoding: " + e.getMessage() );
         }

+ 1 - 0
pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSet1Test.java

@@ -33,6 +33,7 @@ import java.io.InputStreamReader;
 import java.io.Reader;
 import java.nio.charset.Charset;
 
+@SuppressWarnings( "checkstyle:MultipleStringLiterals" )
 public class ChaiXmlResponseSet1Test
 {
 

+ 1 - 1
pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSetReaderTest.java

@@ -25,7 +25,7 @@ import password.pwm.cr.api.ChallengeItemPolicy;
 import password.pwm.cr.api.QuestionSource;
 import password.pwm.cr.api.ResponseLevel;
 
-
+@SuppressWarnings( "checkstyle:MultipleStringLiterals" )
 public class ChaiXmlResponseSetReaderTest
 {
 

+ 1 - 1
rest-test-service/src/main/java/password/pwm/resttest/RestTestUtilities.java

@@ -44,7 +44,7 @@ public class RestTestUtilities
         {
             IOUtils.copy( readerStream, stringWriter );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             final String errorMsg = "error reading request body stream: " + e.getMessage();
             throw new IOException( errorMsg );

+ 1 - 0
server/pom.xml

@@ -52,6 +52,7 @@
                         </goals>
                         <phase>test</phase>
                         <configuration>
+                            <trimStackTrace>false</trimStackTrace>
                             <skipTests>${skipTests}</skipTests>
                             <excludes>
                                 <exclude>**/ExtendedTest*.java</exclude>

+ 19 - 3
server/src/main/java/password/pwm/PwmAboutProperty.java

@@ -28,8 +28,10 @@ import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
+import javax.net.ssl.SSLContext;
 import java.lang.management.ManagementFactory;
 import java.nio.charset.Charset;
+import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.Date;
@@ -69,6 +71,7 @@ public enum PwmAboutProperty
     app_secureBlockAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultBlockAlgorithm().getLabel() ),
     app_secureHashAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultHashAlgorithm().toString() ),
     app_ldapProfileCount( null, pwmApplication -> Integer.toString( pwmApplication.getConfig().getLdapProfiles().size() ) ),
+    app_ldapConnectionCount( null, pwmApplication -> Integer.toString( pwmApplication.getLdapConnectionService().connectionCount() ) ),
 
     build_Time( "Build Time", pwmApplication -> PwmConstants.BUILD_TIME ),
     build_Number( "Build Number", pwmApplication -> PwmConstants.BUILD_NUMBER ),
@@ -91,9 +94,10 @@ public enum PwmAboutProperty
     java_osName( "Operating System Name", pwmApplication -> System.getProperty( "os.name" ) ),
     java_osVersion( "Operating System Version", pwmApplication -> System.getProperty( "os.version" ) ),
     java_osArch( "Operating System Architecture", pwmApplication -> System.getProperty( "os.arch" ) ),
-    java_randomAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().pwmRandom().getAlgorithm() ),
-    java_defaultCharset( null, pwmApplication -> Charset.defaultCharset().name() ),
+    java_randomAlgorithm( "Random Algorithm", pwmApplication -> pwmApplication.getSecureService().pwmRandom().getAlgorithm() ),
+    java_defaultCharset( "Default Character Set", pwmApplication -> Charset.defaultCharset().name() ),
     java_appServerInfo( "Java AppServer Info", pwmApplication -> pwmApplication.getPwmEnvironment().getContextManager().getServerInfo() ),
+    java_sslVersions( "Java SSL Versions", pwmApplication ->  readSslVersions() ),
 
     database_driverName( null,
             pwmApplication -> pwmApplication.getDatabaseService().getConnectionDebugProperties().get( DatabaseService.DatabaseAboutProperty.driverName ) ),
@@ -136,7 +140,7 @@ public enum PwmAboutProperty
                     final String value = valueProvider.value( pwmApplication );
                     aboutMap.put( pwmAboutProperty.name(), value == null ? "" : value );
                 }
-                catch ( Throwable t )
+                catch ( final Throwable t )
                 {
                     aboutMap.put( pwmAboutProperty.name(), LocaleHelper.getLocalizedMessage( null, Display.Value_NotApplicable, null ) );
                     LOGGER.trace( () -> "error generating about value for '" + pwmAboutProperty.name() + "', error: " + t.getMessage() );
@@ -188,4 +192,16 @@ public enum PwmAboutProperty
         }
         return Collections.unmodifiableMap( outputProps );
     }
+
+    private static String readSslVersions()
+    {
+        try
+        {
+            return String.join( " ", SSLContext.getDefault().getSupportedSSLParameters().getProtocols() );
+        }
+        catch ( final NoSuchAlgorithmException e )
+        {
+            return "";
+        }
+    }
 }

+ 51 - 42
server/src/main/java/password/pwm/PwmApplication.java

@@ -28,6 +28,8 @@ import password.pwm.bean.SmsItemBean;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
@@ -101,8 +103,8 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
-
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
 
 /**
  * A repository for objects common to the servlet context.  A singleton
@@ -177,7 +179,7 @@ public class PwmApplication
         {
             initialize();
         }
-        catch ( PwmUnrecoverableException e )
+        catch ( final PwmUnrecoverableException e )
         {
             LOGGER.fatal( e.getMessage() );
             throw e;
@@ -242,7 +244,7 @@ public class PwmApplication
             {
                 FileSystemUtility.deleteDirectoryContents( tempFileDirectory );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_STARTUP_ERROR,
                         "unable to clear temp file directory '" + tempFileDirectory.getAbsolutePath() + "', error: " + e.getMessage()
@@ -305,34 +307,16 @@ public class PwmApplication
     {
         final Instant startTime = Instant.now();
 
-        pwmEnvironment.getConfig().outputToLog();
-
-        // detect if config has been modified since previous startup
         try
         {
-            final String previousHash = readAppAttribute( AppAttribute.CONFIG_HASH, String.class );
-            final String currentHash = pwmEnvironment.getConfig().configurationHash();
-            if ( previousHash == null || !previousHash.equals( currentHash ) )
-            {
-                writeAppAttribute( AppAttribute.CONFIG_HASH, currentHash );
-                LOGGER.warn( "configuration checksum does not match previously seen checksum, configuration has been modified since last startup" );
-                if ( this.getAuditManager() != null )
-                {
-                    final String modifyMessage = "configuration was modified directly (not using ConfigEditor UI)";
-                    this.getAuditManager().submit( new AuditRecordFactory( this ).createUserAuditRecord(
-                            AuditEvent.MODIFY_CONFIGURATION,
-                            null,
-                            null,
-                            modifyMessage
-                    ) );
-                }
-            }
+            outputConfigurationToLog( this );
         }
-        catch ( Exception e )
+        catch ( final PwmException e )
         {
-            LOGGER.debug( () -> "unable to detect if configuration has been modified since previous startup: " + e.getMessage() );
+            LOGGER.error( "error outputting log to debug: " + e.getMessage() );
         }
 
+
         if ( this.getConfig() != null )
         {
             final Map<AppProperty, String> nonDefaultProperties = getConfig().readAllNonDefaultAppProperties();
@@ -360,7 +344,7 @@ public class PwmApplication
             );
             getAuditManager().submit( auditRecord );
         }
-        catch ( PwmException e )
+        catch ( final PwmException e )
         {
             LOGGER.warn( "unable to submit start alert event " + e.getMessage() );
         }
@@ -370,7 +354,7 @@ public class PwmApplication
             final Map<PwmAboutProperty, String> infoMap = PwmAboutProperty.makeInfoBean( this );
             LOGGER.trace( () ->  "application info: " + JsonUtil.serializeMap( infoMap ) );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             LOGGER.error( "error generating about application bean: " + e.getMessage(), e );
         }
@@ -379,7 +363,7 @@ public class PwmApplication
         {
             this.getIntruderManager().clear( RecordType.USERNAME, PwmConstants.CONFIGMANAGER_INTRUDER_USERNAME );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             LOGGER.warn( "error while clearing configmanager-intruder-username from intruder table: " + e.getMessage() );
         }
@@ -390,7 +374,7 @@ public class PwmApplication
             {
                 outputKeystore( this );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.debug( () -> "error while generating keystore output: " + e.getMessage() );
             }
@@ -399,7 +383,7 @@ public class PwmApplication
             {
                 outputTomcatConf( this );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.debug( () -> "error while generating tomcat conf output: " + e.getMessage() );
             }
@@ -411,7 +395,7 @@ public class PwmApplication
         {
             UserAgentUtils.initializeCache();
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             LOGGER.debug( () -> "error initializing UserAgentUtils: " + e.getMessage() );
         }
@@ -511,6 +495,31 @@ public class PwmApplication
         }
     }
 
+    private static void outputConfigurationToLog( final PwmApplication pwmApplication )
+            throws PwmUnrecoverableException
+    {
+        if ( !LOGGER.isEnabled( PwmLogLevel.TRACE ) )
+        {
+            return;
+        }
+
+        final StoredConfiguration storedConfiguration = pwmApplication.getConfig().getStoredConfiguration();
+        final Map<String, String> debugStrings = StoredConfigurationUtil.makeDebugMap( storedConfiguration, storedConfiguration.modifiedItems(), PwmConstants.DEFAULT_LOCALE );
+        final List<Supplier<CharSequence>> outputStrings = new ArrayList<>();
+
+        for ( final Map.Entry<String, String> entry : debugStrings.entrySet() )
+        {
+            final String spacedValue = entry.getValue().replace( "\n", "\n   " );
+            final String output = " " + entry.getKey() + "\n   " + spacedValue + "\n";
+            outputStrings.add( () -> output );
+        }
+
+        LOGGER.trace( () -> "--begin current configuration output--" );
+        outputStrings.forEach( LOGGER::trace );
+        LOGGER.trace( () -> "--end current configuration output--" );
+    }
+
+
     public String getInstanceID( )
     {
         return instanceID;
@@ -534,7 +543,7 @@ public class PwmApplication
             final ChaiProvider proxiedProvider = getProxyChaiProvider( userIdentity.getLdapProfileID() );
             return proxiedProvider.getEntryFactory().newChaiUser( userIdentity.getUserDN() );
         }
-        catch ( ChaiUnavailableException e )
+        catch ( final ChaiUnavailableException e )
         {
             throw PwmUnrecoverableException.fromChaiException( e );
         }
@@ -689,7 +698,7 @@ public class PwmApplication
                     return Instant.ofEpochMilli( Long.parseLong( storedDateStr ) );
                 }
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.error( "error retrieving installation date from localDB: " + e.getMessage() );
             }
@@ -790,7 +799,7 @@ public class PwmApplication
         {
             smsQueue.addSmsToQueue( smsItemBean );
         }
-        catch ( PwmUnrecoverableException e )
+        catch ( final PwmUnrecoverableException e )
         {
             LOGGER.warn( "unable to add sms to queue: " + e.getMessage() );
         }
@@ -814,7 +823,7 @@ public class PwmApplication
                     getAuditManager().submit( auditRecord );
                 }
             }
-            catch ( PwmException e )
+            catch ( final PwmException e )
             {
                 LOGGER.warn( "unable to submit shutdown alert event " + e.getMessage() );
             }
@@ -830,7 +839,7 @@ public class PwmApplication
             {
                 localDBLogger.close();
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.error( "error closing localDBLogger: " + e.getMessage(), e );
             }
@@ -844,7 +853,7 @@ public class PwmApplication
                 LOGGER.trace( () -> "beginning close of LocalDB" );
                 localDB.close();
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.fatal( "error closing localDB: " + e, e );
             }
@@ -883,7 +892,7 @@ public class PwmApplication
                 final String localDBLocationSetting = pwmApplication.getConfig().readAppProperty( AppProperty.LOCALDB_LOCATION );
                 databaseDirectory = FileSystemUtility.figureFilepath( localDBLocationSetting, pwmApplication.pwmEnvironment.getApplicationPath() );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 pwmApplication.lastLocalDBFailure = new ErrorInformation( PwmError.ERROR_LOCALDB_UNAVAILABLE, "error locating configured LocalDB directory: " + e.getMessage() );
                 LOGGER.warn( pwmApplication.lastLocalDBFailure.toDebugStr() );
@@ -898,7 +907,7 @@ public class PwmApplication
                 final boolean readOnly = pwmApplication.getApplicationMode() == PwmApplicationMode.READ_ONLY;
                 return LocalDBFactory.getInstance( databaseDirectory, readOnly, pwmApplication, pwmApplication.getConfig() );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 pwmApplication.lastLocalDBFailure = new ErrorInformation( PwmError.ERROR_LOCALDB_UNAVAILABLE, "unable to initialize LocalDB: " + e.getMessage() );
                 LOGGER.warn( pwmApplication.lastLocalDBFailure.toDebugStr() );
@@ -935,7 +944,7 @@ public class PwmApplication
             final String strValue = localDB.get( LocalDB.DB.PWM_META, appAttribute.getKey() );
             return JsonUtil.deserialize( strValue, returnClass );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             LOGGER.error( "error retrieving key '" + appAttribute.getKey() + "' value from localDB: " + e.getMessage() );
         }
@@ -967,14 +976,14 @@ public class PwmApplication
                 localDB.put( LocalDB.DB.PWM_META, appAttribute.getKey(), jsonValue );
             }
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             LOGGER.error( "error retrieving key '" + appAttribute.getKey() + "' installation date from localDB: " + e.getMessage() );
             try
             {
                 localDB.remove( LocalDB.DB.PWM_META, appAttribute.getKey() );
             }
-            catch ( Exception e2 )
+            catch ( final Exception e2 )
             {
                 LOGGER.error( "error removing bogus appAttribute value for key " + appAttribute.getKey() + ", error: " + localDB );
             }

+ 2 - 2
server/src/main/java/password/pwm/PwmApplicationMode.java

@@ -39,7 +39,7 @@ public enum PwmApplicationMode
         {
             contextManager = ContextManager.getContextManager( httpServletRequest.getServletContext() );
         }
-        catch ( Throwable t )
+        catch ( final Throwable t )
         {
             return ERROR;
         }
@@ -49,7 +49,7 @@ public enum PwmApplicationMode
         {
             pwmApplication = contextManager.getPwmApplication();
         }
-        catch ( Throwable t )
+        catch ( final Throwable t )
         {
             return ERROR;
         }

+ 3 - 6
server/src/main/java/password/pwm/PwmConstants.java

@@ -23,7 +23,6 @@ package password.pwm;
 import org.apache.commons.csv.CSVFormat;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
-import password.pwm.util.secure.PwmHashAlgorithm;
 
 import java.io.InputStream;
 import java.net.URL;
@@ -101,6 +100,7 @@ public abstract class PwmConstants
 
     public static final String LDAP_AD_PASSWORD_POLICY_CONTROL_ASN = "1.2.840.113556.1.4.2066";
     public static final String PROFILE_ID_ALL = "all";
+    public static final String PROFILE_ID_DEFAULT = "default";
 
     public static final String TOKEN_KEY_PWD_CHG_DATE = "_lastPwdChange";
 
@@ -118,9 +118,6 @@ public abstract class PwmConstants
     public static final String REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE = "ForgottenPw-AvailableTokenDestCache";
     public static final String REQUEST_ATTR_PWM_APPLICATION = "PwmApplication";
 
-    public static final PwmHashAlgorithm SETTING_CHECKSUM_HASH_METHOD = PwmHashAlgorithm.SHA256;
-
-
     public static final String LOG_REMOVED_VALUE_REPLACEMENT = readPwmConstantsBundle( "log.removedValue" );
 
     public static final Collection<Locale> INCLUDED_LOCALES;
@@ -272,14 +269,14 @@ public abstract class PwmConstants
                             }
                         }
                     }
-                    catch ( Throwable t )
+                    catch ( final Throwable t )
                     {
                         System.out.println( t );
                     }
                 }
             }
         }
-        catch ( Throwable t )
+        catch ( final Throwable t )
         {
             System.out.println( t );
         }

+ 10 - 9
server/src/main/java/password/pwm/PwmEnvironment.java

@@ -74,7 +74,8 @@ public class PwmEnvironment
         AppliancePort,
         ApplianceHostnameFile,
         ApplianceTokenFile,
-        InstanceID,;
+        InstanceID,
+        InitConsoleLogLevel,;
 
         public static ApplicationParameter forString( final String input )
         {
@@ -317,7 +318,7 @@ public class PwmEnvironment
             final String rawValue = readValueFromSystem( EnvironmentParameter.applicationParamFile, contextName );
             if ( rawValue != null )
             {
-                return parseApplicationParamValueParameter( rawValue );
+                return readAppParametersFromPath( rawValue );
             }
             return Collections.emptyMap();
         }
@@ -372,7 +373,7 @@ public class PwmEnvironment
                 }
                 return Collections.unmodifiableList( returnFlags );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 //
             }
@@ -393,7 +394,7 @@ public class PwmEnvironment
             return returnFlags;
         }
 
-        public static Map<ApplicationParameter, String> parseApplicationParamValueParameter( final String input )
+        public static Map<ApplicationParameter, String> readAppParametersFromPath( final String input )
         {
             if ( input == null )
             {
@@ -405,7 +406,7 @@ public class PwmEnvironment
             {
                 propValues.load( fileInputStream );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.warn( "error reading properties file '" + input + "' specified by environment setting "
                         + EnvironmentParameter.applicationParamFile.toString() + ", error: " + e.getMessage() );
@@ -429,7 +430,7 @@ public class PwmEnvironment
                 }
                 return Collections.unmodifiableMap( returnParams );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.warn( "unable to parse jason value of " + EnvironmentParameter.applicationParamFile.toString() + ", error: " + e.getMessage() );
             }
@@ -625,7 +626,7 @@ public class PwmEnvironment
                         LOGGER.debug( () -> "unable to obtain file lock on file " + lockfile.getAbsolutePath() );
                     }
                 }
-                catch ( Exception e )
+                catch ( final Exception e )
                 {
                     LOGGER.error( "unable to obtain file lock on file " + lockfile.getAbsolutePath() + " due to error: " + e.getMessage() );
                 }
@@ -645,7 +646,7 @@ public class PwmEnvironment
                 props.store( stringWriter, comment );
                 file.write( stringWriter.getBuffer().toString().getBytes( PwmConstants.DEFAULT_CHARSET ) );
             }
-            catch ( IOException e )
+            catch ( final IOException e )
             {
                 LOGGER.error( "unable to write contents of application lock file: " + e.getMessage() );
             }
@@ -660,7 +661,7 @@ public class PwmEnvironment
                 {
                     lock.release();
                 }
-                catch ( IOException e )
+                catch ( final IOException e )
                 {
                     LOGGER.error( "error releasing file lock: " + e.getMessage() );
                 }

+ 5 - 19
server/src/main/java/password/pwm/bean/PrivateKeyCertificate.java

@@ -20,30 +20,16 @@
 
 package password.pwm.bean;
 
+import lombok.Value;
+
 import java.io.Serializable;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
-import java.util.Collections;
 import java.util.List;
 
+@Value
 public class PrivateKeyCertificate implements Serializable
 {
-    private final List<X509Certificate> certificates;
-    private final PrivateKey key;
-
-    public PrivateKeyCertificate( final List<X509Certificate> certificates, final PrivateKey key )
-    {
-        this.certificates = Collections.unmodifiableList( certificates );
-        this.key = key;
-    }
-
-    public List<X509Certificate> getCertificates( )
-    {
-        return Collections.unmodifiableList( certificates );
-    }
-
-    public PrivateKey getKey( )
-    {
-        return key;
-    }
+    private List<X509Certificate> certificates;
+    private PrivateKey key;
 }

+ 3 - 3
server/src/main/java/password/pwm/bean/UserIdentity.java

@@ -112,7 +112,7 @@ public class UserIdentity implements Serializable, Comparable
             cacheService.put( cacheKey, CachePolicy.makePolicyWithExpiration( TimeDuration.DAY ), localValue );
             return localValue;
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "unexpected error making obfuscated user key: " + e.getMessage() ) );
         }
@@ -146,7 +146,7 @@ public class UserIdentity implements Serializable, Comparable
             final String jsonValue = pwmApplication.getSecureService().decryptStringValue( input );
             return JsonUtil.deserialize( jsonValue, UserIdentity.class );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "unexpected error reversing obfuscated user key: " + e.getMessage() ) );
         }
@@ -260,7 +260,7 @@ public class UserIdentity implements Serializable, Comparable
         {
             userDN = chaiUser.readCanonicalDN();
         }
-        catch ( ChaiException e )
+        catch ( final ChaiException e )
         {
             throw PwmUnrecoverableException.fromChaiException( e );
         }

+ 64 - 94
server/src/main/java/password/pwm/config/Configuration.java

@@ -45,8 +45,8 @@ import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.config.profile.SetupOtpProfile;
 import password.pwm.config.profile.UpdateProfileProfile;
-import password.pwm.config.stored.ConfigurationProperty;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.BooleanValue;
 import password.pwm.config.value.CustomLinkValue;
@@ -70,6 +70,7 @@ import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.StringUtil;
@@ -77,8 +78,8 @@ import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmSecurityKey;
+import password.pwm.util.secure.SecureService;
 
-import java.io.Serializable;
 import java.lang.reflect.InvocationTargetException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
@@ -95,7 +96,6 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.function.Supplier;
 
 /**
  * @author Jason D. Rivard
@@ -104,13 +104,11 @@ public class Configuration implements SettingReader
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( Configuration.class );
 
-    private final StoredConfigurationImpl storedConfiguration;
+    private final StoredConfiguration storedConfiguration;
 
     private DataCache dataCache = new DataCache();
 
-    private String cashedConfigurationHash;
-
-    public Configuration( final StoredConfigurationImpl storedConfiguration )
+    public Configuration( final StoredConfiguration storedConfiguration )
     {
         this.storedConfiguration = storedConfiguration;
     }
@@ -125,28 +123,6 @@ public class Configuration implements SettingReader
         }
     }
 
-    public void outputToLog( )
-    {
-        if ( !LOGGER.isEnabled( PwmLogLevel.TRACE ) )
-        {
-            return;
-        }
-
-        final Map<String, String> debugStrings = storedConfiguration.getModifiedSettingDebugValues( PwmConstants.DEFAULT_LOCALE, true );
-        final List<Supplier<CharSequence>> outputStrings = new ArrayList<>();
-
-        for ( final Map.Entry<String, String> entry : debugStrings.entrySet() )
-        {
-            final String spacedValue = entry.getValue().replace( "\n", "\n   " );
-            final String output = " " + entry.getKey() + "\n   " + spacedValue + "\n";
-            outputStrings.add( () -> output );
-        }
-
-        LOGGER.trace( () -> "--begin current configuration output--" );
-        outputStrings.forEach( LOGGER::trace );
-        LOGGER.trace( () -> "--end current configuration output--" );
-    }
-
     public List<FormConfiguration> readSettingAsForm( final PwmSetting setting )
     {
         final StoredValue value = readStoredValue( setting );
@@ -468,14 +444,14 @@ public class Configuration implements SettingReader
                 {
                     return ( E ) enumClass.getMethod( "valueOf", String.class ).invoke( null, strValue );
                 }
-                catch ( InvocationTargetException e1 )
+                catch ( final InvocationTargetException e1 )
                 {
                     if ( e1.getCause() instanceof IllegalArgumentException )
                     {
                         LOGGER.error( "illegal setting value for option '" + strValue + "' for setting key '" + setting.getKey() + "' is not recognized, will use default" );
                     }
                 }
-                catch ( Exception e1 )
+                catch ( final Exception e1 )
                 {
                     LOGGER.error( "unexpected error", e1 );
                 }
@@ -499,14 +475,14 @@ public class Configuration implements SettingReader
                 {
                     returnSet.add( ( E ) enumClass.getMethod( "valueOf", String.class ).invoke( null, strValue ) );
                 }
-                catch ( InvocationTargetException e1 )
+                catch ( final InvocationTargetException e1 )
                 {
                     if ( e1.getCause() instanceof IllegalArgumentException )
                     {
                         LOGGER.error( "illegal setting value for option '" + strValue + "' is not recognized, will use default" );
                     }
                 }
-                catch ( Exception e1 )
+                catch ( final Exception e1 )
                 {
                     LOGGER.error( "unexpected error", e1 );
                 }
@@ -516,7 +492,7 @@ public class Configuration implements SettingReader
         }
     }
 
-    public Map<Locale, String> readLocalizedBundle( final String className, final String keyName )
+    public Map<Locale, String> readLocalizedBundle( final PwmLocaleBundle className, final String keyName )
     {
         final String key = className + "-" + keyName;
         if ( dataCache.customText.containsKey( key ) )
@@ -643,7 +619,7 @@ public class Configuration implements SettingReader
 
         // set case sensitivity
         final String caseSensitivitySetting = JavaTypeConverter.valueToString( storedConfiguration.readSetting(
-                PwmSetting.PASSWORD_POLICY_CASE_SENSITIVITY ) );
+                PwmSetting.PASSWORD_POLICY_CASE_SENSITIVITY, null ) );
         if ( !"read".equals( caseSensitivitySetting ) )
         {
             passwordPolicySettings.put( PwmPasswordRule.CaseSensitive.getKey(), caseSensitivitySetting );
@@ -674,7 +650,7 @@ public class Configuration implements SettingReader
 
     public boolean isDefaultValue( final PwmSetting pwmSetting )
     {
-        return storedConfiguration.isDefaultValue( pwmSetting );
+        return storedConfiguration.isDefaultValue( pwmSetting, null );
     }
 
     public Collection<Locale> localesForSetting( final PwmSetting setting )
@@ -705,11 +681,6 @@ public class Configuration implements SettingReader
         return returnCollection;
     }
 
-    public String readProperty( final ConfigurationProperty key )
-    {
-        return storedConfiguration.readConfigProperty( key );
-    }
-
     public boolean readSettingAsBoolean( final PwmSetting setting )
     {
         return JavaTypeConverter.valueToBoolean( readStoredValue( setting ) );
@@ -717,7 +688,7 @@ public class Configuration implements SettingReader
 
     public Map<FileValue.FileInformation, FileValue.FileContent> readSettingAsFile( final PwmSetting setting )
     {
-        final FileValue fileValue = ( FileValue ) storedConfiguration.readSetting( setting );
+        final FileValue fileValue = ( FileValue ) storedConfiguration.readSetting( setting, null );
         return ( Map ) fileValue.toNativeObject();
     }
 
@@ -748,48 +719,50 @@ public class Configuration implements SettingReader
         return ( PrivateKeyCertificate ) readStoredValue( setting ).toNativeObject();
     }
 
-    public String getNotes( )
-    {
-        return storedConfiguration.readConfigProperty( ConfigurationProperty.NOTES );
-    }
-
     private PwmSecurityKey tempInstanceKey = null;
 
     public PwmSecurityKey getSecurityKey( ) throws PwmUnrecoverableException
     {
-        final PasswordData configValue = readSettingAsPassword( PwmSetting.PWM_SECURITY_KEY );
-
-        if ( configValue == null || configValue.getStringValue().isEmpty() )
+        if ( dataCache.pwmSecurityKey == null )
         {
-            final String errorMsg = "Security Key value is not configured,will generate temp value for use by runtime instance";
-            final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
-            LOGGER.warn( errorInfo.toDebugStr() );
-            if ( tempInstanceKey == null )
+            final PasswordData configValue = readSettingAsPassword( PwmSetting.PWM_SECURITY_KEY );
+
+            if ( configValue == null || configValue.getStringValue().isEmpty() )
             {
-                tempInstanceKey = new PwmSecurityKey( PwmRandom.getInstance().alphaNumericString( 256 ) );
+                final String errorMsg = "Security Key value is not configured, will generate temp value for use by runtime instance";
+                final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
+                LOGGER.warn( errorInfo.toDebugStr() );
+                if ( tempInstanceKey == null )
+                {
+                    tempInstanceKey = new PwmSecurityKey( PwmRandom.getInstance().alphaNumericString( 1024 ) );
+                }
+                dataCache.pwmSecurityKey = tempInstanceKey;
             }
-            return tempInstanceKey;
-        }
+            else
+            {
+                final int minSecurityKeyLength = Integer.parseInt( readAppProperty( AppProperty.SECURITY_CONFIG_MIN_SECURITY_KEY_LENGTH ) );
+                if ( configValue.getStringValue().length() < minSecurityKeyLength )
+                {
+                    final String errorMsg = "Security Key must be greater than 32 characters in length";
+                    final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
+                    throw new PwmUnrecoverableException( errorInfo );
+                }
 
-        final int minSecurityKeyLength = Integer.parseInt( readAppProperty( AppProperty.SECURITY_CONFIG_MIN_SECURITY_KEY_LENGTH ) );
-        if ( configValue.getStringValue().length() < minSecurityKeyLength )
-        {
-            final String errorMsg = "Security Key must be greater than 32 characters in length";
-            final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
-            throw new PwmUnrecoverableException( errorInfo );
+                try
+                {
+                    dataCache.pwmSecurityKey = new PwmSecurityKey( configValue.getStringValue() );
+                }
+                catch ( final Exception e )
+                {
+                    final String errorMsg = "unexpected error generating Security Key crypto: " + e.getMessage();
+                    final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
+                    LOGGER.error( errorInfo.toDebugStr(), e );
+                    throw new PwmUnrecoverableException( errorInfo );
+                }
+            }
         }
 
-        try
-        {
-            return new PwmSecurityKey( configValue.getStringValue() );
-        }
-        catch ( Exception e )
-        {
-            final String errorMsg = "unexpected error generating Security Key crypto: " + e.getMessage();
-            final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
-            LOGGER.error( errorInfo.toDebugStr(), e );
-            throw new PwmUnrecoverableException( errorInfo );
-        }
+        return dataCache.pwmSecurityKey;
     }
 
     public List<DataStorageMethod> getResponseStorageLocations( final PwmSetting setting )
@@ -813,7 +786,7 @@ public class Configuration implements SettingReader
             {
                 storageMethods.add( DataStorageMethod.valueOf( rawValue ) );
             }
-            catch ( IllegalArgumentException e )
+            catch ( final IllegalArgumentException e )
             {
                 LOGGER.error( "unknown STORAGE_METHOD found: " + rawValue );
             }
@@ -902,7 +875,7 @@ public class Configuration implements SettingReader
         {
             return TokenStorageMethod.valueOf( readSettingAsString( PwmSetting.TOKEN_STORAGEMETHOD ) );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             final String errorMsg = "unknown storage method specified: " + readSettingAsString( PwmSetting.TOKEN_STORAGEMETHOD );
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INVALID_CONFIG, errorMsg );
@@ -1012,12 +985,12 @@ public class Configuration implements SettingReader
             return dataCache.settings.get( setting );
         }
 
-        final StoredValue readValue = storedConfiguration.readSetting( setting );
+        final StoredValue readValue = storedConfiguration.readSetting( setting, null );
         dataCache.settings.put( setting, readValue );
         return readValue;
     }
 
-    private static class DataCache implements Serializable
+    private static class DataCache
     {
         private final Map<String, Map<Locale, PwmPasswordPolicy>> cachedPasswordPolicy = new LinkedHashMap<>();
         private Map<Locale, String> localeFlagMap = null;
@@ -1025,6 +998,7 @@ public class Configuration implements SettingReader
         private final Map<String, Map<Locale, String>> customText = new LinkedHashMap<>();
         private final Map<ProfileDefinition, Map> profileCache = new LinkedHashMap<>();
         private Map<String, String> appPropertyOverrides = null;
+        private PwmSecurityKey pwmSecurityKey;
     }
 
     public Map<AppProperty, String> readAllNonDefaultAppProperties( )
@@ -1118,18 +1092,16 @@ public class Configuration implements SettingReader
         {
             profileFactory = profileFactoryClass.getDeclaredConstructor().newInstance();
         }
-        catch ( InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e )
+        catch ( final InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e )
         {
             throw new IllegalStateException( "unable to create profile instance for " + profileDefinition );
         }
         return profileFactory.makeFromStoredConfiguration( storedConfiguration, profileID );
     }
 
-    public StoredConfigurationImpl getStoredConfiguration( ) throws PwmUnrecoverableException
+    public StoredConfiguration getStoredConfiguration( )
     {
-        final StoredConfigurationImpl copiedStoredConfiguration = StoredConfigurationImpl.copy( storedConfiguration );
-        copiedStoredConfiguration.lock();
-        return copiedStoredConfiguration;
+        return this.storedConfiguration;
     }
 
     public boolean isDevDebugMode( )
@@ -1137,22 +1109,21 @@ public class Configuration implements SettingReader
         return Boolean.parseBoolean( readAppProperty( AppProperty.LOGGING_DEV_OUTPUT ) );
     }
 
-    public String configurationHash( )
+    public String configurationHash( final SecureService secureService )
             throws PwmUnrecoverableException
     {
-        if ( this.cashedConfigurationHash == null )
-        {
-            this.cashedConfigurationHash = storedConfiguration.settingChecksum();
-        }
-        return cashedConfigurationHash;
+        return storedConfiguration.valueHash();
     }
 
     public Set<PwmSetting> nonDefaultSettings( )
     {
-        final Set returnSet = new LinkedHashSet();
-        for ( final StoredConfigurationImpl.SettingValueRecord valueRecord : this.storedConfiguration.modifiedSettings() )
+        final Set<PwmSetting> returnSet = new LinkedHashSet<>();
+        for ( final StoredConfigItemKey key : this.storedConfiguration.modifiedItems() )
         {
-            returnSet.add( valueRecord.getSetting() );
+            if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+            {
+                returnSet.add( key.toPwmSetting() );
+            }
         }
         return returnSet;
     }
@@ -1163,7 +1134,6 @@ public class Configuration implements SettingReader
         return mode == null
                 ? CertificateMatchingMode.CA_ONLY
                 : mode;
-
     }
 
     public Optional<PeopleSearchProfile> getPublicPeopleSearchProfile()

+ 228 - 232
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -20,11 +20,13 @@
 
 package password.pwm.config;
 
+import lombok.Value;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.ValueFactory;
 import password.pwm.i18n.Config;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.logging.PwmLogger;
@@ -36,10 +38,13 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.function.Supplier;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
@@ -1269,9 +1274,6 @@ public enum PwmSetting
             "helpdesk.otp.verify", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_BASE ),;
 
 
-
-
-
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSetting.class );
 
     private final String key;
@@ -1279,16 +1281,16 @@ public enum PwmSetting
     private final PwmSettingCategory category;
 
     // cached values read from XML file
-    private transient Supplier<List<TemplateSetAssociation>> defaultValues;
-    private transient Supplier<List<TemplateSetAssociation>> examples;
-    private transient Supplier<Map<String, String>> options;
-    private transient Supplier<Collection<PwmSettingFlag>> flags;
-    private transient Supplier<Map<PwmSettingProperty, String>> properties;
-    private transient Supplier<Collection<LDAPPermissionInfo>> ldapPermissionInfo;
-    private transient Supplier<Boolean> required;
-    private transient Supplier<Boolean> hidden;
-    private transient Supplier<Integer> level;
-    private transient Supplier<Pattern> pattern;
+    private final Supplier<List<TemplateSetAssociation>> defaultValues = new LazySupplier<>( () -> PwmSettingReader.readDefaultValue( PwmSetting.this ) );
+    private final Supplier<List<TemplateSetAssociation>> examples = new LazySupplier<>( () -> PwmSettingReader.readExamples( PwmSetting.this ) );
+    private final Supplier<Map<String, String>> options = new LazySupplier<>( () -> PwmSettingReader.readOptions( PwmSetting.this ) );
+    private final Supplier<Collection<PwmSettingFlag>> flags = new LazySupplier<>( () -> PwmSettingReader.readFlags( PwmSetting.this ) );
+    private final Supplier<Map<PwmSettingProperty, String>> properties = new LazySupplier<>( () -> PwmSettingReader.readProperties( PwmSetting.this ) );
+    private final Supplier<Collection<LDAPPermissionInfo>> ldapPermissionInfo = new LazySupplier<>( () -> PwmSettingReader.readLdapPermissionInfo( PwmSetting.this ) );
+    private final Supplier<Boolean> required = new LazySupplier<>( () -> PwmSettingReader.readRequired( PwmSetting.this ) );
+    private final Supplier<Boolean> hidden = new LazySupplier<>( () -> PwmSettingReader.readHidden( PwmSetting.this ) );
+    private final Supplier<Integer> level = new LazySupplier<>( () -> PwmSettingReader.readLevel( PwmSetting.this ) );
+    private final Supplier<Pattern> pattern = new LazySupplier<>( () -> PwmSettingReader.readPattern( PwmSetting.this ) );
 
     PwmSetting(
             final String key,
@@ -1323,31 +1325,6 @@ public enum PwmSetting
 
     private List<TemplateSetAssociation> getDefaultValue()
     {
-        if ( defaultValues == null )
-        {
-            final List<TemplateSetAssociation> returnObj = new ArrayList<>();
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( this );
-            final List<XmlElement> defaultElements = settingElement.getChildren( PwmSettingXml.XML_ELEMENT_DEFAULT );
-            if ( this.getSyntax() == PwmSettingSyntax.PASSWORD )
-            {
-                returnObj.add( new TemplateSetAssociation( new PasswordValue( null ), Collections.emptySet() ) );
-            }
-            else
-            {
-                for ( final XmlElement defaultElement : defaultElements )
-                {
-                    final Set<PwmSettingTemplate> definedTemplates = PwmSettingXml.parseTemplateAttribute( defaultElement );
-                    final StoredValue storedValue = ValueFactory.fromXmlValues( this, defaultElement, null );
-                    returnObj.add( new TemplateSetAssociation( storedValue, definedTemplates ) );
-                }
-            }
-            if ( returnObj.isEmpty() )
-            {
-                throw new IllegalStateException( "no default value for setting " + this.getKey() );
-            }
-            final List<TemplateSetAssociation> finalObj = Collections.unmodifiableList( returnObj );
-            defaultValues = ( ) -> finalObj;
-        }
         return defaultValues.get();
     }
 
@@ -1370,132 +1347,22 @@ public enum PwmSetting
         return Collections.unmodifiableMap( returnObj );
     }
 
-    public Map<String, String> getOptions( )
-    {
-        if ( options == null )
-        {
-            final Map<String, String> returnList = new LinkedHashMap<>();
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( this );
-            final XmlElement optionsElement = settingElement.getChild( "options" );
-            if ( optionsElement != null )
-            {
-                final List<XmlElement> optionElements = optionsElement.getChildren( "option" );
-                if ( optionElements != null )
-                {
-                    for ( final XmlElement optionElement : optionElements )
-                    {
-                        if ( optionElement.getAttributeValue( "value" ) == null )
-                        {
-                            throw new IllegalStateException( "option element is missing 'value' attribute for key " + this.getKey() );
-                        }
-                        returnList.put( optionElement.getAttributeValue( "value" ), optionElement.getText() );
-                    }
-                }
-            }
-            final Map<String, String> finalList = Collections.unmodifiableMap( returnList );
-            options = ( ) -> Collections.unmodifiableMap( finalList );
-        }
-
-        return options.get( );
-    }
-
     public Map<PwmSettingProperty, String> getProperties( )
     {
-        if ( properties == null )
-        {
-            final Map<PwmSettingProperty, String> newProps = new LinkedHashMap<>();
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( this );
-            final XmlElement propertiesElement = settingElement.getChild( "properties" );
-            if ( propertiesElement != null )
-            {
-                final List<XmlElement> propertyElements = propertiesElement.getChildren( "property" );
-                if ( propertyElements != null )
-                {
-                    for ( final XmlElement propertyElement : propertyElements )
-                    {
-                        if ( propertyElement.getAttributeValue( "key" ) == null )
-                        {
-                            throw new IllegalStateException( "property element is missing 'key' attribute for value " + this.getKey() );
-                        }
-                        final PwmSettingProperty property = JavaHelper.readEnumFromString( PwmSettingProperty.class, null, propertyElement.getAttributeValue( "key" ) );
-                        if ( property == null )
-                        {
-                            throw new IllegalStateException( "property element has unknown 'key' attribute for value " + this.getKey() );
-                        }
-                        newProps.put( property, propertyElement.getText() );
-                    }
-                }
-            }
-            final Map<PwmSettingProperty, String> finalProps = Collections.unmodifiableMap( newProps );
-            properties = ( ) -> finalProps;
-        }
-
         return properties.get();
     }
 
     public Collection<PwmSettingFlag> getFlags( )
     {
-        if ( flags == null )
-        {
-            final Collection<PwmSettingFlag> returnObj = new ArrayList<>();
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( this );
-            final List<XmlElement> flagElements = settingElement.getChildren( "flag" );
-            for ( final XmlElement flagElement : flagElements )
-            {
-                final String value = flagElement.getTextTrim();
-
-                try
-                {
-                    final PwmSettingFlag flag = PwmSettingFlag.valueOf( value );
-                    returnObj.add( flag );
-                }
-                catch ( IllegalArgumentException e )
-                {
-                    LOGGER.error( "unknown flag for setting " + this.getKey() + ", error: unknown flag value: " + value );
-                }
-
-            }
-            final Collection<PwmSettingFlag> finalObj = Collections.unmodifiableCollection( returnObj );
-            flags = ( ) -> finalObj;
-        }
         return flags.get();
     }
 
-    public Collection<LDAPPermissionInfo> getLDAPPermissionInfo( )
+    public Map<String, String> getOptions()
     {
-        if ( ldapPermissionInfo == null )
-        {
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( this );
-            final List<XmlElement> permissionElements = settingElement.getChildren( PwmSettingXml.XML_ELEMENT_LDAP_PERMISSION );
-            final List<LDAPPermissionInfo> returnObj = new ArrayList<>();
-            if ( permissionElements != null )
-            {
-                for ( final XmlElement permissionElement : permissionElements )
-                {
-                    final LDAPPermissionInfo.Actor actor = JavaHelper.readEnumFromString(
-                            LDAPPermissionInfo.Actor.class,
-                            null,
-                            permissionElement.getAttributeValue( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACTOR )
-                    );
-                    final LDAPPermissionInfo.Access type = JavaHelper.readEnumFromString(
-                            LDAPPermissionInfo.Access.class,
-                            null,
-                            permissionElement.getAttributeValue( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACCESS )
-                    );
-                    if ( actor != null && type != null )
-                    {
-                        final LDAPPermissionInfo permissionInfo = new LDAPPermissionInfo( type, actor );
-                        returnObj.add( permissionInfo );
-                    }
-                }
-            }
-            final List<LDAPPermissionInfo> finalObj = Collections.unmodifiableList( returnObj );
-            ldapPermissionInfo = ( ) -> finalObj;
-        }
-
-        return ldapPermissionInfo.get();
+        return options.get();
     }
 
+
     public String getLabel( final Locale locale )
     {
         final String propertyKey = password.pwm.i18n.PwmSetting.SETTING_LABEL_PREFIX + this.getKey();
@@ -1512,94 +1379,27 @@ public enum PwmSetting
 
     public String getExample( final PwmSettingTemplateSet template )
     {
-        if ( examples == null )
-        {
-            final List<TemplateSetAssociation> returnObj = new ArrayList<>();
-            final MacroMachine macroMachine = MacroMachine.forStatic();
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( this );
-            final List<XmlElement> exampleElements = settingElement.getChildren( PwmSettingXml.XML_ELEMENT_EXAMPLE );
-            for ( final XmlElement exampleElement : exampleElements )
-            {
-                final Set<PwmSettingTemplate> definedTemplates = PwmSettingXml.parseTemplateAttribute( exampleElement );
-                final String exampleString = macroMachine.expandMacros( exampleElement.getText() );
-                returnObj.add( new TemplateSetAssociation( exampleString, Collections.unmodifiableSet( definedTemplates ) ) );
-            }
-            if ( returnObj.isEmpty() )
-            {
-                returnObj.add( new TemplateSetAssociation( "", Collections.emptySet() ) );
-            }
-            final List<TemplateSetAssociation> exampleOutput = Collections.unmodifiableList( returnObj );
-            examples = ( ) -> exampleOutput;
-        }
-
         return ( String ) associationForTempleSet( examples.get(), template ).getObject();
     }
 
     public boolean isRequired( )
     {
-        if ( required == null )
-        {
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( this );
-            final String requiredAttribute = settingElement.getAttributeValue( "required" );
-            final boolean requiredOutput = requiredAttribute != null && "true".equalsIgnoreCase( requiredAttribute );
-            required = ( ) -> requiredOutput;
-        }
         return required.get();
     }
 
     public boolean isHidden( )
     {
-        if ( hidden == null )
-        {
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( this );
-            final String requiredAttribute = settingElement.getAttributeValue( "hidden" );
-            final boolean outputHidden = requiredAttribute != null && "true".equalsIgnoreCase( requiredAttribute ) || this.getCategory().isHidden();
-            hidden = ( ) -> outputHidden;
-        }
         return hidden.get();
     }
 
     public int getLevel( )
     {
-        if ( level == null )
-        {
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( this );
-            final String levelAttribute = settingElement.getAttributeValue( "level" );
-            final int outputLevel = levelAttribute != null ? Integer.parseInt( levelAttribute ) : 0;
-            level = ( ) -> outputLevel;
-        }
         return level.get();
     }
 
     public Pattern getRegExPattern( )
     {
-        if ( pattern == null )
-        {
-            final XmlElement settingNode = PwmSettingXml.readSettingXml( this );
-            final XmlElement regexNode = settingNode.getChild( "regex" );
-            if ( regexNode != null )
-            {
-                try
-                {
-                    final Pattern output = Pattern.compile( regexNode.getText() );
-                    pattern = ( ) -> output;
-                }
-                catch ( PatternSyntaxException e )
-                {
-                    final String errorMsg = "error compiling regex constraints for setting " + this.toString() + ", error: " + e.getMessage();
-                    LOGGER.error( errorMsg, e );
-                    throw new IllegalStateException( errorMsg, e );
-                }
-            }
-            if ( pattern == null )
-            {
-                final Pattern output = Pattern.compile( ".*", Pattern.DOTALL );
-                pattern = ( ) -> output;
-            }
-        }
-
         return pattern.get();
-
     }
 
     public static PwmSetting forKey( final String key )
@@ -1619,6 +1419,11 @@ public enum PwmSetting
         return this.getCategory().toMenuLocationDebug( profileID, locale ) + separator + this.getLabel( locale );
     }
 
+    public Collection<LDAPPermissionInfo> getLDAPPermissionInfo()
+    {
+        return ldapPermissionInfo.get();
+    }
+
     public enum SettingStat
     {
         Total,
@@ -1658,26 +1463,21 @@ public enum PwmSetting
         return returnObj;
     }
 
+    @Value
     public static class TemplateSetAssociation
     {
         private final Object object;
         private final Set<PwmSettingTemplate> settingTemplates;
+    }
 
-        TemplateSetAssociation( final Object association, final Set<PwmSettingTemplate> settingTemplates )
-        {
-            this.object = association;
-            this.settingTemplates = settingTemplates;
-        }
-
-        public Object getObject( )
-        {
-            return object;
-        }
-
-        Set<PwmSettingTemplate> getSettingTemplates( )
+    public static Set<PwmSetting> sortedByMenuLocation( final Locale locale )
+    {
+        final TreeMap<String, PwmSetting> treeMap = new TreeMap<>();
+        for ( final PwmSetting pwmSetting : PwmSetting.values() )
         {
-            return settingTemplates;
+            treeMap.put( pwmSetting.toMenuLocationDebug( null, locale ), pwmSetting );
         }
+        return Collections.unmodifiableSet( new LinkedHashSet<>( treeMap.values() ) );
     }
 
     private static TemplateSetAssociation associationForTempleSet(
@@ -1711,5 +1511,201 @@ public enum PwmSetting
 
         return associationSets.iterator().next();
     }
-}
 
+    static class PwmSettingReader
+    {
+
+        private static Collection<PwmSettingFlag> readFlags( final PwmSetting pwmSetting )
+        {
+            final Collection<PwmSettingFlag> returnObj = new ArrayList<>();
+            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
+            final List<XmlElement> flagElements = settingElement.getChildren( "flag" );
+            for ( final XmlElement flagElement : flagElements )
+            {
+                final String value = flagElement.getTextTrim();
+
+                try
+                {
+                    final PwmSettingFlag flag = PwmSettingFlag.valueOf( value );
+                    returnObj.add( flag );
+                }
+                catch ( final IllegalArgumentException e )
+                {
+                    LOGGER.error( "unknown flag for setting " + pwmSetting.getKey() + ", error: unknown flag value: " + value );
+                }
+
+            }
+            return Collections.unmodifiableCollection( returnObj );
+        }
+
+        private static Map<String, String> readOptions( final PwmSetting pwmSetting )
+        {
+            final Map<String, String> returnList = new LinkedHashMap<>();
+            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
+            final Optional<XmlElement> optionsElement = settingElement.getChild( PwmSettingXml.XML_ELEMENT_OPTIONS );
+            if ( optionsElement.isPresent() )
+            {
+                final List<XmlElement> optionElements = optionsElement.get().getChildren( PwmSettingXml.XML_ELEMENT_OPTION );
+                if ( optionElements != null )
+                {
+                    for ( final XmlElement optionElement : optionElements )
+                    {
+                        if ( optionElement.getAttributeValue( PwmSettingXml.XML_ELEMENT_VALUE ) == null )
+                        {
+                            throw new IllegalStateException( "option element is missing 'value' attribute for key " + pwmSetting.getKey() );
+                        }
+                        returnList.put( optionElement.getAttributeValue( PwmSettingXml.XML_ELEMENT_VALUE ), optionElement.getText() );
+                    }
+                }
+            }
+            final Map<String, String> finalList = Collections.unmodifiableMap( returnList );
+            return Collections.unmodifiableMap( finalList );
+        }
+
+        private static Collection<LDAPPermissionInfo> readLdapPermissionInfo( final PwmSetting pwmSetting )
+        {
+            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
+            final List<XmlElement> permissionElements = settingElement.getChildren( PwmSettingXml.XML_ELEMENT_LDAP_PERMISSION );
+            final List<LDAPPermissionInfo> returnObj = new ArrayList<>();
+            if ( permissionElements != null )
+            {
+                for ( final XmlElement permissionElement : permissionElements )
+                {
+                    final LDAPPermissionInfo.Actor actor = JavaHelper.readEnumFromString(
+                            LDAPPermissionInfo.Actor.class,
+                            null,
+                            permissionElement.getAttributeValue( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACTOR )
+                    );
+                    final LDAPPermissionInfo.Access type = JavaHelper.readEnumFromString(
+                            LDAPPermissionInfo.Access.class,
+                            null,
+                            permissionElement.getAttributeValue( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACCESS )
+                    );
+                    if ( actor != null && type != null )
+                    {
+                        final LDAPPermissionInfo permissionInfo = new LDAPPermissionInfo( type, actor );
+                        returnObj.add( permissionInfo );
+                    }
+                }
+            }
+            return Collections.unmodifiableList( returnObj );
+        }
+
+        private static List<TemplateSetAssociation> readExamples( final PwmSetting pwmSetting )
+        {
+            final List<TemplateSetAssociation> returnObj = new ArrayList<>();
+            final MacroMachine macroMachine = MacroMachine.forStatic();
+            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
+            final List<XmlElement> exampleElements = settingElement.getChildren( PwmSettingXml.XML_ELEMENT_EXAMPLE );
+            for ( final XmlElement exampleElement : exampleElements )
+            {
+                final Set<PwmSettingTemplate> definedTemplates = PwmSettingXml.parseTemplateAttribute( exampleElement );
+                final String exampleString = macroMachine.expandMacros( exampleElement.getText() );
+                returnObj.add( new TemplateSetAssociation( exampleString, Collections.unmodifiableSet( definedTemplates ) ) );
+            }
+            if ( returnObj.isEmpty() )
+            {
+                returnObj.add( new TemplateSetAssociation( "", Collections.emptySet() ) );
+            }
+            return Collections.unmodifiableList( returnObj );
+        }
+
+        private static Map<PwmSettingProperty, String> readProperties( final PwmSetting pwmSetting )
+        {
+            final Map<PwmSettingProperty, String> newProps = new LinkedHashMap<>();
+            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
+            final Optional<XmlElement> propertiesElement = settingElement.getChild( PwmSettingXml.XML_ELEMENT_PROPERTIES );
+            if ( propertiesElement.isPresent() )
+            {
+                final List<XmlElement> propertyElements = propertiesElement.get().getChildren( PwmSettingXml.XML_ELEMENT_PROPERTY );
+                if ( propertyElements != null )
+                {
+                    for ( final XmlElement propertyElement : propertyElements )
+                    {
+                        if ( propertyElement.getAttributeValue( PwmSettingXml.XML_ATTRIBUTE_KEY ) == null )
+                        {
+                            throw new IllegalStateException( "property element is missing 'key' attribute for value " + pwmSetting.getKey() );
+                        }
+                        final PwmSettingProperty property = JavaHelper.readEnumFromString(
+                                PwmSettingProperty.class,
+                                null,
+                                propertyElement.getAttributeValue( PwmSettingXml.XML_ATTRIBUTE_KEY ) );
+                        if ( property == null )
+                        {
+                            throw new IllegalStateException( "property element has unknown 'key' attribute for value " + pwmSetting.getKey() );
+                        }
+                        newProps.put( property, propertyElement.getText() );
+                    }
+                }
+            }
+            return Collections.unmodifiableMap( newProps );
+        }
+
+        private static List<TemplateSetAssociation> readDefaultValue( final PwmSetting pwmSetting )
+        {
+            final List<TemplateSetAssociation> returnObj = new ArrayList<>();
+            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
+            final List<XmlElement> defaultElements = settingElement.getChildren( PwmSettingXml.XML_ELEMENT_DEFAULT );
+            if ( pwmSetting.getSyntax() == PwmSettingSyntax.PASSWORD )
+            {
+                returnObj.add( new TemplateSetAssociation( new PasswordValue( null ), Collections.emptySet() ) );
+            }
+            else
+            {
+                for ( final XmlElement defaultElement : defaultElements )
+                {
+                    final Set<PwmSettingTemplate> definedTemplates = PwmSettingXml.parseTemplateAttribute( defaultElement );
+                    final StoredValue storedValue = ValueFactory.fromXmlValues( pwmSetting, defaultElement, null );
+                    returnObj.add( new TemplateSetAssociation( storedValue, definedTemplates ) );
+                }
+            }
+            if ( returnObj.isEmpty() )
+            {
+                throw new IllegalStateException( "no default value for setting " + pwmSetting.getKey() );
+            }
+            return Collections.unmodifiableList( returnObj );
+        }
+
+
+        private static boolean readRequired( final PwmSetting pwmSetting )
+        {
+            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
+            final String requiredAttribute = settingElement.getAttributeValue( PwmSettingXml.XML_ELEMENT_REQUIRED );
+            return "true".equalsIgnoreCase( requiredAttribute );
+        }
+
+        private static boolean readHidden( final PwmSetting pwmSetting )
+        {
+            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
+            final String requiredAttribute = settingElement.getAttributeValue( PwmSettingXml.XML_ELEMENT_HIDDEN );
+            return "true".equalsIgnoreCase( requiredAttribute ) || pwmSetting.getCategory().isHidden();
+        }
+
+        private static int readLevel( final PwmSetting pwmSetting )
+        {
+            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
+            final String levelAttribute = settingElement.getAttributeValue( PwmSettingXml.XML_ELEMENT_LEVEL );
+            return JavaHelper.silentParseInt( levelAttribute, 0 );
+        }
+
+        private static Pattern readPattern( final PwmSetting pwmSetting )
+        {
+            final XmlElement settingNode = PwmSettingXml.readSettingXml( pwmSetting );
+            final Optional<XmlElement> regexNode = settingNode.getChild( PwmSettingXml.XML_ELEMENT_REGEX );
+            if ( regexNode.isPresent() )
+            {
+                try
+                {
+                    return Pattern.compile( regexNode.get().getText() );
+                }
+                catch ( final PatternSyntaxException e )
+                {
+                    final String errorMsg = "error compiling regex constraints for setting " + pwmSetting.toString() + ", error: " + e.getMessage();
+                    LOGGER.error( errorMsg, e );
+                    throw new IllegalStateException( errorMsg, e );
+                }
+            }
+            return Pattern.compile( ".*", Pattern.DOTALL );
+        }
+    }
+}

+ 4 - 3
server/src/main/java/password/pwm/config/PwmSettingCategory.java

@@ -32,6 +32,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.TreeMap;
 import java.util.function.Supplier;
 
@@ -322,10 +323,10 @@ public enum PwmSettingCategory
         while ( nextCategory != null )
         {
             final XmlElement categoryElement = PwmSettingXml.readCategoryXml( nextCategory );
-            final XmlElement profileElement = categoryElement.getChild( "profile" );
-            if ( profileElement != null )
+            final Optional<XmlElement> profileElement = categoryElement.getChild( "profile" );
+            if ( profileElement.isPresent() )
             {
-                final String settingKey = profileElement.getAttributeValue( "setting" );
+                final String settingKey = profileElement.get().getAttributeValue( "setting" );
                 if ( settingKey != null )
                 {
                     return password.pwm.config.PwmSetting.forKey( settingKey );

+ 1 - 0
server/src/main/java/password/pwm/config/PwmSettingFlag.java

@@ -54,4 +54,5 @@ public enum PwmSettingFlag
 
     WebService_NoBody,
 
+    Deprecated,
 }

+ 2 - 1
server/src/main/java/password/pwm/config/PwmSettingProperty.java

@@ -22,7 +22,6 @@ package password.pwm.config;
 
 public enum PwmSettingProperty
 {
-
     ModificationWarning,
 
     Minimum,
@@ -36,4 +35,6 @@ public enum PwmSettingProperty
     Cert_ImportHandler,
 
     MethodType,
+
+    Restart_Requirements,
 }

+ 1 - 1
server/src/main/java/password/pwm/config/PwmSettingSyntax.java

@@ -83,7 +83,7 @@ public enum PwmSettingSyntax
         this.storedValueImpl = storedValueImpl;
     }
 
-    public StoredValue.StoredValueFactory getStoredValueImpl( )
+    public StoredValue.StoredValueFactory getFactory( )
     {
         return storedValueImpl;
     }

+ 39 - 32
server/src/main/java/password/pwm/config/PwmSettingXml.java

@@ -21,6 +21,8 @@
 package password.pwm.config;
 
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.LazySoftReference;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.XmlDocument;
 import password.pwm.util.java.XmlElement;
@@ -34,7 +36,6 @@ import javax.xml.validation.SchemaFactory;
 import javax.xml.validation.Validator;
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.ref.WeakReference;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -46,41 +47,44 @@ public class PwmSettingXml
     public static final String SETTING_XML_FILENAME = ( PwmSetting.class.getPackage().getName()
             + "." + PwmSetting.class.getSimpleName() ).replace( ".", "/" ) + ".xml";
 
-    public static final String XML_ELEMENT_LDAP_PERMISSION = "ldapPermission";
-    public static final String XML_ELEMENT_EXAMPLE = "example";
-    public static final String XML_ELEMENT_DEFAULT = "default";
+    static final String XML_ELEMENT_LDAP_PERMISSION = "ldapPermission";
+    static final String XML_ELEMENT_EXAMPLE = "example";
+    static final String XML_ELEMENT_DEFAULT = "default";
+
+    static final String XML_ATTRIBUTE_PERMISSION_ACTOR = "actor";
+    static final String XML_ATTRIBUTE_PERMISSION_ACCESS = "access";
+    static final String XML_ATTRIBUTE_TEMPLATE = "template";
+    static final String XML_ELEMENT_REGEX = "regex";
+    static final String XML_ELEMENT_HIDDEN = "hidden";
+    static final String XML_ELEMENT_REQUIRED = "required";
+    static final String XML_ELEMENT_LEVEL = "level";
+    static final String XML_ELEMENT_PROPERTIES = "properties";
+    static final String XML_ELEMENT_PROPERTY = "property";
+    static final String XML_ATTRIBUTE_KEY = "key";
+    static final String XML_ELEMENT_VALUE = "value";
+    static final String XML_ELEMENT_OPTION = "option";
+    static final String XML_ELEMENT_OPTIONS = "options";
 
-    public static final String XML_ATTRIBUTE_PERMISSION_ACTOR = "actor";
-    public static final String XML_ATTRIBUTE_PERMISSION_ACCESS = "access";
-    public static final String XML_ATTRIBUTE_TEMPLATE = "template";
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSettingXml.class );
 
-    private static WeakReference<XmlDocument> xmlDocCache = new WeakReference<>( null );
+    private static LazySoftReference<XmlDocument> xmlDocCache = new LazySoftReference<>( () -> readXml() );
     private static final AtomicInteger LOAD_COUNTER = new AtomicInteger( 0 );
 
     private static XmlDocument readXml( )
     {
-        final XmlDocument docRefCopy = xmlDocCache.get();
-        if ( docRefCopy == null )
+        try ( InputStream inputStream = PwmSetting.class.getClassLoader().getResourceAsStream( SETTING_XML_FILENAME ) )
         {
-            try ( InputStream inputStream = PwmSetting.class.getClassLoader().getResourceAsStream( SETTING_XML_FILENAME ) )
-            {
-                final Instant startTime = Instant.now();
-                final XmlDocument newDoc = XmlFactory.getFactory().parseXml( inputStream );
-                final TimeDuration parseDuration = TimeDuration.fromCurrent( startTime );
-                LOGGER.trace( () -> "parsed PwmSettingXml in " + parseDuration.asCompactString() + ", loads=" + LOAD_COUNTER.getAndIncrement() );
-
-                xmlDocCache = new WeakReference<>( newDoc );
-
-                return newDoc;
-            }
-            catch ( IOException | PwmUnrecoverableException e )
-            {
-                throw new IllegalStateException( "error parsing " + SETTING_XML_FILENAME + ": " + e.getMessage() );
-            }
+            final Instant startTime = Instant.now();
+            final XmlDocument newDoc = XmlFactory.getFactory().parseXml( inputStream );
+            final TimeDuration parseDuration = TimeDuration.fromCurrent( startTime );
+            LOGGER.trace( () -> "parsed PwmSettingXml in " + parseDuration.asCompactString() + ", loads=" + LOAD_COUNTER.getAndIncrement() );
+            return newDoc;
+        }
+        catch ( final IOException | PwmUnrecoverableException e )
+        {
+            throw new IllegalStateException( "error parsing " + SETTING_XML_FILENAME + ": " + e.getMessage() );
         }
-        return docRefCopy;
     }
 
     private static void validateXmlSchema( )
@@ -94,7 +98,7 @@ public class PwmSettingXml
             final Validator validator = schema.newValidator();
             validator.validate( new StreamSource( xmlInputStream ) );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             throw new IllegalStateException( "error validating PwmSetting.xml schema using PwmSetting.xsd definition: " + e.getMessage() );
         }
@@ -103,19 +107,22 @@ public class PwmSettingXml
     static XmlElement readSettingXml( final PwmSetting setting )
     {
         final String expression = "/settings/setting[@key=\"" + setting.getKey() + "\"]";
-        return readXml().evaluateXpathToElement( expression );
+        return xmlDocCache.get().evaluateXpathToElement( expression )
+                .orElseThrow( () -> new IllegalStateException( "PwmSetting.xml is missing setting for key '" + setting.getKey() + "'" ) );
     }
 
     static XmlElement readCategoryXml( final PwmSettingCategory category )
     {
         final String expression = "/settings/category[@key=\"" + category.toString() + "\"]";
-        return readXml().evaluateXpathToElement( expression );
+        return xmlDocCache.get().evaluateXpathToElement( expression )
+                .orElseThrow( () -> new IllegalStateException( "PwmSetting.xml is missing category for key '" + category.getKey() + "'" ) );
     }
 
     static XmlElement readTemplateXml( final PwmSettingTemplate template )
     {
         final String expression = "/settings/template[@key=\"" + template.toString() + "\"]";
-        return readXml().evaluateXpathToElement( expression );
+        return xmlDocCache.get().evaluateXpathToElement( expression )
+                .orElseThrow( () -> new IllegalStateException( "PwmSetting.xml is missing template for key '" + template.toString() + "'" ) );
     }
 
     static Set<PwmSettingTemplate> parseTemplateAttribute( final XmlElement element )
@@ -124,14 +131,14 @@ public class PwmSettingXml
         {
             return Collections.emptySet();
         }
-        final String templateStrValues = element.getAttributeValue( "template" );
+        final String templateStrValues = element.getAttributeValue( XML_ATTRIBUTE_TEMPLATE );
         final String[] templateSplitValues = templateStrValues == null
                 ? new String[ 0 ]
                 : templateStrValues.split( "," );
         final Set<PwmSettingTemplate> definedTemplates = new LinkedHashSet<>();
         for ( final String templateStrValue : templateSplitValues )
         {
-            final PwmSettingTemplate template = PwmSettingTemplate.valueOf( templateStrValue );
+            final PwmSettingTemplate template = JavaHelper.readEnumFromString( PwmSettingTemplate.class, null, templateStrValue );
             if ( template != null )
             {
                 definedTemplates.add( template );

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

@@ -20,7 +20,7 @@
 
 package password.pwm.config;
 
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.http.PwmRequest;
 
 import java.io.Serializable;
@@ -29,7 +29,7 @@ public interface SettingUIFunction
 {
     Serializable provideFunction(
             PwmRequest pwmRequest,
-            StoredConfigurationImpl storedConfiguration,
+            StoredConfigurationModifier modifier,
             PwmSetting setting,
             String profile,
             String extraData

+ 3 - 5
server/src/main/java/password/pwm/config/StoredValue.java

@@ -20,8 +20,8 @@
 
 package password.pwm.config;
 
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmException;
-import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.secure.PwmSecurityKey;
 
@@ -31,7 +31,7 @@ import java.util.Locale;
 
 public interface StoredValue extends Serializable
 {
-    List<XmlElement> toXmlValues( String valueElementName, PwmSecurityKey pwmSecurityKey );
+    List<XmlElement> toXmlValues( String valueElementName, XmlOutputProcessData xmlOutputProcessData );
 
     Object toNativeObject( );
 
@@ -41,8 +41,6 @@ public interface StoredValue extends Serializable
 
     String toDebugString( Locale locale );
 
-    boolean requiresStoredUpdate( );
-
     int currentSyntaxVersion( );
 
     interface StoredValueFactory
@@ -53,5 +51,5 @@ public interface StoredValue extends Serializable
                 throws PwmException;
     }
 
-    String valueHash( ) throws PwmUnrecoverableException;
+    String valueHash();
 }

+ 15 - 9
server/src/main/java/password/pwm/config/function/AbstractUriCertImportFunction.java

@@ -24,7 +24,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -45,7 +45,7 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
     @Override
     public String provideFunction(
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting setting,
             final String profile,
             final String extraData )
@@ -54,21 +54,21 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final List<X509Certificate> certs;
 
-        final String urlString = getUri( storedConfiguration, setting, profile, extraData );
+        final String urlString = getUri( modifier, setting, profile, extraData );
         try
         {
             final URI uri = URI.create( urlString );
             if ( "https".equalsIgnoreCase( uri.getScheme() ) )
             {
-                certs = X509Utils.readRemoteHttpCertificates( pwmRequest.getPwmApplication(), pwmSession.getLabel(), uri, new Configuration( storedConfiguration ) );
+                certs = X509Utils.readRemoteHttpCertificates( pwmRequest.getPwmApplication(), pwmSession.getLabel(), uri, new Configuration( modifier.newStoredConfiguration() ) );
             }
             else
             {
-                final Configuration configuration = new Configuration( storedConfiguration );
+                final Configuration configuration = new Configuration( modifier.newStoredConfiguration() );
                 certs = X509Utils.readRemoteCertificates( URI.create( urlString ), configuration );
             }
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             if ( e instanceof PwmException )
             {
@@ -80,7 +80,7 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
 
 
         final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-        store( certs, storedConfiguration, setting, profile, extraData, userIdentity );
+        store( certs, modifier, setting, profile, extraData, userIdentity );
 
         final StringBuffer returnStr = new StringBuffer();
         for ( final X509Certificate loopCert : certs )
@@ -91,12 +91,18 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
         return returnStr.toString();
     }
 
-    abstract String getUri( StoredConfigurationImpl storedConfiguration, PwmSetting pwmSetting, String profile, String extraData ) throws PwmOperationalException;
+    abstract String getUri(
+            StoredConfigurationModifier modifier,
+            PwmSetting pwmSetting,
+            String profile,
+            String extraData
+    )
+            throws PwmOperationalException, PwmUnrecoverableException;
 
 
     void store(
             final List<X509Certificate> certs,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier storedConfiguration,
             final PwmSetting pwmSetting,
             final String profile,
             final String extraData,

+ 13 - 7
server/src/main/java/password/pwm/config/function/ActionCertImportFunction.java

@@ -23,7 +23,7 @@ package password.pwm.config.function;
 import com.google.gson.reflect.TypeToken;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.ActionValue;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.error.ErrorInformation;
@@ -42,14 +42,20 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
     private static final String KEY_ITERATION = "iteration";
     private static final String KEY_WEB_ACTION_ITERATION = "webActionIter";
 
-            @Override
-    String getUri( final StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData ) throws PwmOperationalException
+    @Override
+    String getUri(
+            final StoredConfigurationModifier modifier,
+            final PwmSetting pwmSetting,
+            final String profile,
+            final String extraData
+    )
+            throws PwmOperationalException, PwmUnrecoverableException
     {
         final Map<String, Integer> extraDataMap = JsonUtil.deserialize( extraData, new TypeToken<Map<String, Integer>>()
         {
         } );
 
-        final ActionValue actionValue = ( ActionValue ) storedConfiguration.readSetting( pwmSetting, profile );
+        final ActionValue actionValue = ( ActionValue ) modifier.newStoredConfiguration().readSetting( pwmSetting, profile );
         final ActionConfiguration action = ( actionValue.toNativeObject() ).get( extraDataMap.get( KEY_ITERATION ) );
         final ActionConfiguration.WebAction webAction = action.getWebActions().get( extraDataMap.get( KEY_WEB_ACTION_ITERATION ) );
 
@@ -67,7 +73,7 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
         {
             URI.create( uriString );
         }
-        catch ( IllegalArgumentException e )
+        catch ( final IllegalArgumentException e )
         {
             final ErrorInformation errorInformation = new ErrorInformation(
                     PwmError.CONFIG_FORMAT_ERROR, "Setting "
@@ -79,7 +85,7 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
 
     void store(
             final List<X509Certificate> certs,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier storedConfiguration,
             final PwmSetting pwmSetting,
             final String profile,
             final String extraData,
@@ -91,7 +97,7 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
         {
         } );
 
-        final ActionValue actionValue = ( ActionValue ) storedConfiguration.readSetting( pwmSetting, profile );
+        final ActionValue actionValue = ( ActionValue ) storedConfiguration.newStoredConfiguration().readSetting( pwmSetting, profile );
         final List<ActionConfiguration> actionConfigurations = actionValue.toNativeObject();
         final ActionConfiguration action = actionConfigurations.get( extraDataMap.get( KEY_ITERATION ) );
         final ActionConfiguration.WebAction webAction = action.getWebActions().get( extraDataMap.get( KEY_WEB_ACTION_ITERATION ) );

+ 4 - 4
server/src/main/java/password/pwm/config/function/LdapCertImportFunction.java

@@ -24,7 +24,7 @@ import password.pwm.PwmApplication;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.StringArrayValue;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.PwmUnrecoverableException;
@@ -43,7 +43,7 @@ public class LdapCertImportFunction implements SettingUIFunction
     @Override
     public String provideFunction(
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting setting,
             final String profile,
             final String extraData
@@ -53,7 +53,7 @@ public class LdapCertImportFunction implements SettingUIFunction
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
 
-        final StringArrayValue ldapUrlsValue = ( StringArrayValue ) storedConfiguration.readSetting( PwmSetting.LDAP_SERVER_URLS, profile );
+        final StringArrayValue ldapUrlsValue = ( StringArrayValue ) modifier.newStoredConfiguration().readSetting( PwmSetting.LDAP_SERVER_URLS, profile );
         final Set<X509Certificate> resultCertificates = new LinkedHashSet<>();
         if ( ldapUrlsValue != null && ldapUrlsValue.toNativeObject() != null )
         {
@@ -62,7 +62,7 @@ public class LdapCertImportFunction implements SettingUIFunction
         }
 
         final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-        storedConfiguration.writeSetting( setting, profile, new X509CertificateValue( resultCertificates ), userIdentity );
+        modifier.writeSetting( setting, profile, new X509CertificateValue( resultCertificates ), userIdentity );
         return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmApplication.getConfig() );
     }
 

+ 7 - 5
server/src/main/java/password/pwm/config/function/OAuthCertImportFunction.java

@@ -22,10 +22,11 @@ package password.pwm.config.function;
 
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
 
 import java.net.URI;
@@ -35,7 +36,8 @@ public class OAuthCertImportFunction extends AbstractUriCertImportFunction
 
 
     @Override
-    String getUri( final StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData ) throws PwmOperationalException
+    String getUri( final StoredConfigurationModifier modifier, final PwmSetting pwmSetting, final String profile, final String extraData )
+            throws PwmOperationalException, PwmUnrecoverableException
     {
 
         final String uriString;
@@ -44,12 +46,12 @@ public class OAuthCertImportFunction extends AbstractUriCertImportFunction
         switch ( pwmSetting )
         {
             case OAUTH_ID_CERTIFICATE:
-                uriString = ( String ) storedConfiguration.readSetting( PwmSetting.OAUTH_ID_CODERESOLVE_URL ).toNativeObject();
+                uriString = ( String ) modifier.newStoredConfiguration().readSetting( PwmSetting.OAUTH_ID_CODERESOLVE_URL, null ).toNativeObject();
                 menuDebugLocation = PwmSetting.OAUTH_ID_CODERESOLVE_URL.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
                 break;
 
             case RECOVERY_OAUTH_ID_CERTIFICATE:
-                uriString = ( String ) storedConfiguration.readSetting( PwmSetting.RECOVERY_OAUTH_ID_CODERESOLVE_URL, profile ).toNativeObject();
+                uriString = ( String ) modifier.newStoredConfiguration().readSetting( PwmSetting.RECOVERY_OAUTH_ID_CODERESOLVE_URL, profile ).toNativeObject();
                 menuDebugLocation = PwmSetting.RECOVERY_OAUTH_ID_CERTIFICATE.toMenuLocationDebug( profile, PwmConstants.DEFAULT_LOCALE );
                 break;
 
@@ -68,7 +70,7 @@ public class OAuthCertImportFunction extends AbstractUriCertImportFunction
         {
             URI.create( uriString );
         }
-        catch ( IllegalArgumentException e )
+        catch ( final IllegalArgumentException e )
         {
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, "Setting " + menuDebugLocation + " has an invalid URL syntax" );
             throw new PwmOperationalException( errorInformation );

+ 12 - 11
server/src/main/java/password/pwm/config/function/RemoteWebServiceCertImportFunction.java

@@ -22,14 +22,13 @@ package password.pwm.config.function;
 
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.RemoteWebServiceValue;
 import password.pwm.config.value.data.RemoteWebServiceConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.JsonUtil;
 
 import java.net.URI;
 import java.security.cert.X509Certificate;
@@ -40,9 +39,10 @@ public class RemoteWebServiceCertImportFunction extends AbstractUriCertImportFun
 {
 
     @Override
-    String getUri( final StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData ) throws PwmOperationalException
+    String getUri( final StoredConfigurationModifier modifier, final PwmSetting pwmSetting, final String profile, final String extraData )
+            throws PwmOperationalException, PwmUnrecoverableException
     {
-        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) storedConfiguration.readSetting( pwmSetting, profile );
+        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) modifier.newStoredConfiguration().readSetting( pwmSetting, profile );
         final String serviceName = actionNameFromExtraData( extraData );
         final RemoteWebServiceConfiguration action = actionValue.forName( serviceName );
         final String uriString = action.getUrl();
@@ -57,7 +57,7 @@ public class RemoteWebServiceCertImportFunction extends AbstractUriCertImportFun
         {
             URI.create( uriString );
         }
-        catch ( IllegalArgumentException e )
+        catch ( final IllegalArgumentException e )
         {
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR,
                     "Setting " + pwmSetting.toMenuLocationDebug( profile, null ) + " action " + serviceName + " has an invalid URL syntax" );
@@ -73,7 +73,7 @@ public class RemoteWebServiceCertImportFunction extends AbstractUriCertImportFun
 
     void store(
             final List<X509Certificate> certs,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting pwmSetting,
             final String profile,
             final String extraData,
@@ -81,24 +81,25 @@ public class RemoteWebServiceCertImportFunction extends AbstractUriCertImportFun
     )
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) storedConfiguration.readSetting( pwmSetting, profile );
+        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) modifier.newStoredConfiguration().readSetting( pwmSetting, profile );
         final String actionName = actionNameFromExtraData( extraData );
         final List<RemoteWebServiceConfiguration> newList = new ArrayList<>();
         for ( final RemoteWebServiceConfiguration loopConfiguration : actionValue.toNativeObject() )
         {
             if ( actionName.equals( loopConfiguration.getName() ) )
             {
-                final RemoteWebServiceConfiguration newConfig = JsonUtil.cloneUsingJson( loopConfiguration, RemoteWebServiceConfiguration.class );
-                newConfig.setCertificates( certs );
+                final RemoteWebServiceConfiguration newConfig = loopConfiguration.toBuilder()
+                        .certificates( certs )
+                        .build();
                 newList.add( newConfig );
             }
             else
             {
-                newList.add( JsonUtil.cloneUsingJson( loopConfiguration, RemoteWebServiceConfiguration.class ) );
+                newList.add( loopConfiguration );
             }
         }
         final RemoteWebServiceValue newActionValue = new RemoteWebServiceValue( newList );
-        storedConfiguration.writeSetting( pwmSetting, profile, newActionValue, userIdentity );
+        modifier.writeSetting( pwmSetting, profile, newActionValue, userIdentity );
     }
 
 }

+ 6 - 8
server/src/main/java/password/pwm/config/function/SMSGatewayCertImportFunction.java

@@ -22,26 +22,24 @@ package password.pwm.config.function;
 
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
 
 import java.net.URI;
 
 public class SMSGatewayCertImportFunction extends AbstractUriCertImportFunction
 {
-
-
     @Override
-    String getUri( final StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData ) throws PwmOperationalException
+    String getUri( final StoredConfigurationModifier modifier, final PwmSetting pwmSetting, final String profile, final String extraData )
+            throws PwmOperationalException, PwmUnrecoverableException
     {
-
-
         final String uriString;
         final String menuDebugLocation;
 
-        uriString = ( String ) storedConfiguration.readSetting( PwmSetting.SMS_GATEWAY_URL ).toNativeObject();
+        uriString = ( String ) modifier.newStoredConfiguration().readSetting( PwmSetting.SMS_GATEWAY_URL, null ).toNativeObject();
         menuDebugLocation = PwmSetting.SMS_GATEWAY_URL.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
 
         if ( uriString.isEmpty() )
@@ -53,7 +51,7 @@ public class SMSGatewayCertImportFunction extends AbstractUriCertImportFunction
         {
             URI.create( uriString );
         }
-        catch ( IllegalArgumentException e )
+        catch ( final IllegalArgumentException e )
         {
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, "Setting " + menuDebugLocation + " has an invalid URL syntax" );
             throw new PwmOperationalException( errorInformation );

+ 4 - 4
server/src/main/java/password/pwm/config/function/SmtpCertImportFunction.java

@@ -24,7 +24,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
@@ -41,7 +41,7 @@ public class SmtpCertImportFunction implements SettingUIFunction
     @Override
     public String provideFunction(
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting setting,
             final String profile,
             final String extraData
@@ -50,12 +50,12 @@ public class SmtpCertImportFunction implements SettingUIFunction
     {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
 
-        final Configuration configuration = new Configuration( storedConfiguration );
+        final Configuration configuration = new Configuration( modifier.newStoredConfiguration() );
         final List<X509Certificate> certs = EmailServerUtil.readCertificates( configuration, profile );
         if ( !JavaHelper.isEmpty( certs ) )
         {
             final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-            storedConfiguration.writeSetting( PwmSetting.EMAIL_SERVER_CERTS, profile, new X509CertificateValue( certs ), userIdentity );
+            modifier.writeSetting( PwmSetting.EMAIL_SERVER_CERTS, profile, new X509CertificateValue( certs ), userIdentity );
         }
 
         return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmRequest.getConfig() );

+ 7 - 7
server/src/main/java/password/pwm/config/function/SyslogCertImportFunction.java

@@ -25,7 +25,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -49,7 +49,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
     @Override
     public String provideFunction(
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting setting,
             final String profile,
             final String extraData )
@@ -62,10 +62,10 @@ public class SyslogCertImportFunction implements SettingUIFunction
 
         final Set<X509Certificate> resultCertificates = new LinkedHashSet<>();
 
-        final List<String> syslogConfigStrs = ( List<String> ) storedConfiguration.readSetting( PwmSetting.AUDIT_SYSLOG_SERVERS ).toNativeObject();
+        final List<String> syslogConfigStrs = ( List<String> ) modifier.newStoredConfiguration().readSetting( PwmSetting.AUDIT_SYSLOG_SERVERS, null ).toNativeObject();
         if ( syslogConfigStrs != null && !syslogConfigStrs.isEmpty() )
         {
-            for ( String entry : syslogConfigStrs )
+            for ( final String entry : syslogConfigStrs )
             {
                 if ( entry.toUpperCase().startsWith( "TLS" ) )
                 {
@@ -77,7 +77,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
                             final List<X509Certificate> certs = X509Utils.readRemoteCertificates(
                                     syslogConfig.getHost(),
                                     syslogConfig.getPort(),
-                                    new Configuration( storedConfiguration )
+                                    new Configuration( modifier.newStoredConfiguration() )
                             );
                             if ( certs != null )
                             {
@@ -85,7 +85,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
                                 error = false;
                             }
                         }
-                        catch ( Exception e )
+                        catch ( final Exception e )
                         {
                             error = true;
                             exeception = e;
@@ -98,7 +98,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
         if ( !error )
         {
             final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-            storedConfiguration.writeSetting( setting, new X509CertificateValue( resultCertificates ), userIdentity );
+            modifier.writeSetting( setting, null, new X509CertificateValue( resultCertificates ), userIdentity );
             return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmApplication.getConfig() );
         }
         else

+ 7 - 6
server/src/main/java/password/pwm/config/function/UserMatchViewerFunction.java

@@ -31,7 +31,8 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -58,7 +59,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
     @Override
     public Serializable provideFunction(
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier storedConfiguration,
             final PwmSetting setting,
             final String profile,
             final String extraData )
@@ -68,7 +69,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
 
         final Instant startSearchTime = Instant.now();
         final int maxResultSize = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.CONFIG_EDITOR_QUERY_FILTER_TEST_LIMIT ) );
-        final Collection<UserIdentity> users = discoverMatchingUsers( pwmApplication, maxResultSize, storedConfiguration, setting, profile );
+        final Collection<UserIdentity> users = discoverMatchingUsers( pwmApplication, maxResultSize, storedConfiguration.newStoredConfiguration(), setting, profile );
         final TimeDuration searchDuration = TimeDuration.fromCurrent( startSearchTime );
 
         final UserMatchViewerResults userMatchViewerResults = new UserMatchViewerResults();
@@ -88,7 +89,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
     public Collection<UserIdentity> discoverMatchingUsers(
             final PwmApplication pwmApplication,
             final int maxResultSize,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfiguration storedConfiguration,
             final PwmSetting setting,
             final String profile
     )
@@ -141,7 +142,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
                 final ChaiProvider proxiedProvider = pwmApplication.getProxyChaiProvider( loopID );
                 chaiEntry = proxiedProvider.getEntryFactory().newChaiEntry( baseDN );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.error( "error while testing entry DN for profile '" + profileID + "', error:" + profileID );
             }
@@ -153,7 +154,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
                     throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_LDAP_DATA_ERROR, errorMsg ) );
                 }
             }
-            catch ( ChaiUnavailableException e )
+            catch ( final ChaiUnavailableException e )
             {
                 throw PwmUnrecoverableException.fromChaiException( e );
             }

+ 3 - 3
server/src/main/java/password/pwm/config/profile/ChallengeProfile.java

@@ -98,7 +98,7 @@ public class ChallengeProfile implements Profile, Serializable
                     minRandomRequired
             );
         }
-        catch ( PwmOperationalException e )
+        catch ( final PwmOperationalException e )
         {
             LOGGER.trace( () -> "configured challengeSet for profile '" + profileID + "' is not valid: " + e.getMessage() );
         }
@@ -115,7 +115,7 @@ public class ChallengeProfile implements Profile, Serializable
                     1
             );
         }
-        catch ( PwmOperationalException e )
+        catch ( final PwmOperationalException e )
         {
             LOGGER.trace( () -> "discarding configured helpdesk challengeSet for profile '" + profileID + "' issue: " + e.getMessage() );
         }
@@ -256,7 +256,7 @@ public class ChallengeProfile implements Profile, Serializable
         {
             return new ChaiChallengeSet( challenges, randoms, locale, PwmConstants.PWM_APP_NAME + "-defined " + PwmConstants.SERVLET_VERSION );
         }
-        catch ( ChaiValidationException e )
+        catch ( final ChaiValidationException e )
         {
             throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, "invalid challenge set configuration: " + e.getMessage() ) );
         }

+ 1 - 1
server/src/main/java/password/pwm/config/profile/LdapProfile.java

@@ -173,7 +173,7 @@ public class LdapProfile extends AbstractProfile implements Profile
                     LOGGER.trace( () -> "read and cached canonical ldap DN value for input '" + dnValue + "' as '" + finalCanonical + "'" );
                 }
             }
-            catch ( ChaiUnavailableException | ChaiOperationException e )
+            catch ( final ChaiUnavailableException | ChaiOperationException e )
             {
                 LOGGER.error( "error while reading canonicalDN for dn value '" + dnValue + "', error: " + e.getMessage() );
                 return dnValue;

+ 1 - 1
server/src/main/java/password/pwm/config/profile/NewUserProfile.java

@@ -135,7 +135,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
                     final UserIdentity userIdentity = new UserIdentity( lookupDN, defaultLdapProfile.getIdentifier() );
                     thePolicy = PasswordUtility.readPasswordPolicyForUser( pwmApplication, null, userIdentity, chaiUser, userLocale );
                 }
-                catch ( ChaiUnavailableException e )
+                catch ( final ChaiUnavailableException e )
                 {
                     throw new PwmUnrecoverableException( PwmError.forChaiError( e.getErrorCode() ) );
                 }

+ 8 - 5
server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java

@@ -59,7 +59,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
 
     private static final PwmPasswordPolicy DEFAULT_POLICY;
 
-    private final Map<String, String> policyMap = new HashMap<>();
+    private final Map<String, String> policyMap;
 
     private final transient ChaiPasswordPolicy chaiPasswordPolicy;
 
@@ -111,7 +111,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
             }
             newDefaultPolicy = createPwmPasswordPolicy( defaultPolicyMap, null );
         }
-        catch ( Throwable t )
+        catch ( final Throwable t )
         {
             LOGGER.fatal( "error initializing PwmPasswordPolicy class: " + t.getMessage(), t );
         }
@@ -130,19 +130,20 @@ public class PwmPasswordPolicy implements Profile, Serializable
             final PolicyMetaData policyMetaData
     )
     {
+        final Map<String, String> effectivePolicyMap = new HashMap<>();
         if ( policyMap != null )
         {
-            this.policyMap.putAll( policyMap );
+            effectivePolicyMap.putAll( policyMap );
         }
         if ( chaiPasswordPolicy != null )
         {
             if ( Boolean.parseBoolean( chaiPasswordPolicy.getValue( ChaiPasswordRule.ADComplexity ) ) )
             {
-                this.policyMap.put( PwmPasswordRule.ADComplexityLevel.getKey(), ADPolicyComplexity.AD2003.toString() );
+                effectivePolicyMap.put( PwmPasswordRule.ADComplexityLevel.getKey(), ADPolicyComplexity.AD2003.toString() );
             }
             else if ( Boolean.parseBoolean( chaiPasswordPolicy.getValue( ChaiPasswordRule.ADComplexity2008 ) ) )
             {
-                this.policyMap.put( PwmPasswordRule.ADComplexityLevel.getKey(), ADPolicyComplexity.AD2008.toString() );
+                effectivePolicyMap.put( PwmPasswordRule.ADComplexityLevel.getKey(), ADPolicyComplexity.AD2008.toString() );
             }
         }
         this.chaiPasswordPolicy = chaiPasswordPolicy;
@@ -152,6 +153,8 @@ public class PwmPasswordPolicy implements Profile, Serializable
             this.userPermissions = policyMetaData.getUserPermissions();
             this.profileID = policyMetaData.getProfileID();
         }
+
+        this.policyMap = Collections.unmodifiableMap( effectivePolicyMap );
     }
 
     @Override

+ 2 - 2
server/src/main/java/password/pwm/config/profile/PwmPasswordRule.java

@@ -403,7 +403,7 @@ public enum PwmPasswordRule
             }
             assert keys.size() == PwmSetting.values().length;
         }
-        catch ( Throwable t )
+        catch ( final Throwable t )
         {
             LOGGER.fatal( "error initializing PwmPasswordRule class: " + t.getMessage(), t );
         }
@@ -515,7 +515,7 @@ public enum PwmPasswordRule
         {
             return LocaleHelper.getLocalizedMessage( locale, key, config, Message.class );
         }
-        catch ( MissingResourceException e )
+        catch ( final MissingResourceException e )
         {
             return "MissingKey-" + key;
         }

+ 0 - 150
server/src/main/java/password/pwm/config/stored/ConfigChangeLogImpl.java

@@ -1,150 +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;
-
-import password.pwm.config.PwmSetting;
-import password.pwm.config.StoredValue;
-import password.pwm.util.java.StringUtil;
-
-import java.io.Serializable;
-import java.util.Collection;
-import java.util.LinkedHashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TreeMap;
-
-public class ConfigChangeLogImpl implements Serializable, ConfigChangeLog
-{
-    private final Map<StoredConfigReference, StoredValue> changeLog = new LinkedHashMap<>();
-    private final Map<StoredConfigReference, StoredValue> originalValue = new LinkedHashMap<>();
-    private final transient StorageEngine storedConfiguration;
-
-    public ConfigChangeLogImpl( final StorageEngine storageEngine )
-    {
-        this.storedConfiguration = storageEngine;
-    }
-
-    @Override
-    public boolean isModified( )
-    {
-        return !changeLog.isEmpty();
-    }
-
-    @Override
-    public String changeLogAsDebugString( final Locale locale, final boolean asHtml )
-    {
-        final Map<String, String> outputMap = new TreeMap<>();
-
-        for ( final StoredConfigReference configReference : changeLog.keySet() )
-        {
-            switch ( configReference.getRecordType() )
-            {
-                case SETTING:
-                {
-                    final PwmSetting pwmSetting = PwmSetting.forKey( configReference.getRecordID() );
-                    final StoredValue currentValue = storedConfiguration.read( configReference );
-                    final String keyName = pwmSetting.toMenuLocationDebug( configReference.getProfileID(), locale );
-                    final String debugValue = currentValue.toDebugString( locale );
-                    outputMap.put( keyName, debugValue );
-                }
-                break;
-
-                /*
-                case LOCALE_BUNDLE: {
-                    final String SEPARATOR = LocaleHelper.getLocalizedMessage(locale, Config.Display_SettingNavigationSeparator, null);
-                    final String key = (String) configReference.recordID;
-                    final String bundleName = key.split("!")[0];
-                    final String keys = key.split("!")[1];
-                    final Map<String,String> currentValue = readLocaleBundleMap(bundleName,keys);
-                    final String debugValue = JsonUtil.serializeMap(currentValue, JsonUtil.Flag.PrettyPrint);
-                    outputMap.put("LocaleBundle" + SEPARATOR + bundleName + " " + keys,debugValue);
-                }
-                break;
-                */
-
-                default:
-                    //continue
-                    break;
-            }
-        }
-        final StringBuilder output = new StringBuilder();
-        if ( outputMap.isEmpty() )
-        {
-            output.append( "No setting changes." );
-        }
-        else
-        {
-            for ( final Map.Entry<String, String> entry : outputMap.entrySet() )
-            {
-                final String keyName = entry.getKey();
-                final String value = entry.getValue();
-                if ( asHtml )
-                {
-                    output.append( "<div class=\"changeLogKey\">" );
-                    output.append( keyName );
-                    output.append( "</div><div class=\"changeLogValue\">" );
-                    output.append( StringUtil.escapeHtml( value ) );
-                    output.append( "</div>" );
-                }
-                else
-                {
-                    output.append( keyName );
-                    output.append( "\n" );
-                    output.append( " Value: " );
-                    output.append( value );
-                    output.append( "\n" );
-                }
-            }
-        }
-        return output.toString();
-    }
-
-    @Override
-    public void updateChangeLog( final StoredConfigReference reference, final StoredValue newValue )
-    {
-        changeLog.put( reference, newValue );
-        originalValue.put( reference, null );
-    }
-
-    @Override
-    public void updateChangeLog( final StoredConfigReference reference, final StoredValue currentValue, final StoredValue newValue )
-    {
-        if ( originalValue.containsKey( reference ) )
-        {
-            if ( newValue.equals( originalValue.get( reference ) ) )
-            {
-                originalValue.remove( reference );
-                changeLog.remove( reference );
-            }
-        }
-        else
-        {
-            originalValue.put( reference, currentValue );
-            changeLog.put( reference, newValue );
-        }
-    }
-
-    @Override
-    public Collection<StoredConfigReference> changedValues( )
-    {
-        return changeLog.keySet();
-    }
-}

+ 2 - 15
server/src/main/java/password/pwm/config/stored/StoredConfigReference.java → server/src/main/java/password/pwm/config/stored/ConfigRestartRequirement.java

@@ -20,20 +20,7 @@
 
 package password.pwm.config.stored;
 
-import java.io.Serializable;
-
-public interface StoredConfigReference extends Serializable, Comparable
+public enum ConfigRestartRequirement
 {
-    RecordType getRecordType( );
-
-    String getRecordID( );
-
-    String getProfileID( );
-
-    enum RecordType
-    {
-        SETTING,
-        LOCALE_BUNDLE,
-        PROPERTY,
-    }
+    Application
 }

+ 258 - 200
server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java

@@ -22,106 +22,130 @@ package password.pwm.config.stored;
 
 import password.pwm.PwmConstants;
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 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;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 
 class ConfigurationCleaner
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigurationCleaner.class );
-    private static final String NEW_PROFILE_NAME = "default";
 
-    private final StoredConfigurationImpl storedConfiguration;
-    private final XmlDocument document;
 
-    ConfigurationCleaner(
-            final StoredConfigurationImpl storedConfiguration, final XmlDocument document
-    )
-    {
-        this.storedConfiguration = storedConfiguration;
-        this.document = document;
-    }
+    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(),
+            new UpdateDeprecatedMinPwdLifetimeSetting(),
+            new UpdateDeprecatedPublicHealthSetting()
+    ) );
 
-    static void cleanup(
-            final StoredConfigurationImpl storedConfiguration, final XmlDocument document
-    )
-            throws PwmUnrecoverableException
-    {
-        new ConfigurationCleaner( storedConfiguration, document ).cleanupImpl();
-    }
 
-    static void updateMandatoryElements(
-            final StoredConfigurationImpl storedConfiguration,
+    static void preProcessXml(
             final XmlDocument document
     )
     {
-        new ConfigurationCleaner( storedConfiguration, document ).updateMandatoryElementsImpl();
+        XML_PRE_PROCESSORS.forEach( ( c ) -> PwmExceptionLoggingConsumer.wrapConsumer( c ).accept( document ) );
     }
 
 
-    private void cleanupImpl(
+    static void postProcessStoredConfig(
+            final StoredConfigurationModifier storedConfiguration
     )
-            throws PwmUnrecoverableException
     {
-        updateProperitiesWithoutType( );
-        updateMandatoryElementsImpl();
-        profilizeNonProfiledSettings( );
-        stripOrphanedProfileSettings( );
-        migrateAppProperties( );
-        updateDeprecatedSettings( );
-        migrateDeprecatedProperties( );
+        STORED_CONFIG_POST_PROCESSORS.forEach( aClass -> PwmExceptionLoggingConsumer.wrapConsumer( aClass ).accept( storedConfiguration ) );
     }
 
-
-    private void updateMandatoryElementsImpl( )
+    private static class MigratePreValueXmlElements implements PwmExceptionLoggingConsumer<XmlDocument>
     {
-        final XmlElement rootElement = document.getRootElement();
-        rootElement.setComment( Collections.singletonList( generateCommentText() ) );
+        @Override
+        public void accept( final XmlDocument xmlDocument )
+        {
+            if ( readDocVersion( xmlDocument ) >= 4 )
+            {
+                return;
+            }
 
-        rootElement.setAttribute( "pwmVersion", PwmConstants.BUILD_VERSION );
-        rootElement.setAttribute( "pwmBuild", PwmConstants.BUILD_NUMBER );
-        rootElement.setAttribute( "xmlVersion", StoredConfigurationImpl.XML_FORMAT_VERSION );
+            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 );
+                    }
+                }
+            }
+        }
+    }
 
-        // migrate old properties
+    private static class MigrateOldPropertyFormat implements PwmExceptionLoggingConsumer<XmlDocument>
+    {
+        @Override
+        public void accept( final XmlDocument xmlDocument )
         {
-
             // read correct (new) //properties[@type="config"]
-            final String configPropertiesXpath = "//" + StoredConfigurationImpl.XML_ELEMENT_PROPERTIES
-                    + "[@" + StoredConfigurationImpl.XML_ATTRIBUTE_TYPE + "=\"" + StoredConfigurationImpl.XML_ATTRIBUTE_VALUE_CONFIG + "\"]";
-            final XmlElement configPropertiesElement = document.evaluateXpathToElement( configPropertiesXpath );
+            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 = "//" + StoredConfigurationImpl.XML_ELEMENT_PROPERTIES
-                    + "[not (@" + StoredConfigurationImpl.XML_ATTRIBUTE_TYPE + ")]/" + StoredConfigurationImpl.XML_ELEMENT_PROPERTY;
-            final List<XmlElement> nonAttributedProperties = document.evaluateXpathToElements( nonAttributedPropertyXpath );
+            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 != null && nonAttributedProperties != null )
+            if ( configPropertiesElement.isPresent() && nonAttributedProperties != null )
             {
                 for ( final XmlElement element : nonAttributedProperties )
                 {
                     element.detach();
-                    configPropertiesElement.addContent( element );
+                    configPropertiesElement.get().addContent( element );
                 }
             }
 
             // remove old //properties[not (@type] element
-            final String oldPropertiesXpath = "//" + StoredConfigurationImpl.XML_ELEMENT_PROPERTIES + "[not (@" + StoredConfigurationImpl.XML_ATTRIBUTE_TYPE + ")]";
-            final List<XmlElement> oldPropertiesElements = document.evaluateXpathToElements( oldPropertiesXpath );
+            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 )
@@ -132,226 +156,260 @@ class ConfigurationCleaner
         }
     }
 
-    private String generateCommentText( )
+    static class ProfileNonProfiledSettings implements PwmExceptionLoggingConsumer<XmlDocument>
     {
-        final StringBuilder commentText = new StringBuilder();
-        commentText.append( "\t\t" ).append( " " ).append( "\n" );
-        commentText.append( "\t\t" ).append( "This configuration file has been auto-generated by the " ).append( PwmConstants.PWM_APP_NAME )
-                .append( " password self service application." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "WARNING: This configuration file contains sensitive security information, please handle with care!" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "WARNING: If a server is currently running using this configuration file, it will be restarted" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "         and the configuration updated immediately when it is modified." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "NOTICE: This file is encoded as UTF-8.  Do not save or edit this file with an editor that does not" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "        support UTF-8 encoding." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "If unable to edit using the application ConfigurationEditor web UI, the following options are available." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "   or 1. Edit this file directly by hand." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "   or 2. Remove restrictions of the configuration by setting the property 'configIsEditable' to 'true' in this file.  This will " )
-                .append( "\n" );
-        commentText.append( "\t\t" ).append( "         allow access to the ConfigurationEditor web UI without having to authenticate to an LDAP server first." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "   or 3. Remove restrictions of the configuration by using the the command line utility. " ).append( "\n" );
-        commentText.append( "\t\t" ).append( "" ).append( "\n" );
-        return commentText.toString();
-    }
-
-
-    private void profilizeNonProfiledSettings()
-            throws PwmUnrecoverableException
-    {
-        for ( final PwmSetting setting : PwmSetting.values() )
+        @Override
+        public void accept( final XmlDocument xmlDocument )
         {
-            if ( setting.getCategory().hasProfiles() )
+            final StoredConfigurationFactory.XmlInputDocumentReader reader = new StoredConfigurationFactory.XmlInputDocumentReader( xmlDocument );
+            for ( final PwmSetting setting : PwmSetting.values() )
             {
-
-                final XmlElement settingElement = storedConfiguration.getXmlHelper().xpathForSetting( setting, null );
-                if ( settingElement != null )
+                if ( setting.getCategory().hasProfiles() )
                 {
-                    settingElement.detach();
-
-                    final PwmSetting profileSetting = setting.getCategory().getProfileSetting();
-                    final List<String> profileStringDefinitions = new ArrayList<>();
+                    reader.xpathForSetting( setting, null ).ifPresent( existingSettingElement ->
                     {
-                        final StringArrayValue profileDefinitions = ( StringArrayValue ) storedConfiguration.readSetting( profileSetting );
-                        if ( profileDefinitions != null )
+                        final List<String> profileStringDefinitions = new ArrayList<>();
                         {
-                            if ( profileDefinitions.toNativeObject() != null )
+                            final List<String> configuredProfiles = reader.profilesForSetting( setting );
+                            if ( !JavaHelper.isEmpty( configuredProfiles ) )
                             {
-                                profileStringDefinitions.addAll( profileDefinitions.toNativeObject() );
+                                profileStringDefinitions.addAll( configuredProfiles );
                             }
                         }
-                    }
 
-                    if ( profileStringDefinitions.isEmpty() )
-                    {
-                        profileStringDefinitions.add( NEW_PROFILE_NAME );
-                    }
-
-                    final UserIdentity userIdentity = settingElement.getAttributeValue( StoredConfigurationImpl.XML_ATTRIBUTE_MODIFY_USER ) != null
-                            ? UserIdentity.fromDelimitedKey( settingElement.getAttributeValue(  StoredConfigurationImpl.XML_ATTRIBUTE_MODIFY_USER ) )
-                            : null;
+                        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 + "\"." );
+                        for ( final String destProfile : profileStringDefinitions )
                         {
-                            storedConfiguration.writeSetting( profileSetting, new StringArrayValue( profileStringDefinitions ), userIdentity );
+                            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 void migrateDeprecatedProperties(
-    )
-            throws PwmUnrecoverableException
+    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 = document.evaluateXpathToElements( xpathString );
-            if ( propertyElement != null && !propertyElement.isEmpty() )
             {
-                final String value = propertyElement.get( 0 ).getText();
-                storedConfiguration.writeSetting( PwmSetting.TEMPLATE_LDAP, new StringValue( value ), null );
-                propertyElement.get( 0 ).detach();
+                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 = document.evaluateXpathToElements( xpathString );
-            if ( propertyElement != null && !propertyElement.isEmpty() )
             {
-                final String value = propertyElement.get( 0 ).getText();
-                storedConfiguration.writeSetting( PwmSetting.NOTES, new StringValue( value ), null );
-                propertyElement.get( 0 ).detach();
+                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 void updateProperitiesWithoutType()
+    private static class UpdatePropertiesWithoutType implements PwmExceptionLoggingConsumer<XmlDocument>
     {
-        final String xpathString = "//properties[not(@type)]";
-        final List<XmlElement> propertiesElements = document.evaluateXpathToElements( xpathString );
-        for ( final XmlElement propertiesElement : propertiesElements )
+        @Override
+        public void accept( final XmlDocument xmlDocument )
         {
-            propertiesElement.setAttribute( StoredConfigurationImpl.XML_ATTRIBUTE_TYPE, StoredConfigurationImpl.XML_ATTRIBUTE_VALUE_CONFIG );
+            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 void stripOrphanedProfileSettings()
+    private static class AppPropertyOverrideMigration implements PwmExceptionLoggingConsumer<XmlDocument>
     {
-        for ( final PwmSetting setting : PwmSetting.values() )
+        @Override
+        public void accept( final XmlDocument xmlDocument ) throws PwmUnrecoverableException
         {
-            if ( setting.getCategory().hasProfiles() )
+            final StoredConfigurationFactory.XmlInputDocumentReader documentReader = new StoredConfigurationFactory.XmlInputDocumentReader( xmlDocument );
+            final List<XmlElement> appPropertiesElements = documentReader.xpathForAppProperties();
+            for ( final XmlElement element : appPropertiesElements )
             {
-                final List<String> validProfiles = storedConfiguration.profilesForSetting( setting );
-                final String xpathString = "//setting[@key=\"" + setting.getKey() + "\"]";
-                final List<XmlElement> settingElements =  document.evaluateXpathToElements( xpathString );
-                for ( final XmlElement settingElement : settingElements )
+                final List<XmlElement> properties = element.getChildren();
+                for ( final XmlElement property : properties )
                 {
-                    final String profileID = settingElement.getAttributeValue( StoredConfigurationImpl.XML_ATTRIBUTE_PROFILE );
-                    if ( profileID != null )
+                    final String key = property.getAttributeValue( "key" );
+                    final String value = property.getText();
+                    if ( key != null && !key.isEmpty() && value != null && !value.isEmpty() )
                     {
-                        if ( !validProfiles.contains( profileID ) )
+                        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<>();
                         {
-                            LOGGER.info( () -> "removing setting " + setting.getKey() + " with profile \"" + profileID + "\", profile is not a valid profile" );
-                            settingElement.detach();
+                            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 void migrateAppProperties(
-    )
-            throws PwmUnrecoverableException
+    private static class UpdateDeprecatedAdComplexitySettings implements PwmExceptionLoggingConsumer<StoredConfigurationModifier>
     {
-        final List<XmlElement> appPropertiesElements = storedConfiguration.getXmlHelper().xpathForAppProperties();
-        for ( final XmlElement element : appPropertiesElements )
+        @Override
+        public void accept( final StoredConfigurationModifier modifier )
+                throws PwmUnrecoverableException
         {
-            final List<XmlElement> properties = element.getChildren();
-            for ( final XmlElement property : properties )
+            final StoredConfiguration oldConfig = modifier.newStoredConfiguration();
+            final Configuration configuration = new Configuration( oldConfig );
+            for ( final String profileID : configuration.getPasswordProfileIDs() )
             {
-                final String key = property.getAttributeValue( "key" );
-                final String value = property.getText();
-                if ( key != null && !key.isEmpty() && value != null && !value.isEmpty() )
+                if ( !oldConfig.isDefaultValue( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ) )
                 {
-                    LOGGER.info( () -> "migrating app-property config element '" + key + "' to setting " + PwmSetting.APP_PROPERTY_OVERRIDES.getKey() );
-                    final String newValue = key + "=" + value;
-                    List<String> existingValues = ( List<String> ) storedConfiguration.readSetting( PwmSetting.APP_PROPERTY_OVERRIDES ).toNativeObject();
-                    if ( existingValues == null )
+                    final boolean ad2003Enabled = ( boolean ) oldConfig.readSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ).toNativeObject();
+                    final StoredValue value;
+                    if ( ad2003Enabled )
+                    {
+                        value = new StringValue( ADPolicyComplexity.AD2003.toString() );
+                    }
+                    else
                     {
-                        existingValues = new ArrayList<>();
+                        value = new StringValue( ADPolicyComplexity.NONE.toString() );
                     }
-                    existingValues = new ArrayList<>( existingValues );
-                    existingValues.add( newValue );
-                    storedConfiguration.writeSetting( PwmSetting.APP_PROPERTY_OVERRIDES, new StringArrayValue( existingValues ), null );
+                    LOGGER.info( () -> "converting deprecated non-default setting " + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY.getKey() + "/" + profileID
+                            + " to replacement setting " + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL + ", value=" + value.toNativeObject().toString() );
+                    final Optional<ValueMetaData> valueMetaData = oldConfig.readMetaData(
+                            StoredConfigItemKey.fromSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ) );
+                    final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
+                    modifier.writeSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL, profileID, value, userIdentity );
                 }
             }
-            element.detach();
         }
     }
 
-    private void updateDeprecatedSettings( ) throws PwmUnrecoverableException
+    private static class UpdateDeprecatedMinPwdLifetimeSetting implements PwmExceptionLoggingConsumer<StoredConfigurationModifier>
     {
-        final UserIdentity actor = new UserIdentity( "UpgradeProcessor", null );
-        for ( final String profileID : storedConfiguration.profilesForSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY ) )
+        @Override
+        public void accept( final StoredConfigurationModifier modifier )
+                throws PwmUnrecoverableException
         {
-            if ( !storedConfiguration.isDefaultValue( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ) )
+            final StoredConfiguration oldConfig = modifier.newStoredConfiguration();
+            for ( final String profileID : oldConfig.profilesForSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME ) )
             {
-                final boolean ad2003Enabled = ( boolean ) storedConfiguration.readSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ).toNativeObject();
-                final StoredValue value;
-                if ( ad2003Enabled )
+                if ( !oldConfig.isDefaultValue( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ) )
                 {
-                    value = new StringValue( ADPolicyComplexity.AD2003.toString() );
-                }
-                else
-                {
-                    value = new StringValue( ADPolicyComplexity.NONE.toString() );
+                    final boolean enforceEnabled = ( boolean ) oldConfig.readSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ).toNativeObject();
+                    final StoredValue value = enforceEnabled
+                            ? new StringValue( RecoveryMinLifetimeOption.NONE.name() )
+                            : new StringValue( RecoveryMinLifetimeOption.ALLOW.name() );
+                    final ValueMetaData existingData = oldConfig.readSettingMetadata( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID );
+                    final UserIdentity newActor = existingData != null && existingData.getUserIdentity() != null
+                            ? existingData.getUserIdentity()
+                            : null;
+                    LOGGER.info( () -> "converting deprecated non-default setting "
+                            + PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) + "/" + profileID
+                            + " to replacement setting " + PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE )
+                            + ", value=" + value.toNativeObject().toString() );
+                    modifier.writeSetting( PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS, profileID, value, newActor );
                 }
-                LOGGER.warn( "converting deprecated non-default setting " + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY.getKey() + "/" + profileID
-                        + " to replacement setting " + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL + ", value=" + value.toNativeObject().toString() );
-                storedConfiguration.writeSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL, profileID, value, actor );
-                storedConfiguration.resetSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID, actor );
             }
         }
+    }
 
-        for ( final String profileID : storedConfiguration.profilesForSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME ) )
+    private static class UpdateDeprecatedPublicHealthSetting implements PwmExceptionLoggingConsumer<StoredConfigurationModifier>
+    {
+        @Override
+        public void accept( final StoredConfigurationModifier modifier )
+                throws PwmUnrecoverableException
         {
-            if ( !storedConfiguration.isDefaultValue( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ) )
+            final StoredConfiguration oldConfig = modifier.newStoredConfiguration();
+            if ( !oldConfig.isDefaultValue( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null ) )
             {
-                final boolean enforceEnabled = ( boolean ) storedConfiguration.readSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ).toNativeObject();
-                final StoredValue value = enforceEnabled
-                        ? new StringValue( "NONE" )
-                        : new StringValue( "ALLOW" );
-                final ValueMetaData existingData = storedConfiguration.readSettingMetadata( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID );
-                LOGGER.warn( "converting deprecated non-default setting "
-                        + PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) + "/" + profileID
-                        + " to replacement setting " + PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE )
-                        + ", value=" + value.toNativeObject().toString() );
-                final UserIdentity newActor = existingData != null && existingData.getUserIdentity() != null
-                        ? existingData.getUserIdentity()
-                        : actor;
-                storedConfiguration.writeSetting( PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS, profileID, value, newActor );
-                storedConfiguration.resetSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID, actor );
+                LOGGER.info( () -> "converting deprecated non-default setting "
+                        + PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE )
+                        + " to replacement setting " + PwmSetting.WEBSERVICES_PUBLIC_ENABLE.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) );
+                final Set<String> existingValues = ( Set<String> ) oldConfig.readSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null ).toNativeObject();
+                final Set<String> newValues = new LinkedHashSet<>( existingValues );
+                newValues.add( WebServiceUsage.Health.name() );
+                newValues.add( WebServiceUsage.Statistics.name() );
+
+                final Optional<ValueMetaData> valueMetaData = oldConfig.readMetaData(
+                        StoredConfigItemKey.fromSetting( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null ) );
+                final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
+
+                modifier.writeSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null, new OptionListValue( newValues ), userIdentity );
             }
         }
+    }
 
-        if ( !storedConfiguration.isDefaultValue( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES ) )
-        {
-            LOGGER.warn( "converting deprecated non-default setting "
-                    + PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE )
-                    + " to replacement setting " + PwmSetting.WEBSERVICES_PUBLIC_ENABLE.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) );
-            final Set<String> existingValues = (Set<String>) storedConfiguration.readSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE ).toNativeObject();
-            final Set<String> newValues = new LinkedHashSet<>( existingValues );
-            newValues.add( WebServiceUsage.Health.name() );
-            newValues.add( WebServiceUsage.Statistics.name() );
-            storedConfiguration.writeSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null, new OptionListValue( newValues ), actor );
-            storedConfiguration.resetSetting( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null, actor );
-        }
+    private static int readDocVersion( final XmlDocument xmlDocument )
+    {
+        final String xmlVersionStr = xmlDocument.getRootElement().getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_XML_VERSION );
+        return JavaHelper.silentParseInt( xmlVersionStr, 0 );
     }
 }

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

@@ -27,8 +27,8 @@ public enum ConfigurationProperty
     LDAP_TEMPLATE( "configTemplate" ),
     NOTES( "notes" ),
     PASSWORD_HASH( "configPasswordHash" ),
-    CONFIG_ON_START( "saveConfigOnStart" ),
-    MODIFIFICATION_TIMESTAMP( "modificationTimestamp" ),
+    STORE_PLAINTEXT_VALUES( "storePlaintextValues" ),
+    MODIFICATION_TIMESTAMP( "modificationTimestamp" ),
     IMPORT_LDAP_CERTIFICATES( "importLdapCertificates" ),;
 
     private final String key;

+ 138 - 63
server/src/main/java/password/pwm/config/stored/ConfigurationReader.java

@@ -25,13 +25,17 @@ import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
+import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
+import password.pwm.config.StoredValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.svc.event.AuditEvent;
+import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.util.java.FileSystemUtility;
-import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -45,6 +49,8 @@ import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
 import java.time.Instant;
 import java.util.List;
+import java.util.Optional;
+import java.util.Set;
 
 /**
  * Read the PWM configuration.
@@ -58,9 +64,10 @@ public class ConfigurationReader
     private final File configFile;
     private final String configFileChecksum;
     private Configuration configuration;
-    private StoredConfigurationImpl storedConfiguration;
+    private StoredConfiguration storedConfiguration;
     private ErrorInformation configFileError;
 
+
     private PwmApplicationMode configMode = PwmApplicationMode.NEW;
 
     private volatile boolean saveInProgress;
@@ -75,7 +82,7 @@ public class ConfigurationReader
             this.storedConfiguration = readStoredConfig();
             this.configFileError = null;
         }
-        catch ( PwmUnrecoverableException e )
+        catch ( final PwmUnrecoverableException e )
         {
             this.configFileError = e.getErrorInformation();
             LOGGER.warn( "error reading configuration file: " + e.getMessage() );
@@ -83,7 +90,7 @@ public class ConfigurationReader
 
         if ( storedConfiguration == null )
         {
-            this.storedConfiguration = StoredConfigurationImpl.newStoredConfiguration();
+            this.storedConfiguration = StoredConfigurationFactory.newConfig();
         }
 
         LOGGER.debug( () -> "configuration mode: " + configMode );
@@ -94,7 +101,7 @@ public class ConfigurationReader
         return configMode;
     }
 
-    public StoredConfigurationImpl getStoredConfiguration( )
+    public StoredConfiguration getStoredConfiguration( )
     {
         return storedConfiguration;
     }
@@ -103,19 +110,15 @@ public class ConfigurationReader
     {
         if ( configuration == null )
         {
-            final StoredConfigurationImpl newStoredConfig = this.storedConfiguration == null
-                    ? StoredConfigurationImpl.newStoredConfiguration()
+            final StoredConfiguration newStoredConfig = this.storedConfiguration == null
+                    ? StoredConfigurationFactory.newConfig()
                     : this.storedConfiguration;
             configuration = new Configuration( newStoredConfig );
-            if ( storedConfiguration != null )
-            {
-                storedConfiguration.lock();
-            }
         }
         return configuration;
     }
 
-    private StoredConfigurationImpl readStoredConfig( ) throws PwmUnrecoverableException
+    private StoredConfiguration readStoredConfig( ) throws PwmUnrecoverableException
     {
         LOGGER.debug( () -> "loading configuration file: " + configFile );
 
@@ -126,12 +129,34 @@ 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
         {
             theFileData = Files.newInputStream( configFile.toPath() );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             final String errorMsg = "unable to read configuration file: " + e.getMessage();
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
@@ -143,13 +168,12 @@ public class ConfigurationReader
             throw new PwmUnrecoverableException( errorInformation );
         }
 
-        final StoredConfigurationImpl storedConfiguration;
+        final StoredConfiguration storedConfiguration;
         try
         {
-            storedConfiguration = StoredConfigurationImpl.fromXml( theFileData );
-            //restoredConfiguration = (new NGStoredConfigurationFactory()).fromXml(theFileData);
+            storedConfiguration = StoredConfigurationFactory.fromXml( theFileData );
         }
-        catch ( PwmUnrecoverableException e )
+        catch ( final PwmUnrecoverableException e )
         {
             final String errorMsg = "unable to parse configuration file: " + e.getMessage();
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
@@ -162,8 +186,8 @@ public class ConfigurationReader
             throw new PwmUnrecoverableException( errorInformation );
         }
 
-        final List<String> validationErrorMsgs = storedConfiguration.validateValues();
-        if ( validationErrorMsgs != null && !validationErrorMsgs.isEmpty() )
+        final List<String> validationErrorMsgs = StoredConfigurationUtil.validateValues( storedConfiguration );
+        if ( !JavaHelper.isEmpty( validationErrorMsgs ) )
         {
             final String errorMsg = "value error in config file, please investigate: " + validationErrorMsgs.get( 0 );
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
@@ -175,8 +199,8 @@ public class ConfigurationReader
             throw new PwmUnrecoverableException( errorInformation );
         }
 
-        final String configIsEditable = storedConfiguration.readConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE );
-        if ( PwmConstants.TRIAL_MODE || ( configIsEditable != null && "true".equalsIgnoreCase( configIsEditable ) ) )
+        final Optional<String> configIsEditable = storedConfiguration.readConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE );
+        if ( PwmConstants.TRIAL_MODE || ( configIsEditable.isPresent() && "true".equalsIgnoreCase( configIsEditable.get() ) ) )
         {
             this.configMode = PwmApplicationMode.CONFIGURATION;
         }
@@ -189,11 +213,13 @@ public class ConfigurationReader
         final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
         LOGGER.debug( () -> "configuration reading/parsing of " + fileSize + " complete in " + timeDuration.asLongString() );
 
+
+
         return storedConfiguration;
     }
 
     public void saveConfiguration(
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfiguration storedConfiguration,
             final PwmApplication pwmApplication,
             final SessionLabel sessionLabel
     )
@@ -216,18 +242,21 @@ public class ConfigurationReader
 
         {
             // increment the config epoch
-            String epochStrValue = storedConfiguration.readConfigProperty( ConfigurationProperty.CONFIG_EPOCH );
+            String newEpochStrValue = "0";
             try
             {
-                final BigInteger epochValue = epochStrValue == null || epochStrValue.length() < 0 ? BigInteger.ZERO : new BigInteger( epochStrValue );
-                epochStrValue = epochValue.add( BigInteger.ONE ).toString();
+                final Optional<String> storedEpochStrValue = storedConfiguration.readConfigProperty( ConfigurationProperty.CONFIG_EPOCH );
+                final BigInteger epochValue = storedEpochStrValue.map( BigInteger::new ).orElse( BigInteger.ZERO );
+                newEpochStrValue = epochValue.add( BigInteger.ONE ).toString();
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.error( sessionLabel, "error trying to parse previous config epoch property: " + e.getMessage() );
-                epochStrValue = "0";
             }
-            storedConfiguration.writeConfigProperty( ConfigurationProperty.CONFIG_EPOCH, epochStrValue );
+
+            final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+            modifier.writeConfigProperty( ConfigurationProperty.CONFIG_EPOCH, newEpochStrValue );
+            this.storedConfiguration = modifier.newStoredConfiguration();
         }
 
         if ( backupDirectory != null && !backupDirectory.exists() )
@@ -239,51 +268,97 @@ public class ConfigurationReader
             }
         }
 
-        try
+        if ( pwmApplication != null && pwmApplication.getAuditManager() != null )
         {
-            final File tempWriteFile = new File( configFile.getAbsoluteFile() + ".new" );
-            LOGGER.info( sessionLabel, () -> "beginning write to configuration file " + tempWriteFile );
-            saveInProgress = true;
-
-            try ( FileOutputStream fileOutputStream = new FileOutputStream( tempWriteFile, false ) )
-            {
-                storedConfiguration.toXml( fileOutputStream );
-            }
+            auditModifiedSettings( pwmApplication, storedConfiguration, sessionLabel );
+        }
 
-            LOGGER.info( () -> "saved configuration " + JsonUtil.serialize( storedConfiguration.toJsonDebugObject() ) );
-            if ( pwmApplication != null )
-            {
-                final String actualChecksum = storedConfiguration.settingChecksum();
-                pwmApplication.writeAppAttribute( PwmApplication.AppAttribute.CONFIG_HASH, actualChecksum );
-            }
+        try
+        {
+            outputConfigurationFile( storedConfiguration, pwmApplication, sessionLabel, backupRotations, backupDirectory );
+        }
+        finally
+        {
+            saveInProgress = false;
+        }
+    }
 
-            LOGGER.trace( () -> "renaming file " + tempWriteFile.getAbsolutePath() + " to " + configFile.getAbsolutePath() );
-            try
-            {
-                Files.move( tempWriteFile.toPath(), configFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE );
-            }
-            catch ( Exception e )
-            {
-                final String errorMsg = "unable to rename temporary save file from " + tempWriteFile.getAbsolutePath()
-                        + " to " + configFile.getAbsolutePath() + "; error: " + e.getMessage();
-                throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg ) );
-            }
+    private static void auditModifiedSettings( final PwmApplication pwmApplication, final StoredConfiguration newConfig, final SessionLabel sessionLabel )
+            throws PwmUnrecoverableException
+    {
+        final Set<StoredConfigItemKey> changedKeys = StoredConfigurationUtil.changedValues( newConfig, pwmApplication.getConfig().getStoredConfiguration() );
 
-            if ( backupDirectory != null )
+        for ( final StoredConfigItemKey key : changedKeys )
+        {
+            if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING
+                    || key.getRecordType() == StoredConfigItemKey.RecordType.LOCALE_BUNDLE )
             {
-                final String configFileName = configFile.getName();
-                final String backupFilePath = backupDirectory.getAbsolutePath() + File.separatorChar + configFileName + "-backup";
-                final File backupFile = new File( backupFilePath );
-                FileSystemUtility.rotateBackups( backupFile, backupRotations );
-                try ( FileOutputStream fileOutputStream = new FileOutputStream( backupFile, false ) )
+                final Optional<StoredValue> storedValue = newConfig.readStoredValue( key );
+                if ( storedValue.isPresent() )
                 {
-                    storedConfiguration.toXml( fileOutputStream );
+                    final Optional<ValueMetaData> valueMetaData = newConfig.readMetaData( key );
+                    final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
+                    final String modifyMessage = "configuration record '" + key.getLabel( PwmConstants.DEFAULT_LOCALE )
+                            + "' has been modified, new value: " + storedValue.get().toDebugString( PwmConstants.DEFAULT_LOCALE );
+                    pwmApplication.getAuditManager().submit( new AuditRecordFactory( pwmApplication ).createUserAuditRecord(
+                            AuditEvent.MODIFY_CONFIGURATION,
+                            userIdentity,
+                            sessionLabel,
+                            modifyMessage
+                    ) );
                 }
             }
         }
-        finally
+    }
+
+    private void outputConfigurationFile(
+            final StoredConfiguration storedConfiguration,
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final int backupRotations,
+            final File backupDirectory
+    )
+            throws IOException, PwmUnrecoverableException
+    {
+        final Instant saveFileStartTime = Instant.now();
+        final File tempWriteFile = new File( configFile.getAbsoluteFile() + ".new" );
+        LOGGER.info( sessionLabel, () -> "beginning write to configuration file " + tempWriteFile );
+        saveInProgress = true;
+
+        try ( FileOutputStream fileOutputStream = new FileOutputStream( tempWriteFile, false ) )
         {
-            saveInProgress = false;
+            StoredConfigurationFactory.toXml( storedConfiguration, fileOutputStream );
+        }
+
+        LOGGER.info( () -> "saved configuration in " + TimeDuration.compactFromCurrent( saveFileStartTime ) );
+        if ( pwmApplication != null )
+        {
+            final String actualChecksum = storedConfiguration.valueHash();
+            pwmApplication.writeAppAttribute( PwmApplication.AppAttribute.CONFIG_HASH, actualChecksum );
+        }
+
+        LOGGER.trace( () -> "renaming file " + tempWriteFile.getAbsolutePath() + " to " + configFile.getAbsolutePath() );
+        try
+        {
+            Files.move( tempWriteFile.toPath(), configFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE );
+        }
+        catch ( final Exception e )
+        {
+            final String errorMsg = "unable to rename temporary save file from " + tempWriteFile.getAbsolutePath()
+                    + " to " + configFile.getAbsolutePath() + "; error: " + e.getMessage();
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg ) );
+        }
+
+        if ( backupDirectory != null )
+        {
+            final String configFileName = configFile.getName();
+            final String backupFilePath = backupDirectory.getAbsolutePath() + File.separatorChar + configFileName + "-backup";
+            final File backupFile = new File( backupFilePath );
+            FileSystemUtility.rotateBackups( backupFile, backupRotations );
+            try ( FileOutputStream fileOutputStream = new FileOutputStream( backupFile, false ) )
+            {
+                StoredConfigurationFactory.toXml( storedConfiguration, fileOutputStream );
+            }
         }
     }
 

+ 70 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigData.java

@@ -0,0 +1,70 @@
+/*
+ * 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 lombok.Builder;
+import lombok.Singular;
+import lombok.Value;
+import password.pwm.config.StoredValue;
+
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Value
+@Builder( toBuilder = true )
+class StoredConfigData
+{
+    @Builder.Default
+    private String createTime = "";
+
+    @Builder.Default
+    private Instant modifyTime = Instant.now();
+
+    @Singular
+    private Map<StoredConfigItemKey, StoredValue> storedValues;
+
+    @Singular
+    private Map<StoredConfigItemKey, ValueMetaData> metaDatas;
+
+    @Value
+    static class ValueAndMetaCarrier
+    {
+        private final StoredConfigItemKey key;
+        private final StoredValue value;
+        private final ValueMetaData metaData;
+    }
+
+    static Map<StoredConfigItemKey, ValueMetaData> carrierAsMetaDataMap( final Collection<ValueAndMetaCarrier> input )
+    {
+        return input.stream()
+                .filter( ( t ) -> t.getKey() != null && t.getMetaData() != null )
+                .collect( Collectors.toMap( StoredConfigData.ValueAndMetaCarrier::getKey, StoredConfigData.ValueAndMetaCarrier::getMetaData ) );
+    }
+
+    static Map<StoredConfigItemKey, StoredValue> carrierAsStoredValueMap( final Collection<ValueAndMetaCarrier> input )
+    {
+        return input.stream()
+                .filter( ( t ) -> t.getKey() != null && t.getValue() != null )
+                .collect( Collectors.toMap( StoredConfigData.ValueAndMetaCarrier::getKey, StoredConfigData.ValueAndMetaCarrier::getValue ) );
+    }
+}

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

@@ -0,0 +1,235 @@
+/*
+ * 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.config.PwmSetting;
+import password.pwm.i18n.Config;
+import password.pwm.i18n.PwmLocaleBundle;
+import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+
+import java.io.Serializable;
+import java.util.Locale;
+import java.util.Objects;
+
+public class StoredConfigItemKey implements Serializable, Comparable
+{
+    public enum RecordType
+    {
+        SETTING( "Setting" ),
+        LOCALE_BUNDLE ( "Localization" ),
+        PROPERTY ( "Property" ),;
+
+        private final String label;
+
+        RecordType( final String label )
+        {
+            this.label = label;
+        }
+
+        public String getLabel()
+        {
+            return label;
+        }
+    }
+
+    private final RecordType recordType;
+    private final String recordID;
+    private final String profileID;
+
+    private StoredConfigItemKey( final RecordType recordType, final String recordID, final String profileID )
+    {
+        Objects.requireNonNull( recordType, "recordType can not be null" );
+        Objects.requireNonNull( recordID, "recordID can not be null" );
+
+        this.recordType = recordType;
+        this.recordID = recordID;
+        this.profileID = profileID;
+    }
+
+    public RecordType getRecordType()
+    {
+        return recordType;
+    }
+
+    public String getRecordID()
+    {
+        return recordID;
+    }
+
+    public String getProfileID()
+    {
+        return profileID;
+    }
+
+    static StoredConfigItemKey fromSetting( final PwmSetting pwmSetting, final String profileID )
+    {
+        return new StoredConfigItemKey( RecordType.SETTING, pwmSetting.getKey(), profileID );
+    }
+
+    static StoredConfigItemKey fromLocaleBundle( final PwmLocaleBundle localeBundle, final String key )
+    {
+        return new StoredConfigItemKey( RecordType.LOCALE_BUNDLE, localeBundle.getKey(), key );
+    }
+
+    static StoredConfigItemKey fromConfigurationProperty( final ConfigurationProperty configurationProperty )
+    {
+        return new StoredConfigItemKey( RecordType.PROPERTY, configurationProperty.getKey(), null );
+    }
+
+    public boolean isValid()
+    {
+        try
+        {
+            validate();
+            return true;
+        }
+        catch ( final IllegalStateException e )
+        {
+            /* ignore */
+        }
+        return false;
+    }
+
+    public void validate()
+    {
+        switch ( recordType )
+        {
+            case SETTING:
+            {
+                final PwmSetting pwmSetting = this.toPwmSetting();
+                final boolean hasProfileID = !StringUtil.isEmpty( profileID );
+                if ( pwmSetting.getCategory().hasProfiles() && !hasProfileID )
+                {
+                    throw new IllegalStateException( "profileID is required for setting " + pwmSetting.getKey() );
+                }
+                else if ( !pwmSetting.getCategory().hasProfiles() && hasProfileID )
+                {
+                    throw new IllegalStateException( "profileID is not required for setting " + pwmSetting.getKey() );
+                }
+            }
+            break;
+
+            case LOCALE_BUNDLE:
+            {
+                Objects.requireNonNull( profileID, "profileID is required when recordType is LOCALE_BUNDLE" );
+                final PwmLocaleBundle pwmLocaleBundle = toLocaleBundle();
+                if ( !pwmLocaleBundle.getDisplayKeys().contains( profileID ) )
+                {
+                    throw new IllegalStateException( "key '" + profileID + "' is unrecognized for locale bundle " + pwmLocaleBundle.name() );
+                }
+            }
+            break;
+
+            case PROPERTY:
+                break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( recordType );
+        }
+    }
+
+    public String getLabel( final Locale locale )
+    {
+        final String separator = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
+
+        switch ( recordType )
+        {
+            case SETTING:
+                return recordType.getLabel() + separator + toPwmSetting().toMenuLocationDebug( profileID, locale );
+
+            case PROPERTY:
+                return recordType.getLabel() + separator + this.getRecordID();
+
+            case LOCALE_BUNDLE:
+                return recordType.getLabel()
+                        + separator
+                        + this.getRecordID()
+                        + separator
+                        + this.getProfileID();
+
+            default:
+                JavaHelper.unhandledSwitchStatement( recordType );
+        }
+
+        throw new IllegalStateException(  );
+    }
+
+    public PwmSetting toPwmSetting()
+    {
+        if ( getRecordType() != RecordType.SETTING )
+        {
+            throw new IllegalStateException( "attempt to read pwmSetting key for non-setting ConfigItemKey" );
+        }
+
+        return PwmSetting.forKey( this.recordID );
+    }
+
+    public PwmLocaleBundle toLocaleBundle()
+    {
+        if ( getRecordType() != RecordType.LOCALE_BUNDLE )
+        {
+            throw new IllegalStateException( "attempt to read PwmLocaleBundle key for non-locale ConfigItemKey" );
+        }
+
+        return PwmLocaleBundle.forKey( this.recordID )
+                .orElseThrow( () -> new IllegalStateException( "unexpected key value for locale bundle: " + this.recordID ) );
+    }
+
+    public ConfigurationProperty toConfigurationProperty()
+    {
+        if ( getRecordType() != RecordType.PROPERTY )
+        {
+            throw new IllegalStateException( "attempt to read ConfigurationProperty key for non-config property ConfigItemKey" );
+        }
+
+        return ConfigurationProperty.valueOf( recordID );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return toString().hashCode();
+    }
+
+    @Override
+    public boolean equals( final Object anotherObject )
+    {
+        return anotherObject != null
+                && anotherObject instanceof StoredConfigItemKey
+                && toString().equals( anotherObject.toString() );
+    }
+
+    @Override
+    public String toString()
+    {
+        return getLabel( PwmConstants.DEFAULT_LOCALE );
+       // return getRecordType().name() + "-" + this.getRecordID() + "-" + this.getProfileID();
+    }
+
+    @Override
+    public int compareTo( final Object o )
+    {
+        return getLabel( PwmConstants.DEFAULT_LOCALE ).compareTo( o.toString() );
+    }
+}

+ 0 - 67
server/src/main/java/password/pwm/config/stored/StoredConfigReferenceBean.java

@@ -1,67 +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;
-
-import lombok.Value;
-
-import java.io.Serializable;
-
-@Value
-public class StoredConfigReferenceBean implements StoredConfigReference, Serializable, Comparable
-{
-    private RecordType recordType;
-    private String recordID;
-    private String profileID;
-
-    public StoredConfigReferenceBean( final RecordType type, final String recordID, final String profileID )
-    {
-        if ( type == null )
-        {
-            throw new NullPointerException( "recordType can not be null" );
-        }
-
-        if ( recordID == null )
-        {
-            throw new NullPointerException( "recordID can not be null" );
-        }
-
-        this.recordType = type;
-        this.recordID = recordID;
-        this.profileID = profileID;
-    }
-
-
-    @Override
-    public String toString( )
-    {
-        return this.getRecordType().toString()
-                + "-"
-                + ( this.getProfileID() == null ? "" : this.getProfileID() )
-                + "-"
-                + this.getRecordID();
-    }
-
-    @Override
-    public int compareTo( final Object o )
-    {
-        return toString().compareTo( o.toString() );
-    }
-}

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

@@ -0,0 +1,50 @@
+/*
+ * 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";
+}

+ 18 - 51
server/src/main/java/password/pwm/config/stored/StoredConfiguration.java

@@ -20,81 +20,48 @@
 
 package password.pwm.config.stored;
 
-import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.PwmSettingCategory;
+import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.StoredValue;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.secure.PwmSecurityKey;
 
 import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 
 public interface StoredConfiguration
 {
-    String XML_ELEMENT_ROOT = "PwmConfiguration";
-    String XML_ELEMENT_PROPERTIES = "properties";
-    String XML_ELEMENT_PROPERTY = "property";
-    String XML_ELEMENT_SETTINGS = "settings";
-    String XML_ELEMENT_SETTING = "setting";
-    String XML_ELEMENT_DEFAULT = "default";
-    String XML_ELEMENT_LOCALEBUNDLE = "localeBundle";
-
-    String XML_ATTRIBUTE_TYPE = "type";
-    String XML_ATTRIBUTE_KEY = "key";
-    String XML_ATTRIBUTE_SYNTAX = "syntax";
-    String XML_ATTRIBUTE_PROFILE = "profile";
-    String XML_ATTRIBUTE_VALUE_APP = "app";
-    String XML_ATTRIBUTE_VALUE_CONFIG = "config";
-    String XML_ATTRIBUTE_CREATE_TIME = "createTime";
-    String XML_ATTRIBUTE_MODIFY_TIME = "modifyTime";
-    String XML_ATTRIBUTE_MODIFY_USER = "modifyUser";
-    String XML_ATTRIBUTE_MODIFY_USER_PROFILE = "modifyUserProfile";
-    String XML_ATTRIBUTE_SYNTAX_VERSION = "syntaxVersion";
-    String XML_ATTRIBUTE_BUNDLE = "bundle";
-
-
     PwmSecurityKey getKey( ) throws PwmUnrecoverableException;
 
-    Instant modifyTime( );
+    String createTime();
 
-    boolean isLocked( );
+    Instant modifyTime( );
 
-    String readConfigProperty( ConfigurationProperty propertyName );
+    Optional<String> readConfigProperty( ConfigurationProperty propertyName );
 
-    void writeConfigProperty(
-            ConfigurationProperty propertyName,
-            String value
-    );
+    PwmSettingTemplateSet getTemplateSet();
 
-    void lock( );
+    List<String> profilesForSetting( PwmSetting pwmSetting );
 
     ValueMetaData readSettingMetadata( PwmSetting setting, String profileID );
 
-    void resetSetting( PwmSetting setting, String profileID, UserIdentity userIdentity );
+    Map<String, String> readLocaleBundleMap( PwmLocaleBundle bundleName, String keyName );
 
-    boolean isDefaultValue( PwmSetting setting );
+    StoredValue readSetting( PwmSetting setting, String profileID );
 
     boolean isDefaultValue( PwmSetting setting, String profileID );
 
-    StoredValue readSetting( PwmSetting setting );
-
-    StoredValue readSetting( PwmSetting setting, String profileID );
-
-    void copyProfileID( PwmSettingCategory category, String sourceID, String destinationID, UserIdentity userIdentity )
-            throws PwmUnrecoverableException;
+    String valueHash();
 
-    void writeSetting(
-            PwmSetting setting,
-            StoredValue value,
-            UserIdentity userIdentity
-    ) throws PwmUnrecoverableException;
+    Set<StoredConfigItemKey> modifiedItems();
 
-    void writeSetting(
-            PwmSetting setting,
-            String profileID,
-            StoredValue value,
-            UserIdentity userIdentity
-    ) throws PwmUnrecoverableException;
+    Optional<ValueMetaData> readMetaData( StoredConfigItemKey storedConfigItemKey );
 
+    Optional<StoredValue> readStoredValue( StoredConfigItemKey storedConfigItemKey );
 
+    StoredConfiguration copy();
 }

+ 671 - 3
server/src/main/java/password/pwm/config/stored/StoredConfigurationFactory.java

@@ -20,14 +20,682 @@
 
 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;
 
-interface StoredConfigurationFactory
+public class StoredConfigurationFactory
 {
-    StoredConfiguration fromXml( InputStream inputStream ) throws PwmUnrecoverableException;
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationFactory.class );
 
-    void toXml( OutputStream outputStream );
+    private static final String XML_FORMAT_VERSION = "5";
+
+
+    public static StoredConfiguration newConfig() 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.newStoredConfiguration();
+    }
+
+    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;
+    }
+
+    public static StoredConfiguration fromXml( final InputStream inputStream )
+            throws PwmUnrecoverableException
+    {
+        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 ) );
+        ConfigurationCleaner.postProcessStoredConfig( modifier );
+
+        return modifier.newStoredConfiguration();
+    }
+
+    public static void toXml(
+            final StoredConfiguration storedConfiguration,
+            final OutputStream outputStream
+    )
+            throws PwmUnrecoverableException, IOException
+    {
+        toXml( storedConfiguration, outputStream, OutputSettings.builder().build() );
+    }
+
+    public static void toXml(
+            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();
+            }
+
+            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 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
+    public static class OutputSettings
+    {
+        @Builder.Default
+        private SecureOutputMode mode = SecureOutputMode.NORMAL;
+
+        public enum SecureOutputMode
+        {
+            NORMAL,
+            STRIPPED,
+        }
+    }
 }
+

+ 124 - 1418
server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java

@@ -20,429 +20,163 @@
 
 package password.pwm.config.stored;
 
-import password.pwm.AppProperty;
-import password.pwm.PwmConstants;
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.PwmSettingCategory;
-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.NamedSecretValue;
-import password.pwm.config.value.PasswordValue;
-import password.pwm.config.value.PrivateKeyValue;
-import password.pwm.config.value.StringArrayValue;
-import password.pwm.config.value.ValueFactory;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmException;
-import password.pwm.error.PwmOperationalException;
+import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.StringValue;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.i18n.Config;
 import password.pwm.i18n.PwmLocaleBundle;
-import password.pwm.util.PasswordData;
-import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.JsonUtil;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.java.TimeDuration;
-import password.pwm.util.java.XmlDocument;
-import password.pwm.util.java.XmlElement;
-import password.pwm.util.java.XmlFactory;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.BCrypt;
-import password.pwm.util.secure.PwmRandom;
+import password.pwm.util.secure.HmacAlgorithm;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.SecureEngine;
 
-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.Arrays;
-import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
-import java.util.Queue;
-import java.util.ResourceBundle;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
-import java.util.SortedSet;
 import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.stream.Collectors;
+import java.util.function.Supplier;
 
 /**
+ * Immutable in-memory configuration.
+ *
  * @author Jason D. Rivard
  */
-@SuppressWarnings( "all" ) // this class will be replaced by NGStoredConfiguration
 public class StoredConfigurationImpl implements StoredConfiguration
 {
+    private final String createTime;
+    private final Instant modifyTime;
+    private final Map<StoredConfigItemKey, StoredValue> storedValues;
+    private final Map<StoredConfigItemKey, ValueMetaData> metaValues;
+    private final PwmSettingTemplateSet templateSet;
 
-    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationImpl.class );
-    static final String XML_FORMAT_VERSION = "4";
-
-    private XmlDocument document = XmlFactory.getFactory().newDocument( XML_ELEMENT_ROOT );
-    private ChangeLog changeLog = new ChangeLog();
-
-    private boolean locked;
-    private final boolean setting_writeLabels = true;
-    private final ReentrantReadWriteLock domModifyLock = new ReentrantReadWriteLock();
+    private final transient Supplier<String> valueHashSupplier = new LazySupplier<>( this::valueHashImpl );
 
-    private final XmlHelper xmlHelper = new XmlHelper();
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationImpl.class );
 
-    public static StoredConfigurationImpl newStoredConfiguration( ) throws PwmUnrecoverableException
+    StoredConfigurationImpl( final StoredConfigData storedConfigData )
     {
-        return new StoredConfigurationImpl();
-    }
+        this.createTime = storedConfigData.getCreateTime();
+        this.modifyTime = storedConfigData.getModifyTime();
+        this.metaValues = Collections.unmodifiableMap(  new TreeMap<>( storedConfigData.getMetaDatas() ) );
+        this.templateSet = readTemplateSet( storedConfigData.getStoredValues() );
 
-    public static StoredConfigurationImpl copy( final StoredConfigurationImpl input ) throws PwmUnrecoverableException
-    {
-        final StoredConfigurationImpl copy = new StoredConfigurationImpl();
-        copy.document = input.document.copy();
-        return copy;
+        final Map<StoredConfigItemKey, StoredValue> tempMap = new TreeMap<>( storedConfigData.getStoredValues() );
+        removeAllDefaultValues( tempMap, templateSet );
+        this.storedValues = Collections.unmodifiableMap( tempMap );
     }
 
-    public static StoredConfigurationImpl fromXml( final InputStream xmlData )
-            throws PwmUnrecoverableException
+    StoredConfigurationImpl()
     {
-        final Instant startTime = Instant.now();
-        //validateXmlSchema(xmlData);
-
-        final XmlDocument inputDocument = XmlFactory.getFactory().parseXml( xmlData );
-        final StoredConfigurationImpl newConfiguration = StoredConfigurationImpl.newStoredConfiguration();
-
-        try
-        {
-            newConfiguration.document = inputDocument;
-            newConfiguration.createTime(); // verify create time;
-            ConfigurationCleaner.cleanup( newConfiguration, newConfiguration.document );
-        }
-        catch ( Exception e )
-        {
-            final String errorMsg = "error reading configuration file format, error=" + e.getMessage();
-            final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] { errorMsg } );
-            throw new PwmUnrecoverableException( errorInfo );
-        }
-
-        checkIfXmlRequiresUpdate( newConfiguration );
-        LOGGER.debug( () -> "successfully loaded configuration (" + TimeDuration.compactFromCurrent( startTime ) + ")" );
-        return newConfiguration;
+        this.createTime = JavaHelper.toIsoDate( Instant.now() );
+        this.modifyTime = Instant.now();
+        this.storedValues = Collections.emptyMap();
+        this.metaValues = Collections.emptyMap();
+        this.templateSet = readTemplateSet( Collections.emptyMap() );
     }
 
-    /**
-     * Loop through all settings to see if setting value has flag {@link StoredValue#requiresStoredUpdate()} set to true.
-     * If so, then call {@link #writeSetting(PwmSetting, StoredValue, password.pwm.bean.UserIdentity)} or {@link #writeSetting(PwmSetting, String, StoredValue, password.pwm.bean.UserIdentity)}
-     * for that value so that the xml dom can be updated.
-     *
-     * @param storedConfiguration stored configuration to check
-     */
-    private static void checkIfXmlRequiresUpdate( final StoredConfigurationImpl storedConfiguration ) throws PwmUnrecoverableException
+    private static void removeAllDefaultValues( final Map<StoredConfigItemKey, StoredValue> valueMap, final PwmSettingTemplateSet pwmSettingTemplateSet )
     {
-        for ( final PwmSetting setting : PwmSetting.values() )
-        {
-            if ( setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles() )
-            {
-                final StoredValue value = storedConfiguration.readSetting( setting );
-                if ( value.requiresStoredUpdate() )
-                {
-                    storedConfiguration.writeSetting( setting, value, null );
-                }
-            }
-        }
-
-        for ( final PwmSettingCategory category : PwmSettingCategory.values() )
+        valueMap.entrySet().removeIf( entry ->
         {
-            if ( category.hasProfiles() )
+            final StoredConfigItemKey key = entry.getKey();
+            if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
             {
-                for ( final String profileID : storedConfiguration.profilesForSetting( category.getProfileSetting() ) )
-                {
-                    for ( final PwmSetting profileSetting : category.getSettings() )
-                    {
-                        final StoredValue value = storedConfiguration.readSetting( profileSetting, profileID );
-                        if ( value.requiresStoredUpdate() )
-                        {
-                            storedConfiguration.writeSetting( profileSetting, profileID, value, null );
-                        }
-                    }
-                }
+                final StoredValue loopValue = entry.getValue();
+                final StoredValue defaultValue = key.toPwmSetting().getDefaultValue( pwmSettingTemplateSet );
+                return Objects.equals( loopValue.valueHash(), defaultValue.valueHash() );
             }
-        }
+            return false;
+        } );
     }
 
-    public void resetAllPasswordValues( final String comment )
-            throws PwmUnrecoverableException
-    {
-        for ( final Iterator<SettingValueRecord> settingValueRecordIterator = new StoredValueIterator( false ); settingValueRecordIterator.hasNext(); )
-        {
-            final SettingValueRecord settingValueRecord = settingValueRecordIterator.next();
-            if ( settingValueRecord.getSetting().getSyntax() == PwmSettingSyntax.PASSWORD )
-            {
-                final ValueMetaData valueMetaData = this.readSettingMetadata( settingValueRecord.getSetting(), settingValueRecord.getProfile() );
-                final PasswordValue passwordValue = new PasswordValue( new PasswordData( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT ) );
-                this.writeSetting( settingValueRecord.getSetting(), settingValueRecord.getProfile(), passwordValue, valueMetaData.getUserIdentity() );
-                if ( comment != null && !comment.isEmpty() )
-                {
-                    final XmlElement settingElement = xmlHelper.xpathForSetting( settingValueRecord.getSetting(), settingValueRecord.getProfile() );
-                    if ( settingElement != null )
-                    {
-                        settingElement.setComment( Collections.singletonList( comment ) );
-                    }
-                }
-            }
-        }
 
-        final String pwdHash = this.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
-        if ( pwdHash != null && !pwdHash.isEmpty() )
-        {
-            this.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, comment );
-        }
-    }
 
-    public StoredConfigurationImpl( ) throws PwmUnrecoverableException
+    @Override
+    public StoredConfiguration copy()
     {
-        try
-        {
-            ConfigurationCleaner.cleanup( this, document );
-            final String createTime = JavaHelper.toIsoDate( Instant.now() );
-            document.getRootElement().setAttribute( XML_ATTRIBUTE_CREATE_TIME, createTime );
-        }
-        catch ( Exception e )
-        {
-            e.printStackTrace(  );
-            throw new IllegalStateException( e );
-        }
+        return new StoredConfigurationImpl( asStoredConfigData() );
     }
 
-
-    @Override
-    public String readConfigProperty( final ConfigurationProperty propertyName )
+    StoredConfigData asStoredConfigData()
     {
-        final XmlElement propertyElement = xmlHelper.xpathForConfigProperty( propertyName );
-        return propertyElement == null ? null : propertyElement.getText();
+        return new StoredConfigData( createTime, modifyTime, storedValues, metaValues );
     }
 
     @Override
-    public void writeConfigProperty(
-            final ConfigurationProperty propertyName,
-            final String value
-    )
+    public Optional<String> readConfigProperty( final ConfigurationProperty propertyName )
     {
-        domModifyLock.writeLock().lock();
-        try
-        {
-
-            // remove existing element
-            {
-                final XmlElement propertyElement  = xmlHelper.xpathForConfigProperty( propertyName );
-                if ( propertyElement != null )
-                {
-                    propertyElement.detach();
-                }
-            }
-
-            // add new property
-            final XmlElement propertyElement = xmlHelper.getXmlFactory().newElement( XML_ELEMENT_PROPERTY );
-            propertyElement.setAttribute( XML_ATTRIBUTE_KEY, propertyName.getKey() );
-            propertyElement.addText( value );
-
-            if ( null == xmlHelper.xpathForConfigProperties() )
-            {
-                final XmlElement configProperties = xmlHelper.getXmlFactory().newElement( XML_ELEMENT_PROPERTIES );
-                configProperties.setAttribute( XML_ATTRIBUTE_TYPE, XML_ATTRIBUTE_VALUE_CONFIG );
-                document.getRootElement().addContent( configProperties );
-            }
-
-            final XmlElement propertiesElement = xmlHelper.xpathForConfigProperties();
-            propertyElement.setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-            propertiesElement.setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-            propertiesElement.addContent( propertyElement );
-        }
-        finally
+        final StoredConfigItemKey key = StoredConfigItemKey.fromConfigurationProperty( propertyName );
+        final StoredValue storedValue = storedValues.get( key );
+        if ( storedValue != null )
         {
-            domModifyLock.writeLock().unlock();
+            return Optional.of( ( ( StringValue ) storedValue ).toNativeObject() );
         }
+        return Optional.empty();
     }
 
-    public void lock( )
+    public boolean isDefaultValue( final PwmSetting setting, final String profileID )
     {
-        locked = true;
+        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+        return !storedValues.containsKey( key );
     }
 
-    public Map<String, String> readLocaleBundleMap( final String bundleName, final String keyName )
+    @Override
+    public Map<String, String> readLocaleBundleMap( final PwmLocaleBundle pwmLocaleBundle, final String keyName )
     {
-        domModifyLock.readLock().lock();
-        try
-        {
-            final XmlElement localeBundleElement = xmlHelper.xpathForLocaleBundleSetting( bundleName, keyName );
-
-            if ( localeBundleElement != null )
-            {
-                final Map<String, String> bundleMap = new LinkedHashMap<>();
-                for ( final XmlElement valueElement : localeBundleElement.getChildren( "value" ) )
-                {
-                    final String localeStrValue = valueElement.getAttributeValue( "locale" );
-                    bundleMap.put( localeStrValue == null ? "" : localeStrValue, valueElement.getText() );
-                }
-                if ( !bundleMap.isEmpty() )
-                {
-                    return bundleMap;
-                }
-            }
-        }
-        finally
+        final StoredConfigItemKey key = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, keyName );
+        final StoredValue value = storedValues.get( key );
+        if ( value != null )
         {
-            domModifyLock.readLock().unlock();
+            return ( ( LocalizedStringValue ) value ).toNativeObject();
         }
         return Collections.emptyMap();
     }
 
-    public Map<String, Object> toOutputMap( final Locale locale )
-    {
-        final List<Map<String, String>> settingData = new ArrayList<>();
-        for ( final StoredConfigurationImpl.SettingValueRecord settingValueRecord : this.modifiedSettings() )
-        {
-            final Map<String, String> recordMap = new HashMap<>();
-            recordMap.put( "label", settingValueRecord.getSetting().getLabel( locale ) );
-            if ( settingValueRecord.getProfile() != null )
-            {
-                recordMap.put( "profile", settingValueRecord.getProfile() );
-            }
-            if ( settingValueRecord.getStoredValue() != null )
-            {
-                recordMap.put( "value", settingValueRecord.getStoredValue().toDebugString( locale ) );
-            }
-            final ValueMetaData settingMetaData = readSettingMetadata( settingValueRecord.getSetting(), settingValueRecord.getProfile() );
-            if ( settingMetaData != null )
-            {
-                if ( settingMetaData.getModifyDate() != null )
-                {
-                    recordMap.put( "modifyTime", JavaHelper.toIsoDate( settingMetaData.getModifyDate() ) );
-                }
-                if ( settingMetaData.getUserIdentity() != null )
-                {
-                    recordMap.put( "modifyUser", settingMetaData.getUserIdentity().toDisplayString() );
-                }
-            }
-            settingData.add( recordMap );
-        }
-
-        final HashMap<String, Object> outputObj = new HashMap<>();
-        outputObj.put( "settings", settingData );
-        outputObj.put( "template", this.getTemplateSet().toString() );
-
-        return Collections.unmodifiableMap( outputObj );
-    }
-
-    public void resetLocaleBundleMap( final String bundleName, final String keyName )
-    {
-        preModifyActions();
-        domModifyLock.writeLock().lock();
-        try
-        {
-            final XmlElement oldBundleElements = xmlHelper.xpathForLocaleBundleSetting( bundleName, keyName );
-            if ( oldBundleElements != null )
-            {
-                oldBundleElements.detach();
-            }
-        }
-        finally
-        {
-            domModifyLock.writeLock().unlock();
-        }
-    }
-
-    public void resetSetting( final PwmSetting setting, final String profileID, final UserIdentity userIdentity )
-    {
-        changeLog.updateChangeLog( setting, profileID, defaultValue( setting, this.getTemplateSet() ) );
-        domModifyLock.writeLock().lock();
-        try
-        {
-            preModifyActions();
-            final XmlElement settingElement = createOrGetSettingElement( setting, profileID );
-            settingElement.removeContent();
-            settingElement.addContent( xmlHelper.getXmlFactory().newElement( XML_ELEMENT_DEFAULT ) );
-            updateMetaData( settingElement, userIdentity );
-        }
-        finally
-        {
-            domModifyLock.writeLock().unlock();
-        }
-    }
-
-    public boolean isDefaultValue( final PwmSetting setting )
-    {
-        return isDefaultValue( setting, null );
-    }
-
-    public boolean isDefaultValue( final PwmSetting setting, final String profileID )
-    {
-        domModifyLock.readLock().lock();
-        try
-        {
-            final StoredValue currentValue = readSetting( setting, profileID );
-            if ( setting.getSyntax() == PwmSettingSyntax.PASSWORD )
-            {
-                return currentValue == null || currentValue.toNativeObject() == null;
-            }
-            final StoredValue defaultValue = defaultValue( setting, this.getTemplateSet() );
-            final String currentJsonValue = JsonUtil.serialize( ( Serializable ) currentValue.toNativeObject() );
-            final String defaultJsonValue = JsonUtil.serialize( ( Serializable ) defaultValue.toNativeObject() );
-            return defaultJsonValue.equalsIgnoreCase( currentJsonValue );
-        }
-        finally
-        {
-            domModifyLock.readLock().unlock();
-        }
-    }
-
-    private static StoredValue defaultValue( final PwmSetting pwmSetting, final PwmSettingTemplateSet template )
+    @Override
+    public PwmSettingTemplateSet getTemplateSet()
     {
-        return pwmSetting.getDefaultValue( template );
+        return templateSet;
     }
 
-    public PwmSettingTemplateSet getTemplateSet( )
+    private static PwmSettingTemplateSet readTemplateSet( final Map<StoredConfigItemKey, StoredValue> valueMap )
     {
         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 ) );
+        readTemplateValue( valueMap, PwmSetting.TEMPLATE_LDAP ).ifPresent( templates::add );
+        readTemplateValue( valueMap, PwmSetting.TEMPLATE_STORAGE ).ifPresent( templates::add );
+        readTemplateValue( valueMap, PwmSetting.DB_VENDOR_TEMPLATE ).ifPresent( templates::add );
         return new PwmSettingTemplateSet( templates );
     }
 
-    private PwmSettingTemplate readTemplateValue( final PwmSetting pwmSetting )
+    private static Optional<PwmSettingTemplate> readTemplateValue( final Map<StoredConfigItemKey, StoredValue> valueMap, final PwmSetting pwmSetting )
     {
-        final XmlElement settingElement = xmlHelper.xpathForSetting( pwmSetting, null );
-        if ( settingElement != null )
+        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( pwmSetting, null );
+        final StoredValue storedValue = valueMap.get( key );
+
+        if ( storedValue != null )
         {
             try
             {
-                final String strValue = ( String ) ValueFactory.fromXmlValues( pwmSetting, settingElement, null ).toNativeObject();
-                return JavaHelper.readEnumFromString( PwmSettingTemplate.class, null, strValue );
+                final String strValue = ( String ) storedValue.toNativeObject();
+                return Optional.ofNullable( JavaHelper.readEnumFromString( PwmSettingTemplate.class, null, strValue ) );
             }
-            catch ( IllegalStateException e )
+            catch ( final IllegalStateException e )
             {
                 LOGGER.error( "error reading template", e );
             }
         }
-        return null;
-    }
 
-    public void setTemplate( final PwmSettingTemplate template )
-    {
-        writeConfigProperty( ConfigurationProperty.LDAP_TEMPLATE, template.toString() );
+        return Optional.empty();
     }
 
     public String toString( final PwmSetting setting, final String profileID )
@@ -451,1130 +185,102 @@ public class StoredConfigurationImpl implements StoredConfiguration
         return setting.getKey() + "=" + storedValue.toDebugString( null );
     }
 
-    public Map<String, String> getModifiedSettingDebugValues( final Locale locale, final boolean prettyPrint )
-    {
-        final Map<String, String> returnObj = new TreeMap<>();
-        for ( final SettingValueRecord record : this.modifiedSettings() )
-        {
-            final String label = record.getSetting().toMenuLocationDebug( record.getProfile(), locale );
-            final String value = record.getStoredValue().toDebugString( locale );
-            returnObj.put( label, value );
-        }
-        return returnObj;
-    }
-
-    public List<SettingValueRecord> modifiedSettings( )
-    {
-        final List<SettingValueRecord> returnObj = new ArrayList<>();
-        domModifyLock.readLock().lock();
-        try
-        {
-            for ( final PwmSetting setting : PwmSetting.values() )
-            {
-                if ( setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles() )
-                {
-                    if ( !isDefaultValue( setting, null ) )
-                    {
-                        final StoredValue value = readSetting( setting );
-                        if ( value != null )
-                        {
-                            returnObj.add( new SettingValueRecord( setting, null, value ) );
-                        }
-                    }
-                }
-            }
-
-            for ( final PwmSettingCategory category : PwmSettingCategory.values() )
-            {
-                if ( category.hasProfiles() )
-                {
-                    for ( final String profileID : this.profilesForSetting( category.getProfileSetting() ) )
-                    {
-                        for ( final PwmSetting profileSetting : category.getSettings() )
-                        {
-                            if ( !isDefaultValue( profileSetting, profileID ) )
-                            {
-                                final StoredValue value = readSetting( profileSetting, profileID );
-                                if ( value != null )
-                                {
-                                    returnObj.add( new SettingValueRecord( profileSetting, profileID, value ) );
-
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-
-            return returnObj;
-        }
-        finally
-        {
-            domModifyLock.readLock().unlock();
-        }
-    }
-
-    public Serializable toJsonDebugObject( )
-    {
-        domModifyLock.readLock().lock();
-        try
-        {
-            final TreeMap<String, Object> outputObject = new TreeMap<>();
-
-            for ( final PwmSetting setting : PwmSetting.values() )
-            {
-                if ( setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles() )
-                {
-                    if ( !isDefaultValue( setting, null ) )
-                    {
-                        final StoredValue value = readSetting( setting );
-                        outputObject.put( setting.getKey(), value.toDebugJsonObject( null ) );
-                    }
-                }
-            }
-
-            for ( final PwmSettingCategory category : PwmSettingCategory.values() )
-            {
-                if ( category.hasProfiles() )
-                {
-                    final TreeMap<String, Object> profiles = new TreeMap<>();
-                    for ( final String profileID : this.profilesForSetting( category.getProfileSetting() ) )
-                    {
-                        final TreeMap<String, Object> profileObject = new TreeMap<>();
-                        for ( final PwmSetting profileSetting : category.getSettings() )
-                        {
-                            if ( !isDefaultValue( profileSetting, profileID ) )
-                            {
-                                final StoredValue value = readSetting( profileSetting, profileID );
-                                profileObject.put( profileSetting.getKey(), value.toDebugJsonObject( null ) );
-                            }
-                        }
-                        profiles.put( profileID, profileObject );
-                    }
-                    outputObject.put( category.getProfileSetting().getKey(), profiles );
-                }
-            }
-
-            return outputObject;
-        }
-        finally
-        {
-            domModifyLock.readLock().unlock();
-        }
-    }
-
-    public void toXml( final OutputStream outputStream )
-            throws IOException, PwmUnrecoverableException
+    public Set<StoredConfigItemKey> modifiedItems()
     {
-        ConfigurationCleaner.updateMandatoryElements( this, document );
-        XmlFactory.getFactory().outputDocument( document, outputStream );
+        return Collections.unmodifiableSet( storedValues.keySet() );
     }
 
+    @Override
     public List<String> profilesForSetting( final PwmSetting pwmSetting )
     {
-        return StoredConfigurationUtil.profilesForSetting( pwmSetting, this );
-    }
-
-    public List<String> validateValues( )
-    {
-        final long startTime = System.currentTimeMillis();
-        final List<String> errorStrings = new ArrayList<>();
-
-        for ( final PwmSetting loopSetting : PwmSetting.values() )
+        final List<String> returnObj = new ArrayList<>();
+        for ( final StoredConfigItemKey storedConfigItemKey : storedValues.keySet() )
         {
-
-            if ( loopSetting.getCategory().hasProfiles() )
+            if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING
+                    && Objects.equals( storedConfigItemKey.getRecordID(), pwmSetting.getKey() ) )
             {
-                for ( final String profile : profilesForSetting( loopSetting ) )
-                {
-                    final StoredValue loopValue = readSetting( loopSetting, profile );
-
-                    try
-                    {
-                        final List<String> errors = loopValue.validateValue( loopSetting );
-                        for ( final String loopError : errors )
-                        {
-                            errorStrings.add( loopSetting.toMenuLocationDebug( profile, PwmConstants.DEFAULT_LOCALE ) + " - " + loopError );
-                        }
-                    }
-                    catch ( Exception e )
-                    {
-                        LOGGER.error( "unexpected error during validate value for " + loopSetting.toMenuLocationDebug( profile, PwmConstants.DEFAULT_LOCALE ) + ", error: " + e.getMessage(), e );
-                    }
-                }
-            }
-            else
-            {
-                final StoredValue loopValue = readSetting( loopSetting );
-
-                try
-                {
-                    final List<String> errors = loopValue.validateValue( loopSetting );
-                    for ( final String loopError : errors )
-                    {
-                        errorStrings.add( loopSetting.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) + " - " + loopError );
-                    }
-                }
-                catch ( Exception e )
-                {
-                    LOGGER.error( "unexpected error during validate value for " + loopSetting.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) + ", error: " + e.getMessage(), e );
-                }
+                returnObj.add( storedConfigItemKey.getProfileID() );
             }
         }
-
-        LOGGER.trace( () -> "StoredConfiguration validator completed in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
-        return errorStrings;
+        return Collections.unmodifiableList( returnObj );
     }
 
     public ValueMetaData readSettingMetadata( final PwmSetting setting, final String profileID )
     {
-        final XmlElement settingElement = xmlHelper.xpathForSetting( setting, profileID );
-
-        if ( settingElement == null )
-        {
-            return null;
-        }
-
-        Instant modifyDate = null;
-        try
-        {
-            if ( settingElement.getAttributeValue( XML_ATTRIBUTE_MODIFY_TIME ) != null )
-            {
-                modifyDate = JavaHelper.parseIsoToInstant(
-                        settingElement.getAttributeValue( XML_ATTRIBUTE_MODIFY_TIME ) );
-            }
-        }
-        catch ( Exception e )
-        {
-            LOGGER.error( "can't read modifyDate for setting " + setting.getKey() + ", profile " + profileID + ", error: " + e.getMessage() );
-        }
-
-        UserIdentity userIdentity = null;
-        try
-        {
-            if ( settingElement.getAttributeValue( XML_ATTRIBUTE_MODIFY_USER ) != null )
-            {
-                userIdentity = UserIdentity.fromDelimitedKey(
-                        settingElement.getAttributeValue( XML_ATTRIBUTE_MODIFY_USER ) );
-            }
-        }
-        catch ( Exception e )
-        {
-            LOGGER.error( "can't read userIdentity for setting " + setting.getKey() + ", profile " + profileID + ", error: " + e.getMessage() );
-        }
-
-        return new ValueMetaData( modifyDate, userIdentity );
+        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+        return metaValues.get( key );
     }
 
-    public List<ConfigRecordID> search( final String searchTerm, final Locale locale )
+    public StoredValue readSetting( final PwmSetting setting, final String profileID )
     {
-        if ( searchTerm == null )
+        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+        final StoredValue storedValue = storedValues.get( key );
+        if ( storedValue == null )
         {
-            return Collections.emptyList();
+            return setting.getDefaultValue( getTemplateSet() );
         }
 
-        final SortedSet<ConfigRecordID> matches = new TreeSet<>(
-                allSettingConfigRecordIDs()
-                        .parallelStream()
-                        .filter( s -> matchSetting( s, searchTerm, locale ) )
-                        .collect( Collectors.toList() )
-        );
-
-        return new ArrayList<>( matches );
+        return storedValue;
     }
 
-    private boolean matchSetting(
-            final ConfigRecordID configRecordID,
-            final String searchTerm,
-            final Locale locale
-    )
+    public String valueHash()
     {
-
-        final PwmSetting pwmSetting = ( PwmSetting ) configRecordID.getRecordID();
-        final StoredValue value = readSetting( pwmSetting, configRecordID.getProfileID() );
-
-        return StringUtil.whitespaceSplit( searchTerm )
-                .parallelStream()
-                .allMatch( s -> matchSetting( pwmSetting, value, s, locale ) );
+        return valueHashSupplier.get();
     }
 
-    public boolean matchSetting( final PwmSetting setting, final StoredValue value, final String searchTerm, final Locale locale )
+    private String valueHashImpl()
     {
-        if ( setting.isHidden() || setting.getCategory().isHidden() )
-        {
-            return false;
-        }
+        final Set<StoredConfigItemKey> modifiedSettings = modifiedItems();
+        final StringBuilder sb = new StringBuilder();
 
-        if ( searchTerm == null || searchTerm.isEmpty() )
+        for ( final StoredConfigItemKey storedConfigItemKey : modifiedSettings )
         {
-            return false;
+            final StoredValue storedValue = storedValues.get( storedConfigItemKey );
+            sb.append( storedValue.valueHash() );
         }
 
-        final String lowerSearchTerm = searchTerm.toLowerCase();
-
-        {
-            final String key = setting.getKey();
-            if ( key.toLowerCase().contains( lowerSearchTerm ) )
-            {
-                return true;
-            }
-        }
-        {
-            final String label = setting.getLabel( locale );
-            if ( label.toLowerCase().contains( lowerSearchTerm ) )
-            {
-                return true;
-            }
-        }
+        try
         {
-            final String descr = setting.getDescription( locale );
-            if ( descr.toLowerCase().contains( lowerSearchTerm ) )
-            {
-                return true;
-            }
+            return SecureEngine.hmac( HmacAlgorithm.HMAC_SHA_512, getKey(), sb.toString() );
         }
+        catch ( final PwmUnrecoverableException e )
         {
-            final String menuLocationString = setting.toMenuLocationDebug( null, locale );
-            if ( menuLocationString.toLowerCase().contains( lowerSearchTerm ) )
-            {
-                return true;
-            }
+            throw new IllegalStateException( e );
         }
+    }
 
-        if ( setting.isConfidential() )
-        {
-            return false;
-        }
-        {
-            final String valueDebug = value.toDebugString( locale );
-            if ( valueDebug != null && valueDebug.toLowerCase().contains( lowerSearchTerm ) )
-            {
-                return true;
-            }
-        }
-        if ( PwmSettingSyntax.SELECT == setting.getSyntax()
-                || PwmSettingSyntax.OPTIONLIST == setting.getSyntax()
-                || PwmSettingSyntax.VERIFICATION_METHOD == setting.getSyntax()
-        )
+    private PwmSecurityKey cachedKey;
+
+    public PwmSecurityKey getKey() throws PwmUnrecoverableException
+    {
+        if ( cachedKey == null )
         {
-            for ( final String key : setting.getOptions().keySet() )
-            {
-                if ( key.toLowerCase().contains( lowerSearchTerm ) )
-                {
-                    return true;
-                }
-                final String optionValue = setting.getOptions().get( key );
-                if ( optionValue != null && optionValue.toLowerCase().contains( lowerSearchTerm ) )
-                {
-                    return true;
-                }
-            }
+            cachedKey = new PwmSecurityKey( createTime + "StoredConfiguration" );
         }
-        return false;
+        return cachedKey;
     }
 
-
-    public StoredValue readSetting( final PwmSetting setting )
+    @Override
+    public Instant modifyTime()
     {
-        return readSetting( setting, null );
+        return modifyTime;
     }
 
-    public StoredValue readSetting( final PwmSetting setting, final String profileID )
+    @Override
+    public String createTime()
     {
-        if ( profileID == null && setting.getCategory().hasProfiles() )
-        {
-            final IllegalArgumentException e = new IllegalArgumentException( "reading of setting " + setting.getKey() + " requires a non-null profileID" );
-            LOGGER.error( "error", e );
-            throw e;
-        }
-        if ( profileID != null && !setting.getCategory().hasProfiles() )
-        {
-            throw new IllegalStateException( "cannot read setting key " + setting.getKey() + " with non-null profileID" );
-        }
-        domModifyLock.readLock().lock();
-        try
-        {
-            final XmlElement settingElement = xmlHelper.xpathForSetting( setting, profileID );
-
-            if ( settingElement == null )
-            {
-                return defaultValue( setting, getTemplateSet() );
-            }
-
-            if ( settingElement.getChild( XML_ELEMENT_DEFAULT ) != null )
-            {
-                return defaultValue( setting, getTemplateSet() );
-            }
-
-            try
-            {
-                return ValueFactory.fromXmlValues( setting, settingElement, getKey() );
-            }
-            catch ( PwmException e )
-            {
-                final String errorMsg = "unexpected error reading setting '" + setting.getKey() + "' profile '" + profileID + "', error: " + e.getMessage();
-                throw new IllegalStateException( errorMsg );
-            }
-        }
-        finally
-        {
-            domModifyLock.readLock().unlock();
-        }
+        return createTime;
     }
 
-    public void writeLocaleBundleMap( final String bundleName, final String keyName, final Map<String, String> localeMap )
+    @Override
+    public Optional<ValueMetaData> readMetaData( final StoredConfigItemKey storedConfigItemKey )
     {
-        ResourceBundle theBundle = null;
-        for ( final PwmLocaleBundle bundle : PwmLocaleBundle.values() )
-        {
-            if ( bundle.getTheClass().getName().equals( bundleName ) )
-            {
-                theBundle = ResourceBundle.getBundle( bundleName );
-            }
-        }
-
-        if ( theBundle == null )
-        {
-            LOGGER.info( () -> "ignoring unknown locale bundle for bundle=" + bundleName + ", key=" + keyName );
-            return;
-        }
-
-        if ( theBundle.getString( keyName ) == null )
-        {
-            LOGGER.info( () -> "ignoring unknown key for bundle=" + bundleName + ", key=" + keyName );
-            return;
-        }
-
-
-        resetLocaleBundleMap( bundleName, keyName );
-        if ( localeMap == null || localeMap.isEmpty() )
-        {
-            LOGGER.info( () -> "cleared locale bundle map for bundle=" + bundleName + ", key=" + keyName );
-            return;
-        }
-
-        preModifyActions();
-        changeLog.updateChangeLog( bundleName, keyName, localeMap );
-        try
-        {
-            domModifyLock.writeLock().lock();
-            final XmlElement localeBundleElement = xmlHelper.getXmlFactory().newElement( "localeBundle" );
-            localeBundleElement.setAttribute( "bundle", bundleName );
-            localeBundleElement.setAttribute( "key", keyName );
-            for ( final Map.Entry<String, String> entry : localeMap.entrySet() )
-            {
-                final String locale = entry.getKey();
-                final String value = entry.getValue();
-                final XmlElement valueElement = xmlHelper.getXmlFactory().newElement( "value" );
-                if ( locale != null && locale.length() > 0 )
-                {
-                    valueElement.setAttribute( "locale", locale );
-                }
-                valueElement.addText( value );
-                localeBundleElement.addContent( valueElement );
-            }
-            localeBundleElement.setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-            document.getRootElement().addContent( localeBundleElement );
-        }
-        finally
-        {
-            domModifyLock.writeLock().unlock();
-        }
-    }
-
-
-    public void copyProfileID( final PwmSettingCategory category, final String sourceID, final String destinationID, final UserIdentity userIdentity )
-            throws PwmUnrecoverableException
-    {
-
-        if ( !category.hasProfiles() )
-        {
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category " + category + ", category does not have profiles" );
-        }
-        final List<String> existingProfiles = this.profilesForSetting( category.getProfileSetting() );
-        if ( !existingProfiles.contains( sourceID ) )
-        {
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category, source profileID '" + sourceID + "' does not exist" );
-        }
-        if ( existingProfiles.contains( destinationID ) )
-        {
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category, destination profileID '" + destinationID + "' already exists" );
-        }
-
-        {
-            final Collection<PwmSettingCategory> interestedCategories = PwmSettingCategory.associatedProfileCategories( category );
-            for ( final PwmSettingCategory interestedCategory : interestedCategories )
-            {
-                for ( final PwmSetting pwmSetting : interestedCategory.getSettings() )
-                {
-                    if ( !isDefaultValue( pwmSetting, sourceID ) )
-                    {
-                        final StoredValue value = readSetting( pwmSetting, sourceID );
-                        writeSetting( pwmSetting, destinationID, value, userIdentity );
-                    }
-                }
-            }
-        }
-
-        final List<String> newProfileIDList = new ArrayList<>();
-        newProfileIDList.addAll( existingProfiles );
-        newProfileIDList.add( destinationID );
-        writeSetting( category.getProfileSetting(), new StringArrayValue( newProfileIDList ), userIdentity );
-    }
-
-
-    public void writeSetting(
-            final PwmSetting setting,
-            final StoredValue value,
-            final UserIdentity userIdentity
-    ) throws PwmUnrecoverableException
-    {
-        writeSetting( setting, null, value, userIdentity );
-    }
-
-    public void writeSetting(
-            final PwmSetting setting,
-            final String profileID,
-            final StoredValue value,
-            final UserIdentity userIdentity
-    ) throws PwmUnrecoverableException
-    {
-        if ( profileID == null && setting.getCategory().hasProfiles() )
-        {
-            throw new IllegalArgumentException( "writing of setting " + setting.getKey() + " requires a non-null profileID" );
-        }
-        if ( profileID != null && !setting.getCategory().hasProfiles() )
-        {
-            throw new IllegalArgumentException( "cannot specify profile for non-profile setting" );
-        }
-
-        preModifyActions();
-        changeLog.updateChangeLog( setting, profileID, value );
-        domModifyLock.writeLock().lock();
-        try
-        {
-            final XmlElement settingElement = createOrGetSettingElement( setting, profileID );
-            settingElement.removeContent();
-            settingElement.setAttribute( XML_ATTRIBUTE_SYNTAX, setting.getSyntax().toString() );
-            settingElement.setAttribute( XML_ATTRIBUTE_SYNTAX_VERSION, Integer.toString( value.currentSyntaxVersion() ) );
-
-            if ( setting_writeLabels )
-            {
-                {
-                    final XmlElement existingLabel = settingElement.getChild( "label" );
-                    if ( existingLabel != null )
-                    {
-                        existingLabel.detach();
-                    }
-                }
-
-                {
-                    final XmlElement newLabelElement = xmlHelper.getXmlFactory().newElement( "label" );
-                    newLabelElement.addText( setting.getLabel( PwmConstants.DEFAULT_LOCALE ) );
-                    settingElement.addContent( newLabelElement );
-                }
-            }
-
-            if ( setting.getSyntax() == PwmSettingSyntax.PASSWORD )
-            {
-                final List<String> commentLines = Arrays.asList(
-                        "Note: This value is encrypted and can not be edited directly.",
-                        "Please use the Configuration Manager GUI to modify this value."
-                );
-                settingElement.setComment( commentLines );
-
-                final List<XmlElement> valueElements = ( ( PasswordValue ) value ).toXmlValues( "value", getKey() );
-                settingElement.addContent( valueElements );
-            }
-            else if ( setting.getSyntax() == PwmSettingSyntax.PRIVATE_KEY )
-            {
-                final List<XmlElement> valueElements = ( ( PrivateKeyValue ) value ).toXmlValues( "value", getKey() );
-                settingElement.addContent( valueElements );
-            }
-            else if ( setting.getSyntax() == PwmSettingSyntax.NAMED_SECRET )
-            {
-                final List<XmlElement> valueElements = ( ( NamedSecretValue ) value ).toXmlValues( "value", getKey() );
-                settingElement.addContent( valueElements );
-            }
-            else
-            {
-                settingElement.addContent( value.toXmlValues( "value", getKey() ) );
-            }
-
-
-            updateMetaData( settingElement, userIdentity );
-        }
-        finally
-        {
-            domModifyLock.writeLock().unlock();
-        }
-    }
-
-    public String settingChecksum( )
-            throws PwmUnrecoverableException
-    {
-        final Instant startTime = Instant.now();
-
-        final List<SettingValueRecord> modifiedSettings = modifiedSettings();
-        final StringBuilder sb = new StringBuilder();
-        sb.append( "PwmSettingsChecksum" );
-        for ( final SettingValueRecord settingValueRecord : modifiedSettings )
-        {
-            final StoredValue storedValue = settingValueRecord.getStoredValue();
-            sb.append( storedValue.valueHash() );
-        }
-
-
-        final String result = SecureEngine.hash( sb.toString(), PwmConstants.SETTING_CHECKSUM_HASH_METHOD );
-        LOGGER.trace( () -> "computed setting checksum in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
-        return result;
-    }
-
-
-    private void preModifyActions( )
-    {
-        if ( locked )
-        {
-            throw new UnsupportedOperationException( "StoredConfiguration is locked and cannot be modified" );
-        }
-        document.getRootElement().setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-    }
-
-    public void setPassword( final String password )
-            throws PwmOperationalException
-    {
-        if ( password == null || password.isEmpty() )
-        {
-            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] { "can not set blank password" } ) );
-        }
-        final String trimmedPassword = password.trim();
-        if ( trimmedPassword.length() < 1 )
-        {
-            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] { "can not set blank password" } ) );
-        }
-
-
-        final String passwordHash = BCrypt.hashPassword( password );
-        this.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, passwordHash );
-    }
-
-    public boolean verifyPassword( final String password, final Configuration configuration )
-    {
-        if ( !hasPassword() )
-        {
-            return false;
-        }
-        final String passwordHash = this.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
-        return BCrypt.testAnswer( password, passwordHash, configuration );
-    }
-
-    public boolean hasPassword( )
-    {
-        final String passwordHash = this.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
-        return passwordHash != null && passwordHash.length() > 0;
-    }
-
-    class XmlHelper
-    {
-        private XmlFactory getXmlFactory()
-        {
-            return XmlFactory.getFactory();
-        }
-
-        private XmlElement xpathForLocaleBundleSetting( final String bundleName, final String keyName )
-        {
-            final String xpathString = "//localeBundle[@bundle=\"" + bundleName + "\"][@key=\"" + keyName + "\"]";
-            return document.evaluateXpathToElement( xpathString );
-        }
-
-        XmlElement xpathForSetting( final PwmSetting setting, final String profileID )
-        {
-            final String xpathString;
-            if ( profileID == null || profileID.length() < 1 )
-            {
-                xpathString = "//setting[@key=\"" + setting.getKey() + "\"][(not (@profile)) or @profile=\"\"]";
-            }
-            else
-            {
-                xpathString = "//setting[@key=\"" + setting.getKey() + "\"][@profile=\"" + profileID + "\"]";
-            }
-
-            return document.evaluateXpathToElement( xpathString );
-        }
-
-        private XmlElement xpathForAppProperty( final AppProperty appProperty )
-        {
-            final String xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\"" + XML_ATTRIBUTE_VALUE_APP + "\"]/"
-                    + XML_ELEMENT_PROPERTY + "[@" + XML_ATTRIBUTE_KEY + "=\"" + appProperty.getKey() + "\"]";
-            return document.evaluateXpathToElement( xpathString );
-        }
-
-        List<XmlElement> xpathForAppProperties( )
-        {
-            final String xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\"" + XML_ATTRIBUTE_VALUE_APP + "\"]";
-            return document.evaluateXpathToElements( xpathString );
-        }
-
-        private XmlElement xpathForConfigProperty( final ConfigurationProperty configProperty )
-        {
-            final String xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\"" + XML_ATTRIBUTE_VALUE_CONFIG + "\"]/"
-                    + XML_ELEMENT_PROPERTY + "[@" + XML_ATTRIBUTE_KEY + "=\"" + configProperty.getKey() + "\"]";
-            return document.evaluateXpathToElement( xpathString );
-        }
-
-        private XmlElement xpathForConfigProperties( )
-        {
-            final String xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\"" + XML_ATTRIBUTE_VALUE_CONFIG + "\"]";
-            return document.evaluateXpathToElement( xpathString );
-        }
-    }
-
-
-    public static class ConfigRecordID implements Serializable, Comparable
-    {
-        private RecordType recordType;
-        private Object recordID;
-        private String profileID;
-
-        public enum RecordType
-        {
-            SETTING,
-            LOCALE_BUNDLE,
-        }
-
-        public ConfigRecordID(
-                final RecordType recordType,
-                final Object recordID,
-                final String profileID
-        )
-        {
-            this.recordType = recordType;
-            this.recordID = recordID;
-            this.profileID = profileID;
-        }
-
-
-        public RecordType getRecordType( )
-        {
-            return recordType;
-        }
-
-        public Object getRecordID( )
-        {
-            return recordID;
-        }
-
-        public String getProfileID( )
-        {
-            return profileID;
-        }
-
-        @Override
-        public boolean equals( final Object o )
-        {
-            return o != null
-                    && o instanceof ConfigRecordID
-                    && toString().equals( o.toString() );
-
-        }
-
-        @Override
-        public int hashCode( )
-        {
-            return toString().hashCode();
-        }
-
-        @Override
-        public String toString( )
-        {
-            return this.getRecordType().toString()
-                    + "-"
-                    + ( this.getProfileID() == null ? "" : this.getProfileID() )
-                    + "-"
-                    + this.getRecordID();
-        }
-
-        @Override
-        public int compareTo( final Object o )
-        {
-            return toString().compareTo( o.toString() );
-        }
-    }
-
-
-    public String changeLogAsDebugString( final Locale locale, final boolean asHtml )
-    {
-        return changeLog.changeLogAsDebugString( locale, asHtml );
-    }
-
-    private PwmSecurityKey cachedKey;
-
-    public PwmSecurityKey getKey( ) throws PwmUnrecoverableException
-    {
-        if ( cachedKey == null )
-        {
-            cachedKey = new PwmSecurityKey( createTime() + "StoredConfiguration" );
-        }
-        return cachedKey;
-    }
-
-    public boolean isModified( )
-    {
-        return changeLog.isModified();
-    }
-
-    private class ChangeLog
-    {
-        /* values contain the _original_ toJson version of the value. */
-        private Map<ConfigRecordID, String> changeLog = new LinkedHashMap<>();
-
-        public boolean isModified( )
-        {
-            return !changeLog.isEmpty();
-        }
-
-        public String changeLogAsDebugString( final Locale locale, final boolean asHtml )
-        {
-            final Map<String, String> outputMap = new TreeMap<>();
-            final String SEPARATOR = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
-
-            for ( final ConfigRecordID configRecordID : changeLog.keySet() )
-            {
-                switch ( configRecordID.recordType )
-                {
-                    case SETTING:
-                    {
-                        final StoredValue currentValue = readSetting( ( PwmSetting ) configRecordID.recordID, configRecordID.profileID );
-                        final PwmSetting pwmSetting = ( PwmSetting ) configRecordID.recordID;
-                        final String keyName = pwmSetting.toMenuLocationDebug( configRecordID.getProfileID(), locale );
-                        final String debugValue = currentValue.toDebugString( locale );
-                        outputMap.put( keyName, debugValue );
-                    }
-                    break;
-
-                    case LOCALE_BUNDLE:
-                    {
-                        final String key = ( String ) configRecordID.recordID;
-                        final String bundleName = key.split( "!" )[ 0 ];
-                        final String keys = key.split( "!" )[ 1 ];
-                        final Map<String, String> currentValue = readLocaleBundleMap( bundleName, keys );
-                        final String debugValue = JsonUtil.serializeMap( currentValue, JsonUtil.Flag.PrettyPrint );
-                        outputMap.put( "LocaleBundle" + SEPARATOR + bundleName + " " + keys, debugValue );
-                    }
-                    break;
-
-                    default:
-                        // continue processing
-                        break;
-                }
-            }
-            final StringBuilder output = new StringBuilder();
-            if ( outputMap.isEmpty() )
-            {
-                output.append( "No setting changes." );
-            }
-            else
-            {
-                for ( final Map.Entry<String, String> entry : outputMap.entrySet() )
-                {
-                    final String keyName = entry.getKey();
-                    final String value = entry.getValue();
-                    if ( asHtml )
-                    {
-                        output.append( "<div class=\"changeLogKey\">" );
-                        output.append( keyName );
-                        output.append( "</div><div class=\"changeLogValue\">" );
-                        output.append( StringUtil.escapeHtml( value ) );
-                        output.append( "</div>" );
-                    }
-                    else
-                    {
-                        output.append( keyName );
-                        output.append( "\n" );
-                        output.append( " Value: " );
-                        output.append( value );
-                        output.append( "\n" );
-                    }
-                }
-            }
-            return output.toString();
-        }
-
-        public void updateChangeLog( final String bundleName, final String keyName, final Map<String, String> localeValueMap )
-        {
-            final String key = bundleName + "!" + keyName;
-            final Map<String, String> currentValue = readLocaleBundleMap( bundleName, keyName );
-            final String currentJsonValue = JsonUtil.serializeMap( currentValue );
-            final String newJsonValue = JsonUtil.serializeMap( localeValueMap );
-            final ConfigRecordID configRecordID = new ConfigRecordID( ConfigRecordID.RecordType.LOCALE_BUNDLE, key, null );
-            updateChangeLog( configRecordID, currentJsonValue, newJsonValue );
-        }
-
-        public void updateChangeLog( final PwmSetting setting, final String profileID, final StoredValue newValue )
-        {
-            final StoredValue currentValue = readSetting( setting, profileID );
-            final String currentJsonValue = JsonUtil.serialize( currentValue );
-            final String newJsonValue = JsonUtil.serialize( newValue );
-            final ConfigRecordID configRecordID = new ConfigRecordID( ConfigRecordID.RecordType.SETTING, setting, profileID );
-            updateChangeLog( configRecordID, currentJsonValue, newJsonValue );
-        }
-
-        public void updateChangeLog( final ConfigRecordID configRecordID, final String currentValueString, final String newValueString )
-        {
-            if ( changeLog.containsKey( configRecordID ) )
-            {
-                final String currentRecord = changeLog.get( configRecordID );
-
-                if ( currentRecord == null && newValueString == null )
-                {
-                    changeLog.remove( configRecordID );
-                }
-                else if ( currentRecord != null && currentRecord.equals( newValueString ) )
-                {
-                    changeLog.remove( configRecordID );
-                }
-            }
-            else
-            {
-                changeLog.put( configRecordID, currentValueString );
-            }
-        }
-    }
-
-    public static void validateXmlSchema( final String xmlDocument )
-            throws PwmUnrecoverableException
-    {
-        return;
-                /*
-        try {
-            final InputStream xsdInputStream = PwmSetting.class.getClassLoader().getResourceAsStream("password/pwm/config/StoredConfiguration.xsd");
-            final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
-            final Schema schema = factory.newSchema(new StreamSource(xsdInputStream));
-            Validator validator = schema.newValidator();
-            validator.validate(new StreamSource(new StringReader(xmlDocument)));
-        } catch (Exception e) {
-            final String errorMsg = "error while validating setting file schema definition: " + e.getMessage();
-            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,errorMsg));
-        }
-        */
-    }
-
-    private void updateMetaData( final XmlElement settingElement, final UserIdentity userIdentity )
-    {
-        final XmlElement settingsElement = document.getRootElement().getChild( XML_ELEMENT_SETTINGS );
-        settingElement.setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-        settingsElement.setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-        settingElement.removeAttribute( XML_ATTRIBUTE_MODIFY_USER );
-        settingsElement.removeAttribute( XML_ATTRIBUTE_MODIFY_USER );
-        if ( userIdentity != null )
-        {
-            settingElement.setAttribute( XML_ATTRIBUTE_MODIFY_USER, userIdentity.toDelimitedKey() );
-            settingsElement.setAttribute( XML_ATTRIBUTE_MODIFY_USER, userIdentity.toDelimitedKey() );
-        }
-    }
-
-    private XmlElement createOrGetSettingElement(
-            final PwmSetting setting,
-            final String profileID
-    )
-    {
-        final XmlElement existingSettingElement = xmlHelper.xpathForSetting( setting, profileID );
-        if ( existingSettingElement != null )
-        {
-            return existingSettingElement;
-        }
-
-        final XmlElement settingElement = xmlHelper.getXmlFactory().newElement( XML_ELEMENT_SETTING );
-        settingElement.setAttribute( XML_ATTRIBUTE_KEY, setting.getKey() );
-        settingElement.setAttribute( XML_ATTRIBUTE_SYNTAX, setting.getSyntax().toString() );
-        if ( profileID != null && profileID.length() > 0 )
-        {
-            settingElement.setAttribute( XML_ATTRIBUTE_PROFILE, profileID );
-        }
-
-        XmlElement settingsElement = document.getRootElement().getChild( XML_ELEMENT_SETTINGS );
-        if ( settingsElement == null )
-        {
-            settingsElement = xmlHelper.getXmlFactory().newElement( XML_ELEMENT_SETTINGS );
-            document.getRootElement().addContent( settingsElement );
-        }
-        settingsElement.addContent( settingElement );
-
-        return settingElement;
-    }
-
-    public static class SettingValueRecord implements Serializable
-    {
-        private PwmSetting setting;
-        private String profile;
-        private StoredValue storedValue;
-
-        public SettingValueRecord(
-                final PwmSetting setting,
-                final String profile,
-                final StoredValue storedValue
-        )
-        {
-            this.setting = setting;
-            this.profile = profile;
-            this.storedValue = storedValue;
-        }
-
-        public PwmSetting getSetting( )
-        {
-            return setting;
-        }
-
-        public String getProfile( )
-        {
-            return profile;
-        }
-
-        public StoredValue getStoredValue( )
-        {
-            return storedValue;
-        }
-    }
-
-    class StoredValueIterator implements Iterator<StoredConfigurationImpl.SettingValueRecord>
-    {
-
-        private Queue<SettingValueRecord> settingQueue = new LinkedList<>();
-
-        StoredValueIterator( final boolean includeDefaults )
-        {
-            for ( final PwmSetting setting : PwmSetting.values() )
-            {
-                if ( setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles() )
-                {
-                    if ( includeDefaults || !isDefaultValue( setting ) )
-                    {
-                        final SettingValueRecord settingValueRecord = new SettingValueRecord( setting, null, null );
-                        settingQueue.add( settingValueRecord );
-                    }
-                }
-            }
-
-            for ( final PwmSettingCategory category : PwmSettingCategory.values() )
-            {
-                if ( category.hasProfiles() )
-                {
-                    for ( final String profileID : profilesForSetting( category.getProfileSetting() ) )
-                    {
-                        for ( final PwmSetting setting : category.getSettings() )
-                        {
-                            if ( includeDefaults || !isDefaultValue( setting, profileID ) )
-                            {
-                                final SettingValueRecord settingValueRecord = new SettingValueRecord( setting, profileID, null );
-                                settingQueue.add( settingValueRecord );
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-
-        @Override
-        public boolean hasNext( )
-        {
-            return !settingQueue.isEmpty();
-        }
-
-        @Override
-        public SettingValueRecord next( )
-        {
-            final StoredConfigurationImpl.SettingValueRecord settingValueRecord = settingQueue.poll();
-            return new SettingValueRecord(
-                    settingValueRecord.getSetting(),
-                    settingValueRecord.getProfile(),
-                    readSetting( settingValueRecord.getSetting(), settingValueRecord.getProfile() )
-            );
-        }
-
-        @Override
-        public void remove( )
-        {
-
-        }
-    }
-
-    private String createTime( )
-    {
-        final XmlElement rootElement = document.getRootElement();
-        final String createTimeString = rootElement.getAttributeValue( XML_ATTRIBUTE_CREATE_TIME );
-        if ( createTimeString == null || createTimeString.isEmpty() )
-        {
-            throw new IllegalStateException( "missing createTime timestamp" );
-        }
-        return createTimeString;
-    }
-
-    @Override
-    public Instant modifyTime( )
-    {
-        final XmlElement rootElement = document.getRootElement();
-        final String modifyTimeString = rootElement.getAttributeValue( XML_ATTRIBUTE_MODIFY_TIME );
-        if ( modifyTimeString != null )
-        {
-            try
-            {
-                return JavaHelper.parseIsoToInstant( modifyTimeString );
-            }
-            catch ( Exception e )
-            {
-                LOGGER.error( "error parsing root last modified timestamp: " + e.getMessage() );
-            }
-        }
-        return null;
-    }
-
-    public void initNewRandomSecurityKey( )
-            throws PwmUnrecoverableException
-    {
-        if ( !isDefaultValue( PwmSetting.PWM_SECURITY_KEY ) )
-        {
-            return;
-        }
-
-        writeSetting(
-                PwmSetting.PWM_SECURITY_KEY,
-                new PasswordValue( new PasswordData( PwmRandom.getInstance().alphaNumericString( 1024 ) ) ),
-                null
-        );
-
-        LOGGER.debug( () -> "initialized new random security key" );
-    }
-
+        return Optional.ofNullable( metaValues.get( storedConfigItemKey ) );
+    }
 
     @Override
-    public boolean isLocked( )
-    {
-        return locked;
-    }
-
-    private List<ConfigRecordID> allSettingConfigRecordIDs( )
-    {
-        final LinkedHashSet<ConfigRecordID> loopResults = new LinkedHashSet<>();
-        for ( final PwmSetting loopSetting : PwmSetting.values() )
-        {
-            if ( loopSetting.getCategory().hasProfiles() )
-            {
-                for ( final String profile : profilesForSetting( loopSetting ) )
-                {
-                    loopResults.add( new ConfigRecordID( ConfigRecordID.RecordType.SETTING, loopSetting, profile ) );
-                }
-            }
-            else
-            {
-                loopResults.add( new ConfigRecordID( ConfigRecordID.RecordType.SETTING, loopSetting, null ) );
-            }
-        }
-        return new ArrayList<>( loopResults );
-    }
-
-    XmlHelper getXmlHelper()
+    public Optional<StoredValue> readStoredValue( final StoredConfigItemKey storedConfigItemKey )
     {
-        return xmlHelper;
+        return Optional.ofNullable( storedValues.get( storedConfigItemKey ) );
     }
 }

+ 291 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java

@@ -0,0 +1,291 @@
+/*
+ * 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.bean.UserIdentity;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingCategory;
+import password.pwm.config.StoredValue;
+import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.StringArrayValue;
+import password.pwm.config.value.StringValue;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.PwmLocaleBundle;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.secure.BCrypt;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class StoredConfigurationModifier
+{
+    private final AtomicReference<StoredConfigData> ref = new AtomicReference<>( );
+
+    private StoredConfigurationModifier( final StoredConfiguration storedConfiguration )
+    {
+        this.ref.set( ( ( StoredConfigurationImpl ) storedConfiguration ).asStoredConfigData() );
+    }
+
+    public static StoredConfigurationModifier newModifier( final StoredConfiguration storedConfiguration )
+    {
+        return new StoredConfigurationModifier( storedConfiguration );
+    }
+
+    public StoredConfiguration newStoredConfiguration()
+    {
+        return new StoredConfigurationImpl( ref.get() );
+    }
+
+    public void writeSetting(
+            final PwmSetting setting,
+            final String profileID,
+            final StoredValue value,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        Objects.requireNonNull( setting );
+        Objects.requireNonNull( value );
+
+        update( ( storedConfigData ) ->
+        {
+            if ( StringUtil.isEmpty( profileID ) && setting.getCategory().hasProfiles() )
+            {
+                throw new IllegalArgumentException( "writing of setting " + setting.getKey() + " requires a non-null profileID" );
+            }
+            if ( !StringUtil.isEmpty( profileID ) && !setting.getCategory().hasProfiles() )
+            {
+                throw new IllegalArgumentException( "cannot specify profile for non-profile setting" );
+            }
+
+            final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+
+
+            return storedConfigData.toBuilder()
+                    .storedValue( key, value )
+                    .metaData( key, new ValueMetaData( Instant.now(), userIdentity ) )
+                    .build();
+        } );
+    }
+
+    public void writeConfigProperty(
+            final ConfigurationProperty propertyName,
+            final String value
+    )
+            throws PwmUnrecoverableException
+    {
+        update( ( storedConfigData ) ->
+        {
+            final StoredConfigItemKey key = StoredConfigItemKey.fromConfigurationProperty( propertyName );
+            final Map<StoredConfigItemKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
+
+            if ( StringUtil.isEmpty( value ) )
+            {
+                existingStoredValues.remove( key );
+            }
+            else
+            {
+                final StoredValue storedValue = new StringValue( value );
+                existingStoredValues.put( key, storedValue );
+            }
+
+            return storedConfigData.toBuilder()
+                    .clearStoredValues()
+                    .storedValues( existingStoredValues )
+                    .build();
+        } );
+    }
+
+    public void resetLocaleBundleMap( final PwmLocaleBundle pwmLocaleBundle, final String keyName )
+            throws PwmUnrecoverableException
+    {
+        update( ( storedConfigData ) ->
+        {
+            final Map<StoredConfigItemKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
+
+            final StoredConfigItemKey key = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, keyName );
+            existingStoredValues.remove( key );
+
+            return storedConfigData.toBuilder()
+                    .clearStoredValues()
+                    .storedValues( existingStoredValues )
+                    .build();
+        } );
+    }
+
+    public void resetSetting( final PwmSetting setting, final String profileID, final UserIdentity userIdentity )
+            throws PwmUnrecoverableException
+    {
+        update( ( storedConfigData ) ->
+        {
+            final Map<StoredConfigItemKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
+
+            final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+            existingStoredValues.remove( key );
+
+            return storedConfigData.toBuilder()
+                    .clearStoredValues()
+                    .storedValues( existingStoredValues )
+                    .metaData( key, new ValueMetaData( Instant.now(), userIdentity ) )
+                    .build();
+        } );
+    }
+
+    public void writeLocaleBundleMap(
+            final PwmLocaleBundle pwmLocaleBundle,
+            final String keyName,
+            final Map<String, String> localeMap
+    )
+            throws PwmUnrecoverableException
+    {
+        update( ( storedConfigData ) ->
+        {
+            final StoredConfigItemKey key = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, keyName );
+            final StoredValue value = new LocalizedStringValue( localeMap );
+
+            return storedConfigData.toBuilder()
+                    .storedValue( key, value )
+                    .build();
+        } );
+    }
+
+    public void copyProfileID(
+            final PwmSettingCategory category,
+            final String sourceID,
+            final String destinationID,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+
+        if ( !category.hasProfiles() )
+        {
+            throw PwmUnrecoverableException.newException(
+                    PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category " + category + ", category does not have profiles" );
+        }
+
+        update( ( storedConfigData ) ->
+        {
+            final StoredConfiguration oldStoredConfiguration = new StoredConfigurationImpl( storedConfigData );
+
+            final List<String> existingProfiles = oldStoredConfiguration.profilesForSetting( category.getProfileSetting() );
+            if ( !existingProfiles.contains( sourceID ) )
+            {
+                throw PwmUnrecoverableException.newException(
+                        PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category, source profileID '" + sourceID + "' does not exist" );
+            }
+
+            if ( existingProfiles.contains( destinationID ) )
+            {
+                throw PwmUnrecoverableException.newException(
+                        PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category, destination profileID '" + destinationID + "' already exists" );
+            }
+
+            final Collection<PwmSettingCategory> interestedCategories = PwmSettingCategory.associatedProfileCategories( category );
+            for ( final PwmSettingCategory interestedCategory : interestedCategories )
+            {
+                for ( final PwmSetting pwmSetting : interestedCategory.getSettings() )
+                {
+                    if ( !oldStoredConfiguration.isDefaultValue( pwmSetting, sourceID ) )
+                    {
+                        final StoredValue value = oldStoredConfiguration.readSetting( pwmSetting, sourceID );
+                        writeSetting( pwmSetting, destinationID, value, userIdentity );
+                    }
+                }
+            }
+            final List<String> newProfileIDList = new ArrayList<>( existingProfiles );
+            newProfileIDList.add( destinationID );
+
+            final StoredValue value = new StringArrayValue( newProfileIDList );
+            final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( category.getProfileSetting(), null );
+            final ValueMetaData valueMetaData = new ValueMetaData( Instant.now(), userIdentity );
+
+            return storedConfigData.toBuilder()
+                    .storedValue( key, value )
+                    .metaData( key, valueMetaData )
+                    .build();
+
+        } );
+    }
+
+    public void setPassword( final String password )
+            throws PwmOperationalException, PwmUnrecoverableException
+    {
+        if ( password == null || password.isEmpty() )
+        {
+            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                    {
+                            "can not set blank password",
+                    }
+            ) );
+        }
+        final String trimmedPassword = password.trim();
+        if ( trimmedPassword.length() < 1 )
+        {
+            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                    {
+                            "can not set blank password",
+                    }
+            ) );
+        }
+
+
+        final String passwordHash = BCrypt.hashPassword( password );
+        this.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, passwordHash );
+    }
+
+    private void update( final FunctionWithException<StoredConfigData> function ) throws PwmUnrecoverableException
+    {
+        try
+        {
+            ref.updateAndGet( storedConfigData ->
+            {
+                try
+                {
+                    return function.applyThrows( storedConfigData );
+                }
+                catch ( final PwmUnrecoverableException e )
+                {
+                    throw new RuntimeException( e );
+                }
+            } );
+        }
+        catch ( final RuntimeException e )
+        {
+            throw ( PwmUnrecoverableException ) e.getCause();
+        }
+        ref.updateAndGet( storedConfigData -> storedConfigData.toBuilder().modifyTime( Instant.now() ).build() );
+    }
+
+    interface FunctionWithException<T>
+    {
+        T applyThrows( T value ) throws PwmUnrecoverableException;
+    }
+}

+ 348 - 37
server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java

@@ -20,24 +20,49 @@
 
 package password.pwm.config.stored;
 
+import password.pwm.PwmConstants;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.StoredValue;
+import password.pwm.config.value.PasswordValue;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.PasswordData;
 import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.BCrypt;
+import password.pwm.util.secure.PwmRandom;
 
-import java.io.Serializable;
+import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
 
 public abstract class StoredConfigurationUtil
 {
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationUtil.class );
+
     public static List<String> profilesForSetting
-            ( final PwmSetting pwmSetting,
-              final StoredConfiguration storedConfiguration
+            (
+                    final PwmSetting pwmSetting,
+                    final StoredConfiguration storedConfiguration
             )
     {
         if ( !pwmSetting.getCategory().hasProfiles() && pwmSetting.getSyntax() != PwmSettingSyntax.PROFILE )
@@ -88,7 +113,7 @@ public abstract class StoredConfigurationUtil
             profileSetting = pwmSetting.getCategory().getProfileSetting();
         }
 
-        final Object nativeObject = storedConfiguration.readSetting( profileSetting ).toNativeObject();
+        final Object nativeObject = storedConfiguration.readSetting( profileSetting, null ).toNativeObject();
         final List<String> settingValues = ( List<String> ) nativeObject;
         final LinkedList<String> profiles = new LinkedList<>( settingValues );
         profiles.removeIf( profile -> StringUtil.isEmpty( profile ) );
@@ -96,66 +121,352 @@ public abstract class StoredConfigurationUtil
 
     }
 
+    public static String changeLogAsDebugString(
+            final StoredConfiguration storedConfiguration,
+            final Set<StoredConfigItemKey> configChangeLog,
+            final Locale locale
+    )
+            throws PwmUnrecoverableException
+    {
+
+        final Map<String, String> outputMap = StoredConfigurationUtil.makeDebugMap( storedConfiguration, configChangeLog, locale );
+        final StringBuilder output = new StringBuilder();
+        if ( outputMap.isEmpty() )
+        {
+            output.append( "No setting changes." );
+        }
+        else
+        {
+            for ( final Map.Entry<String, String> entry : outputMap.entrySet() )
+            {
+                final String keyName = entry.getKey();
+                final String value = entry.getValue();
+                output.append( keyName );
+                output.append( "\n" );
+                output.append( " Value: " );
+                output.append( value );
+                output.append( "\n" );
+            }
+        }
+        return output.toString();
+
+    }
 
-    public static List<StoredConfigReference> modifiedSettings( final StoredConfiguration storedConfiguration )
+    public static StoredConfiguration copyConfigAndBlankAllPasswords( final StoredConfiguration input )
+            throws PwmUnrecoverableException
     {
-        final List<StoredConfigReference> returnObj = new ArrayList<>();
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( input );
 
-        for ( final PwmSetting setting : PwmSetting.values() )
+        for ( final StoredConfigItemKey storedConfigItemKey : input.modifiedItems() )
         {
-            if ( setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles() )
+            if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
             {
-                if ( !storedConfiguration.isDefaultValue( setting, null ) )
+                final PwmSetting pwmSetting = storedConfigItemKey.toPwmSetting();
+                if ( pwmSetting.getSyntax() == PwmSettingSyntax.PASSWORD )
                 {
-                    final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                            StoredConfigReference.RecordType.SETTING,
-                            setting.getKey(),
-                            null
-                    );
-                    returnObj.add( storedConfigReference );
+                    final ValueMetaData valueMetaData = input.readSettingMetadata( pwmSetting, storedConfigItemKey.getProfileID() );
+                    final UserIdentity userIdentity = valueMetaData == null ? null : valueMetaData.getUserIdentity();
+                    final PasswordValue passwordValue = new PasswordValue( new PasswordData( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT ) );
+                    modifier.writeSetting( pwmSetting, storedConfigItemKey.getProfileID(), passwordValue, userIdentity );
                 }
             }
         }
 
-        for ( final PwmSettingCategory category : PwmSettingCategory.values() )
+
+        final Optional<String> pwdHash = input.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
+        if ( pwdHash.isPresent() )
         {
-            if ( category.hasProfiles() )
+            modifier.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT );
+        }
+
+        return modifier.newStoredConfiguration();
+    }
+
+    public static List<String> validateValues( final StoredConfiguration storedConfiguration )
+    {
+        final Instant startTime = Instant.now();
+        final List<String> errorStrings = new ArrayList<>();
+
+        for ( final StoredConfigItemKey storedConfigItemKey : storedConfiguration.modifiedItems() )
+        {
+            if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
             {
-                for ( final String profileID : profilesForSetting( category.getProfileSetting(), storedConfiguration ) )
+                final PwmSetting pwmSetting = storedConfigItemKey.toPwmSetting();
+                final String profileID = storedConfigItemKey.getProfileID();
+                final StoredValue loopValue = storedConfiguration.readSetting( pwmSetting, profileID );
+
+                try
                 {
-                    for ( final PwmSetting setting : category.getSettings() )
+                    final List<String> errors = loopValue.validateValue( pwmSetting );
+                    for ( final String loopError : errors )
                     {
-                        if ( !storedConfiguration.isDefaultValue( setting, profileID ) )
-                        {
-                            final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                                    StoredConfigReference.RecordType.SETTING,
-                                    setting.getKey(),
-                                    profileID
-                            );
-                            returnObj.add( storedConfigReference );
-                        }
+                        errorStrings.add( pwmSetting.toMenuLocationDebug( storedConfigItemKey.getProfileID(), PwmConstants.DEFAULT_LOCALE ) + " - " + loopError );
+                    }
+                }
+                catch ( final Exception e )
+                {
+                    LOGGER.error( "unexpected error during validate value for "
+                            + pwmSetting.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) + ", error: "
+                            + e.getMessage(), e );
+                }
+            }
+        }
+
+        LOGGER.trace( () -> "StoredConfiguration validator completed in " + TimeDuration.compactFromCurrent( startTime ) );
+        return errorStrings;
+    }
+
+    public static Set<StoredConfigItemKey> search( final StoredConfiguration storedConfiguration, final String searchTerm, final Locale locale )
+    {
+        return new SettingSearchMachine( storedConfiguration, searchTerm, locale ).search();
+    }
+
+    public static boolean matchSetting(
+            final StoredConfiguration storedConfiguration,
+            final PwmSetting setting,
+            final StoredValue storedValue,
+            final String term,
+            final Locale defaultLocale )
+    {
+        return new SettingSearchMachine( storedConfiguration, term, defaultLocale ).matchSetting( setting, storedValue, term );
+    }
+
+    private static class SettingSearchMachine
+    {
+        private final StoredConfiguration storedConfiguration;
+        private final String searchTerm;
+        private final Locale locale;
+
+        private SettingSearchMachine( final StoredConfiguration storedConfiguration, final String searchTerm, final Locale locale )
+        {
+            this.storedConfiguration = storedConfiguration;
+            this.searchTerm = searchTerm;
+            this.locale = locale;
+        }
+
+        public Set<StoredConfigItemKey> search()
+        {
+            if ( StringUtil.isEmpty( searchTerm ) )
+            {
+                return Collections.emptySet();
+            }
+
+            return allPossibleSettingKeysForConfiguration( storedConfiguration )
+                    .parallelStream()
+                    .filter( s -> s.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+                    .filter( this::matchSetting )
+                    .collect( Collectors.toCollection( TreeSet::new ) );
+        }
+
+        private boolean matchSetting(
+                final StoredConfigItemKey storedConfigItemKey
+        )
+        {
+            final PwmSetting pwmSetting = storedConfigItemKey.toPwmSetting();
+            final StoredValue value = storedConfiguration.readSetting( pwmSetting, storedConfigItemKey.getProfileID() );
+
+            return StringUtil.whitespaceSplit( searchTerm )
+                    .parallelStream()
+                    .allMatch( s -> matchSetting( pwmSetting, value, s ) );
+        }
+
+        private boolean matchSetting( final PwmSetting setting, final StoredValue value, final String searchTerm )
+        {
+            if ( setting.isHidden() || setting.getCategory().isHidden() )
+            {
+                return false;
+            }
+
+            if ( searchTerm == null || searchTerm.isEmpty() )
+            {
+                return false;
+            }
+
+            final String lowerSearchTerm = searchTerm.toLowerCase();
+
+            {
+                final String key = setting.getKey();
+                if ( key.toLowerCase().contains( lowerSearchTerm ) )
+                {
+                    return true;
+                }
+            }
+            {
+                final String label = setting.getLabel( locale );
+                if ( label.toLowerCase().contains( lowerSearchTerm ) )
+                {
+                    return true;
+                }
+            }
+            {
+                final String descr = setting.getDescription( locale );
+                if ( descr.toLowerCase().contains( lowerSearchTerm ) )
+                {
+                    return true;
+                }
+            }
+            {
+                final String menuLocationString = setting.toMenuLocationDebug( null, locale );
+                if ( menuLocationString.toLowerCase().contains( lowerSearchTerm ) )
+                {
+                    return true;
+                }
+            }
+
+            if ( setting.isConfidential() )
+            {
+                return false;
+            }
+            {
+                final String valueDebug = value.toDebugString( locale );
+                if ( valueDebug != null && valueDebug.toLowerCase().contains( lowerSearchTerm ) )
+                {
+                    return true;
+                }
+            }
+            if ( PwmSettingSyntax.SELECT == setting.getSyntax()
+                    || PwmSettingSyntax.OPTIONLIST == setting.getSyntax()
+                    || PwmSettingSyntax.VERIFICATION_METHOD == setting.getSyntax()
+            )
+            {
+                for ( final String key : setting.getOptions().keySet() )
+                {
+                    if ( key.toLowerCase().contains( lowerSearchTerm ) )
+                    {
+                        return true;
+                    }
+                    final String optionValue = setting.getOptions().get( key );
+                    if ( optionValue != null && optionValue.toLowerCase().contains( lowerSearchTerm ) )
+                    {
+                        return true;
                     }
                 }
             }
+            return false;
         }
+    }
 
-        return returnObj;
+    public static boolean verifyPassword( final StoredConfiguration storedConfiguration, final String password )
+    {
+        if ( !hasPassword( storedConfiguration ) )
+        {
+            return false;
+        }
+        final Optional<String> passwordHash = storedConfiguration.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
+        return passwordHash.isPresent() && BCrypt.testAnswer( password, passwordHash.get(), new Configuration( storedConfiguration ) );
     }
 
-    public static Serializable toJsonDebugObject( final StoredConfiguration storedConfiguration )
+    public static boolean hasPassword( final StoredConfiguration storedConfiguration )
     {
-        final TreeMap<String, Object> outputObject = new TreeMap<>();
+        final Optional<String> passwordHash = storedConfiguration.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
+        return passwordHash.isPresent();
+    }
 
-        for ( final StoredConfigReference storedConfigReference : modifiedSettings( storedConfiguration ) )
+    public static void setPassword( final StoredConfigurationModifier storedConfiguration, final String password )
+            throws PwmOperationalException, PwmUnrecoverableException
+    {
+        if ( StringUtil.isEmpty( password ) )
+        {
+            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                    {
+                            "can not set blank password",
+                    }
+            ) );
+        }
+        final String trimmedPassword = password.trim();
+        if ( trimmedPassword.length() < 1 )
         {
-            final PwmSetting setting = PwmSetting.forKey( storedConfigReference.getRecordID() );
-            if ( setting != null )
+            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                    {
+                            "can not set blank password",
+                    }
+            ) );
+        }
+
+
+        final String passwordHash = BCrypt.hashPassword( password );
+        storedConfiguration.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, passwordHash );
+    }
+
+    public static void initNewRandomSecurityKey( final StoredConfigurationModifier modifier )
+            throws PwmUnrecoverableException
+    {
+        if ( !modifier.newStoredConfiguration().isDefaultValue( PwmSetting.PWM_SECURITY_KEY, null ) )
+        {
+            return;
+        }
+
+        modifier.writeSetting(
+                PwmSetting.PWM_SECURITY_KEY, null,
+                new PasswordValue( new PasswordData( PwmRandom.getInstance().alphaNumericString( 1024 ) ) ),
+                null
+        );
+
+        LOGGER.debug( () -> "initialized new random security key" );
+    }
+
+
+    public static Map<String, String> makeDebugMap(
+            final StoredConfiguration storedConfiguration,
+            final Collection<StoredConfigItemKey> interestedItems,
+            final Locale locale
+    )
+    {
+        final Map<String, String> outputMap = interestedItems.stream()
+                .filter( ( key ) -> key.getRecordType() != StoredConfigItemKey.RecordType.PROPERTY )
+                .filter( ( key ) -> storedConfiguration.readStoredValue( key ).isPresent() )
+                .collect( Collectors.toMap(
+                        key -> key.getLabel( locale ),
+                        key -> storedConfiguration.readStoredValue( key ).get().toDebugString( locale ) ) );
+
+        return Collections.unmodifiableMap( new TreeMap<>( outputMap ) );
+    }
+
+    public static Set<StoredConfigItemKey> allPossibleSettingKeysForConfiguration(
+            final StoredConfiguration storedConfiguration
+    )
+    {
+        final Set<StoredConfigItemKey> loopResults = new HashSet<>();
+        for ( final PwmSetting loopSetting : PwmSetting.values() )
+        {
+            if ( loopSetting.getCategory().hasProfiles() )
             {
-                final StoredValue value = storedConfiguration.readSetting( setting, storedConfigReference.getProfileID() );
-                outputObject.put( setting.getKey(), value.toDebugJsonObject( null ) );
+                for ( final String profile : storedConfiguration.profilesForSetting( loopSetting ) )
+                {
+                    loopResults.add( StoredConfigItemKey.fromSetting( loopSetting, profile ) );
+                }
+            }
+            else
+            {
+                loopResults.add( StoredConfigItemKey.fromSetting( loopSetting, null ) );
             }
         }
-        return outputObject;
+        return Collections.unmodifiableSet( loopResults );
     }
 
+    public static Set<StoredConfigItemKey> changedValues (
+            final StoredConfiguration originalConfiguration,
+            final StoredConfiguration modifiedConfiguration
+    )
+    {
+        final Instant startTime = Instant.now();
+
+        final Set<StoredConfigItemKey> interestedReferences = new HashSet<>();
+        interestedReferences.addAll( originalConfiguration.modifiedItems() );
+        interestedReferences.addAll( modifiedConfiguration.modifiedItems() );
+
+        final Set<StoredConfigItemKey> deltaReferences = interestedReferences
+                .parallelStream()
+                .filter( reference ->
+                        {
+                            final Optional<String> hash1 = originalConfiguration.readStoredValue( reference ).map( StoredValue::valueHash );
+                            final Optional<String> hash2 = modifiedConfiguration.readStoredValue( reference ).map( StoredValue::valueHash );
+                            return hash1.isPresent() && hash2.isPresent() && !Objects.equals( hash1.get(), hash2.get() );
+                        }
+                ).collect( Collectors.toSet() );
+
+        LOGGER.trace( () -> "generated changeLog items via compare in " + TimeDuration.compactFromCurrent( startTime ) );
+
+        return Collections.unmodifiableSet( deltaReferences );
+    }
 }

+ 11 - 15
server/src/main/java/password/pwm/config/stored/ConfigChangeLog.java → server/src/main/java/password/pwm/config/stored/XmlOutputProcessData.java

@@ -20,20 +20,16 @@
 
 package password.pwm.config.stored;
 
-import password.pwm.config.StoredValue;
-
-import java.util.Collection;
-import java.util.Locale;
-
-public interface ConfigChangeLog
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.config.value.StoredValueEncoder;
+import password.pwm.util.secure.PwmSecurityKey;
+
+@Value
+@Builder
+public class XmlOutputProcessData
 {
-    boolean isModified( );
-
-    String changeLogAsDebugString( Locale locale, boolean asHtml );
-
-    void updateChangeLog( StoredConfigReference reference, StoredValue newValue );
-
-    void updateChangeLog( StoredConfigReference reference, StoredValue currentValue, StoredValue newValue );
-
-    Collection<StoredConfigReference> changedValues( );
+    @Builder.Default
+    private StoredValueEncoder.Mode storedValueEncoderMode = StoredValueEncoder.Mode.ENCODED;
+    private PwmSecurityKey pwmSecurityKey;
 }

+ 0 - 99
server/src/main/java/password/pwm/config/stored/ng/NGStorageEngineImpl.java

@@ -1,99 +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.ng;
-
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigReference;
-import password.pwm.config.stored.ValueMetaData;
-
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.Map;
-
-class NGStorageEngineImpl
-{
-    private final Map<StoredConfigReference, StoredValue> storedValues = new HashMap<>();
-    private final Map<StoredConfigReference, ValueMetaData> metaValues = new HashMap<>();
-
-    NGStorageEngineImpl()
-    {
-    }
-
-    NGStorageEngineImpl(
-            final Map<StoredConfigReference, StoredValue> storedValues,
-            final Map<StoredConfigReference, ValueMetaData> metaValues
-    )
-    {
-        this.storedValues.putAll( storedValues );
-        this.metaValues.putAll( metaValues );
-    }
-
-    StoredValue read( final StoredConfigReference storedConfigReference )
-    {
-        return storedValues.get( storedConfigReference );
-    }
-
-    ValueMetaData readMetaData( final StoredConfigReference storedConfigReference )
-    {
-        return metaValues.get( storedConfigReference );
-    }
-
-    void writeMetaData( final StoredConfigReference storedConfigReference, final ValueMetaData valueMetaData )
-    {
-        metaValues.put( storedConfigReference, valueMetaData );
-    }
-
-    void write( final StoredConfigReference reference, final StoredValue value, final UserIdentity userIdentity )
-    {
-        if ( reference != null )
-        {
-            if ( value != null )
-            {
-                storedValues.put( reference, value );
-            }
-
-            updateUserIdentity( reference, userIdentity );
-        }
-    }
-
-    void reset( final StoredConfigReference reference, final UserIdentity userIdentity )
-    {
-        if ( reference != null )
-        {
-            storedValues.remove( reference );
-            updateUserIdentity( reference, userIdentity );
-        }
-    }
-
-    private void updateUserIdentity(
-            final StoredConfigReference reference,
-            final UserIdentity userIdentity
-    )
-    {
-        metaValues.put(
-                reference,
-                ValueMetaData.builder().modifyDate( Instant.now() )
-                        .userIdentity( userIdentity )
-                        .build() );
-
-    }
-}

+ 0 - 202
server/src/main/java/password/pwm/config/stored/ng/NGStoredConfiguration.java

@@ -1,202 +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.ng;
-
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.PwmSetting;
-import password.pwm.config.PwmSettingCategory;
-import password.pwm.config.StoredValue;
-import password.pwm.config.stored.ConfigurationProperty;
-import password.pwm.config.stored.StoredConfigReference;
-import password.pwm.config.stored.StoredConfigReferenceBean;
-import password.pwm.config.stored.StoredConfiguration;
-import password.pwm.config.stored.ValueMetaData;
-import password.pwm.config.value.StringValue;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmSecurityKey;
-
-import java.time.Instant;
-
-public class NGStoredConfiguration implements StoredConfiguration
-{
-    private static final PwmLogger LOGGER = PwmLogger.forClass( NGStoredConfiguration.class );
-    private final PwmSecurityKey configurationSecurityKey;
-    private final NGStorageEngineImpl engine;
-    private boolean readOnly = false;
-
-    NGStoredConfiguration(
-            final NGStorageEngineImpl storageEngine,
-            final PwmSecurityKey pwmSecurityKey )
-    {
-        engine = storageEngine;
-        configurationSecurityKey = pwmSecurityKey;
-    }
-
-    public String readConfigProperty( final ConfigurationProperty configurationProperty )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.PROPERTY,
-                configurationProperty.getKey(),
-                null
-        );
-        final StoredValue storedValue = engine.read( storedConfigReference );
-        if ( storedValue == null | !( storedValue instanceof StringValue ) )
-        {
-            return null;
-        }
-        return ( String ) storedValue.toNativeObject();
-    }
-
-    public void writeConfigProperty(
-            final ConfigurationProperty configurationProperty,
-            final String value )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.PROPERTY,
-                configurationProperty.getKey(),
-                null
-        );
-        final StoredValue storedValue = new StringValue( value );
-        engine.write( storedConfigReference, storedValue, null  );
-    }
-
-    public void resetSetting( final PwmSetting setting, final String profileID, final UserIdentity userIdentity )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.SETTING,
-                setting.getKey(),
-                profileID
-        );
-        engine.reset( storedConfigReference, userIdentity );
-    }
-
-    public boolean isDefaultValue( final PwmSetting setting )
-    {
-        return isDefaultValue( setting, null );
-    }
-
-    public boolean isDefaultValue( final PwmSetting setting, final String profileID )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.SETTING,
-                setting.getKey(),
-                profileID
-        );
-        final StoredValue value = engine.read( storedConfigReference );
-        return value == null;
-    }
-
-    public StoredValue readSetting( final PwmSetting setting )
-    {
-        return readSetting( setting, null );
-    }
-
-    public StoredValue readSetting( final PwmSetting setting, final String profileID )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.SETTING,
-                setting.getKey(),
-                profileID
-        );
-        return engine.read( storedConfigReference );
-    }
-
-    public void copyProfileID( final PwmSettingCategory category, final String sourceID, final String destinationID, final UserIdentity userIdentity )
-            throws PwmUnrecoverableException
-    {
-        //@todo
-        throw new IllegalStateException( "not implemented" );
-    }
-
-    public void writeSetting(
-            final PwmSetting setting,
-            final StoredValue value,
-            final UserIdentity userIdentity
-    ) throws PwmUnrecoverableException
-    {
-        writeSetting( setting, null, value, userIdentity );
-    }
-
-    public void writeSetting(
-            final PwmSetting setting,
-            final String profileID,
-            final StoredValue value,
-            final UserIdentity userIdentity
-    )
-            throws PwmUnrecoverableException
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.SETTING,
-                setting.getKey(),
-                profileID
-        );
-        engine.write( storedConfigReference, value, userIdentity );
-    }
-
-    @Override
-    public PwmSecurityKey getKey( ) throws PwmUnrecoverableException
-    {
-        return configurationSecurityKey;
-    }
-
-    @Override
-    public boolean isLocked( )
-    {
-        return readOnly;
-    }
-
-    @Override
-    public void lock( )
-    {
-        readOnly = true;
-    }
-
-    @Override
-    public ValueMetaData readSettingMetadata( final PwmSetting setting, final String profileID )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.SETTING,
-                setting.getKey(),
-                profileID
-        );
-        return engine.readMetaData( storedConfigReference );
-    }
-
-    public Instant modifyTime( )
-    {
-        final String modifyTimeString = readConfigProperty( ConfigurationProperty.MODIFIFICATION_TIMESTAMP );
-        if ( modifyTimeString != null )
-        {
-            try
-            {
-                return JavaHelper.parseIsoToInstant( ( modifyTimeString ) );
-            }
-            catch ( Exception e )
-            {
-                LOGGER.error( "error parsing last modified timestamp property: " + e.getMessage() );
-            }
-        }
-        return null;
-    }
-
-}

+ 0 - 253
server/src/main/java/password/pwm/config/stored/ng/NGStoredConfigurationFactory.java

@@ -1,253 +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.ng;
-
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.PwmSetting;
-import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigReference;
-import password.pwm.config.stored.StoredConfigReferenceBean;
-import password.pwm.config.stored.StoredConfiguration;
-import password.pwm.config.stored.ValueMetaData;
-import password.pwm.config.value.StringValue;
-import password.pwm.config.value.ValueFactory;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.JavaHelper;
-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.io.InputStream;
-import java.io.OutputStream;
-import java.time.Instant;
-
-
-public class NGStoredConfigurationFactory
-{
-    private static final PwmLogger LOGGER = PwmLogger.forClass( NGStoredConfigurationFactory.class );
-
-    //@Override
-    public NGStoredConfiguration fromXml( final InputStream inputStream ) throws PwmUnrecoverableException
-    {
-        return XmlEngine.fromXmlImpl( inputStream );
-    }
-
-    //@Override
-    public void toXml( final OutputStream outputStream )
-    {
-    }
-
-    private static class XmlEngine
-    {
-        static NGStoredConfiguration fromXmlImpl( final InputStream inputStream )
-                throws PwmUnrecoverableException
-        {
-            final NGStorageEngineImpl storageEngine = new NGStorageEngineImpl();
-
-            final XmlDocument inputDocument = XmlFactory.getFactory().parseXml( inputStream );
-            final XmlElement rootElement = inputDocument.getRootElement();
-
-            final PwmSecurityKey pwmSecurityKey = readSecurityKey( rootElement );
-
-            final XmlElement settingsElement = rootElement.getChild( StoredConfiguration.XML_ELEMENT_SETTINGS );
-
-            for ( final XmlElement loopElement : settingsElement.getChildren() )
-            {
-                if ( StoredConfiguration.XML_ELEMENT_PROPERTIES.equals( loopElement.getName() ) )
-                {
-                    for ( final XmlElement propertyElement : loopElement.getChildren( StoredConfiguration.XML_ELEMENT_PROPERTY ) )
-                    {
-                        readInterestingElement( propertyElement, pwmSecurityKey, storageEngine );
-                    }
-                }
-                else if ( StoredConfiguration.XML_ELEMENT_SETTING.equals( loopElement.getName() ) )
-                {
-                    readInterestingElement( loopElement, pwmSecurityKey, storageEngine );
-                }
-            }
-            return new NGStoredConfiguration( storageEngine, pwmSecurityKey );
-        }
-
-        static void readInterestingElement(
-                final XmlElement loopElement,
-                final PwmSecurityKey pwmSecurityKey,
-                final NGStorageEngineImpl engine
-        )
-        {
-            final StoredConfigReference reference = referenceForElement( loopElement );
-            if ( reference != null )
-            {
-                switch ( reference.getRecordType() )
-                {
-                    case SETTING:
-                    {
-                        final StoredValue storedValue = readSettingValue( reference, loopElement, pwmSecurityKey );
-                        if ( storedValue != null )
-                        {
-                            engine.write( reference, storedValue, null );
-                        }
-                    }
-                    break;
-
-                    case PROPERTY:
-                    {
-                        final StoredValue storedValue = readPropertyValue( reference, loopElement );
-                        if ( storedValue != null )
-                        {
-                            engine.write( reference, storedValue, null );
-                        }
-                    }
-                    break;
-
-                    default:
-                        throw new IllegalArgumentException( "unimplemented setting recordtype in reader" );
-                }
-                engine.writeMetaData( reference, readValueMetaData( loopElement ) );
-            }
-        }
-
-        static PwmSecurityKey readSecurityKey( final XmlElement rootElement )
-                throws PwmUnrecoverableException
-        {
-            final String createTime = rootElement.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_CREATE_TIME );
-            return new PwmSecurityKey( createTime + "StoredConfiguration" );
-        }
-
-        static StoredValue readSettingValue(
-                final StoredConfigReference storedConfigReference,
-                final XmlElement settingElement,
-                final PwmSecurityKey pwmSecurityKey
-        )
-        {
-            final String key = storedConfigReference.getRecordID();
-            final PwmSetting pwmSetting = PwmSetting.forKey( key );
-
-            if ( pwmSetting == null )
-            {
-                LOGGER.debug( () -> "ignoring setting for unknown key: " + key );
-            }
-            else
-            {
-                LOGGER.trace( () -> "parsing setting key=" + key + ", profile=" + storedConfigReference.getProfileID() );
-                final XmlElement defaultElement = settingElement.getChild( StoredConfiguration.XML_ELEMENT_DEFAULT );
-                if ( defaultElement != null )
-                {
-                    return null;
-                }
-
-                {
-                    try
-                    {
-                        return ValueFactory.fromXmlValues( pwmSetting, settingElement, pwmSecurityKey );
-                    }
-                    catch ( IllegalStateException e )
-                    {
-                        LOGGER.error( "error parsing configuration setting " + storedConfigReference + ", error: " + e.getMessage() );
-                    }
-                }
-            }
-            return null;
-        }
-
-        static StoredValue readPropertyValue(
-                final StoredConfigReference storedConfigReference,
-                final XmlElement settingElement
-        )
-        {
-            final String key = storedConfigReference.getRecordID();
-
-            LOGGER.trace( () -> "parsing property key=" + key + ", profile=" + storedConfigReference.getProfileID() );
-            if ( settingElement.getChild( StoredConfiguration.XML_ELEMENT_DEFAULT ) != null )
-            {
-                return new StringValue( settingElement.getText() );
-            }
-            return null;
-        }
-
-        static StoredConfigReference referenceForElement( final XmlElement settingElement )
-        {
-            final String key = settingElement.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_KEY );
-            final String profileID = readProfileID( settingElement );
-            final StoredConfigReference.RecordType recordType;
-            switch ( settingElement.getName() )
-            {
-                case StoredConfiguration.XML_ELEMENT_SETTING:
-                    recordType = StoredConfigReference.RecordType.SETTING;
-                    break;
-
-                case StoredConfiguration.XML_ELEMENT_PROPERTY:
-                    recordType = StoredConfigReference.RecordType.PROPERTY;
-                    break;
-
-                case StoredConfiguration.XML_ELEMENT_LOCALEBUNDLE:
-                    recordType = StoredConfigReference.RecordType.LOCALE_BUNDLE;
-                    break;
-
-                default:
-                    LOGGER.warn( "unrecognized xml element " + settingElement.getName() + " in configuration" );
-                    return null;
-            }
-
-
-            return new StoredConfigReferenceBean(
-                    recordType,
-                    key,
-                    profileID
-            );
-        }
-
-        static String readProfileID( final XmlElement settingElement )
-        {
-            final String profileIDStr = settingElement.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_PROFILE );
-            return profileIDStr != null && !profileIDStr.isEmpty() ? profileIDStr : null;
-        }
-
-        static ValueMetaData readValueMetaData( final XmlElement element )
-        {
-            final String modifyDateStr = element.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_MODIFY_TIME );
-            Instant modifyDate = null;
-            try
-            {
-                modifyDate = modifyDateStr == null || modifyDateStr.isEmpty()
-                        ? null
-                        : JavaHelper.parseIsoToInstant( modifyDateStr );
-            }
-            catch ( Exception e )
-            {
-                LOGGER.warn( "error parsing stored date: " + e.getMessage() );
-            }
-            final String modifyUser = element.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_MODIFY_USER );
-            final String modifyUserProfile = element.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_MODIFY_USER_PROFILE );
-            final UserIdentity userIdentity;
-            userIdentity = modifyUser != null
-                    ? new UserIdentity( modifyUser, modifyUserProfile )
-                    : null;
-
-            return ValueMetaData.builder()
-                    .modifyDate( modifyDate )
-                    .userIdentity( userIdentity )
-                    .build();
-        }
-    }
-
-}

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

@@ -20,27 +20,31 @@
 
 package password.pwm.config.value;
 
-import lombok.Value;
 import password.pwm.PwmConstants;
 import password.pwm.config.StoredValue;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmOperationalException;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JsonUtil;
-import password.pwm.util.secure.PwmBlockAlgorithm;
-import password.pwm.util.secure.PwmRandom;
+import password.pwm.util.java.LazySupplier;
+import password.pwm.util.java.XmlDocument;
+import password.pwm.util.java.XmlElement;
+import password.pwm.util.java.XmlFactory;
+import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.SecureEngine;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.Serializable;
+import java.util.List;
 import java.util.Locale;
 
 public abstract class AbstractValue implements StoredValue
 {
-    static final String ENC_PW_PREFIX = "ENC-PW:";
+    private final transient LazySupplier<String> valueHashSupplier = new LazySupplier<>( () -> valueHashComputer( AbstractValue.this ) );
 
-    public String toString( )
+    public String toString()
     {
         return toDebugString( null );
     }
@@ -57,84 +61,39 @@ public abstract class AbstractValue implements StoredValue
         return ( Serializable ) this.toNativeObject();
     }
 
-    public boolean requiresStoredUpdate( )
-    {
-        return false;
-    }
-
     @Override
-    public int currentSyntaxVersion( )
+    public int currentSyntaxVersion()
     {
         return 0;
     }
 
     @Override
-    public String valueHash( ) throws PwmUnrecoverableException
+    public final String valueHash()
     {
-        return SecureEngine.hash( JsonUtil.serialize( ( Serializable ) this.toNativeObject() ), PwmConstants.SETTING_CHECKSUM_HASH_METHOD );
+        return valueHashSupplier.get();
     }
 
-    static String decryptPwValue( final String input, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+    static String valueHashComputer( final StoredValue storedValue )
     {
-        if ( input == null )
+        try
         {
-            return "";
-        }
+            final PwmSecurityKey testingKey = new PwmSecurityKey( "test" );
+            final XmlOutputProcessData xmlOutputProcessData = XmlOutputProcessData.builder()
+                    .pwmSecurityKey( testingKey )
+                    .storedValueEncoderMode( StoredValueEncoder.Mode.PLAIN )
+                    .build();
+            final List<XmlElement> xmlValues = storedValue.toXmlValues( StoredConfigXmlConstants.XML_ELEMENT_VALUE, xmlOutputProcessData );
+            final XmlDocument document = XmlFactory.getFactory().newDocument( "root" );
+            document.getRootElement().addContent( xmlValues );
+            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            XmlFactory.getFactory().outputDocument( document, byteArrayOutputStream );
+            final String stringToHash = new String( byteArrayOutputStream.toByteArray(), PwmConstants.DEFAULT_CHARSET );
+            return SecureEngine.hash( stringToHash, PwmHashAlgorithm.SHA512 );
 
-        if ( input.startsWith( ENC_PW_PREFIX ) )
-        {
-            try
-            {
-                final String pwValueSuffix = input.substring( ENC_PW_PREFIX.length(), input.length() );
-                final String decrpytedValue = SecureEngine.decryptStringValue( pwValueSuffix, pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
-                final StoredPwData storedPwData = JsonUtil.deserialize( decrpytedValue, StoredPwData.class );
-                return storedPwData.getValue();
-            }
-            catch ( Exception e )
-            {
-                final String errorMsg = "unable to decrypt password value for setting: " + e.getMessage();
-                final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
-                throw new PwmOperationalException( errorInfo );
-            }
         }
-
-        return input;
-    }
-
-    static String encryptPwValue( final String input, final PwmSecurityKey pwmSecurityKey )
-            throws PwmOperationalException
-    {
-        if ( input == null )
+        catch ( final IOException | PwmUnrecoverableException e )
         {
-            return "";
+            throw new IllegalStateException( e );
         }
-
-        if ( !input.startsWith( ENC_PW_PREFIX ) )
-        {
-            try
-            {
-                final String salt = PwmRandom.getInstance().alphaNumericString( 32 );
-                final StoredPwData storedPwData = new StoredPwData( salt, input );
-                final String jsonData = JsonUtil.serialize( storedPwData );
-                final String encryptedValue = SecureEngine.encryptToString( jsonData, pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
-                return ENC_PW_PREFIX + encryptedValue;
-            }
-            catch ( Exception e )
-            {
-                final String errorMsg = "unable to encrypt password value for setting: " + e.getMessage();
-                final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
-                throw new PwmOperationalException( errorInfo );
-            }
-        }
-
-        return input;
     }
-
-    @Value
-    static class StoredPwData implements Serializable
-    {
-        private String salt;
-        private String value;
-    }
-
 }

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

@@ -25,9 +25,9 @@ import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.ActionConfiguration;
-import password.pwm.config.value.data.ActionConfigurationOldVersion1;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -46,6 +46,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 public class ActionValue extends AbstractValue implements StoredValue
@@ -98,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( "syntax" ) );
-                final List<XmlElement> valueElements = settingElement.getChildren( "value" );
+                        settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX ) );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 for ( final XmlElement loopValueElement : valueElements )
                 {
                     final String stringValue = loopValueElement.getText();
@@ -109,17 +110,28 @@ public class ActionValue extends AbstractValue implements StoredValue
                         {
                             if ( oldType )
                             {
-                                if ( loopValueElement.getAttributeValue( "locale" ) == null )
+                                if ( loopValueElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE ) == null )
                                 {
-                                    final ActionConfigurationOldVersion1 oldVersion1 = ActionConfigurationOldVersion1.parseOldConfigString( stringValue );
+                                    final ActionConfiguration.ActionConfigurationOldVersion1 oldVersion1 = ActionConfiguration.ActionConfigurationOldVersion1
+                                            .parseOldConfigString( stringValue );
                                     values.add( convertOldVersion1Values( oldVersion1 ) );
                                 }
                             }
                             else
                             {
-                                final ActionConfigurationOldVersion1 parsedAc = JsonUtil.deserialize( stringValue, ActionConfigurationOldVersion1.class );
-                                parsedAc.setPassword( decryptPwValue( parsedAc.getPassword(), pwmSecurityKey ) );
-                                values.add( convertOldVersion1Values( parsedAc ) );
+                                final ActionConfiguration.ActionConfigurationOldVersion1 parsedAc = JsonUtil
+                                        .deserialize( stringValue, ActionConfiguration.ActionConfigurationOldVersion1.class );
+                                if ( parsedAc != null )
+                                {
+                                    final Optional<String> decodedValue = StoredValueEncoder.decode(
+                                            parsedAc.getPassword(),
+                                            StoredValueEncoder.Mode.ENCODED,
+                                            pwmSecurityKey );
+                                    decodedValue.ifPresent( s ->
+                                    {
+                                        values.add( convertOldVersion1Values( parsedAc.toBuilder().password( s ).build() ) );
+                                    } );
+                                }
                             }
                         }
                         else if ( syntaxVersion == 2 )
@@ -136,14 +148,23 @@ public class ActionValue extends AbstractValue implements StoredValue
                                 // decrypt pw
                                 try
                                 {
-                                    clonedWebActions.add( webAction.toBuilder()
-                                            .password( decryptPwValue( webAction.getPassword(), pwmSecurityKey ) )
-                                            .successStatus( successStatus )
-                                            .build() );
+
+
+                                    final Optional<String> decodedValue = StoredValueEncoder.decode(
+                                            webAction.getPassword(),
+                                            StoredValueEncoder.Mode.ENCODED,
+                                            pwmSecurityKey );
+                                    decodedValue.ifPresent( s ->
+                                    {
+                                        clonedWebActions.add( webAction.toBuilder()
+                                                .password( s )
+                                                .successStatus( successStatus )
+                                                .build() );
+                                    } );
                                 }
-                                catch ( PwmOperationalException e )
+                                catch ( final PwmOperationalException e )
                                 {
-                                    LOGGER.warn( "error decoding stored pw value: " + e.getMessage() );
+                                    LOGGER.warn( "error decoding stored pw value on setting '" + pwmSetting.getKey() + "': " + e.getMessage() );
                                 }
                             }
 
@@ -162,7 +183,7 @@ public class ActionValue extends AbstractValue implements StoredValue
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final ActionConfiguration value : values )
@@ -170,15 +191,22 @@ public class ActionValue extends AbstractValue implements StoredValue
             final List<ActionConfiguration.WebAction> clonedWebActions = new ArrayList<>();
             for ( final ActionConfiguration.WebAction webAction : value.getWebActions() )
             {
-                try
-                {
-                    clonedWebActions.add( webAction.toBuilder()
-                            .password( encryptPwValue( webAction.getPassword(), pwmSecurityKey ) )
-                            .build() );
-                }
-                catch ( PwmOperationalException e )
+                if ( !StringUtil.isEmpty( webAction.getPassword() ) )
                 {
-                    LOGGER.warn( "error encoding stored pw value: " + e.getMessage() );
+                    try
+                    {
+                        final String encodedValue = StoredValueEncoder.encode(
+                                webAction.getPassword(),
+                                xmlOutputProcessData.getStoredValueEncoderMode(),
+                                xmlOutputProcessData.getPwmSecurityKey() );
+                        clonedWebActions.add( webAction.toBuilder()
+                                .password( encodedValue )
+                                .build() );
+                    }
+                    catch ( final PwmOperationalException e )
+                    {
+                        LOGGER.warn( "error encoding stored pw value: " + e.getMessage() );
+                    }
                 }
             }
 
@@ -225,7 +253,7 @@ public class ActionValue extends AbstractValue implements StoredValue
             {
                 loopConfig.validate();
             }
-            catch ( PwmOperationalException e )
+            catch ( final PwmOperationalException e )
             {
                 return Collections.singletonList( "format error: " + e.getErrorInformation().toDebugStr() );
             }
@@ -367,14 +395,14 @@ public class ActionValue extends AbstractValue implements StoredValue
 
     private static int figureCurrentStoredSyntax( final XmlElement settingElement )
     {
-        final String storedSyntaxVersionString = settingElement.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_SYNTAX_VERSION );
+        final String storedSyntaxVersionString = settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION );
         if ( !StringUtil.isEmpty( storedSyntaxVersionString ) )
         {
             try
             {
                 return Integer.parseInt( storedSyntaxVersionString );
             }
-            catch ( NumberFormatException e )
+            catch ( final NumberFormatException e )
             {
                 LOGGER.debug( () -> "unable to parse syntax version for setting " + e.getMessage() );
             }
@@ -382,7 +410,7 @@ public class ActionValue extends AbstractValue implements StoredValue
         return 0;
     }
 
-    private static ActionConfiguration convertOldVersion1Values( final ActionConfigurationOldVersion1 oldAction )
+    private static ActionConfiguration convertOldVersion1Values( final ActionConfiguration.ActionConfigurationOldVersion1 oldAction )
     {
         final ActionConfiguration.ActionConfigurationBuilder builder = ActionConfiguration.builder();
         builder.name( oldAction.getName() );

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

@@ -23,7 +23,8 @@ package password.pwm.config.value;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.i18n.Display;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
@@ -34,10 +35,11 @@ import java.io.Serializable;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 
 public class BooleanValue implements StoredValue
 {
-    boolean value;
+    private final boolean value;
 
     public BooleanValue( final boolean value )
     {
@@ -56,9 +58,13 @@ public class BooleanValue implements StoredValue
 
             public BooleanValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             {
-                final XmlElement valueElement = settingElement.getChild( "value" );
-                final String value = valueElement.getText();
-                return new BooleanValue( Boolean.valueOf( value ) );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                if ( valueElement.isPresent() )
+                {
+                    final String value = valueElement.get().getTextTrim();
+                    return new BooleanValue( Boolean.valueOf( value ) );
+                }
+                return new BooleanValue( false );
             }
 
         };
@@ -70,7 +76,7 @@ public class BooleanValue implements StoredValue
     }
 
     @Override
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         valueElement.addText( String.valueOf( value ) );
@@ -99,12 +105,6 @@ public class BooleanValue implements StoredValue
         return value;
     }
 
-    @Override
-    public boolean requiresStoredUpdate( )
-    {
-        return false;
-    }
-
     @Override
     public int currentSyntaxVersion( )
     {
@@ -112,7 +112,7 @@ public class BooleanValue implements StoredValue
     }
 
     @Override
-    public String valueHash( ) throws PwmUnrecoverableException
+    public String valueHash()
     {
         return value ? "1" : "0";
     }

+ 12 - 6
server/src/main/java/password/pwm/config/value/ChallengeValue.java

@@ -23,6 +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.XmlOutputProcessData;
 import password.pwm.config.value.data.ChallengeItemConfiguration;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
@@ -43,11 +44,11 @@ public class ChallengeValue extends AbstractValue implements StoredValue
     private static final PwmLogger LOGGER = PwmLogger.forClass( ChallengeValue.class );
 
     //locale str as key.
-    final Map<String, List<ChallengeItemConfiguration>> values;
+    private final Map<String, List<ChallengeItemConfiguration>> values;
 
     ChallengeValue( final Map<String, List<ChallengeItemConfiguration>> values )
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
 
     public static StoredValueFactory factory( )
@@ -111,7 +112,7 @@ public class ChallengeValue extends AbstractValue implements StoredValue
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Map.Entry<String, List<ChallengeItemConfiguration>> entry : values.entrySet() )
@@ -207,7 +208,7 @@ public class ChallengeValue extends AbstractValue implements StoredValue
             {
                 minLength = Integer.parseInt( s1[ 1 ] );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.debug( () -> "unexpected error parsing config input '" + inputString + "' " + e.getMessage() );
             }
@@ -218,7 +219,7 @@ public class ChallengeValue extends AbstractValue implements StoredValue
             {
                 maxLength = Integer.parseInt( s1[ 2 ] );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.debug( () -> "unexpected error parsing config input '" + inputString + "' " + e.getMessage() );
             }
@@ -231,7 +232,12 @@ public class ChallengeValue extends AbstractValue implements StoredValue
             adminDefined = false;
         }
 
-        return new ChallengeItemConfiguration( challengeText, minLength, maxLength, adminDefined );
+        return ChallengeItemConfiguration.builder()
+                .text( challengeText )
+                .minLength( minLength )
+                .maxLength( maxLength )
+                .adminDefined( adminDefined )
+                .build();
     }
 
     public String toDebugString( final Locale locale )

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

@@ -21,7 +21,8 @@
 package password.pwm.config.value;
 
 import com.google.gson.reflect.TypeToken;
-import password.pwm.config.CustomLinkConfiguration;
+import password.pwm.config.stored.XmlOutputProcessData;
+import password.pwm.config.value.data.CustomLinkConfiguration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.error.PwmOperationalException;
@@ -39,11 +40,11 @@ import java.util.Set;
 
 public class CustomLinkValue extends AbstractValue implements StoredValue
 {
-    final List<CustomLinkConfiguration> values;
+    private final List<CustomLinkConfiguration> values;
 
     public CustomLinkValue( final List<CustomLinkConfiguration> values )
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
     }
 
     public static StoredValueFactory factory( )
@@ -88,7 +89,7 @@ public class CustomLinkValue extends AbstractValue implements StoredValue
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final CustomLinkConfiguration value : values )

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

@@ -25,6 +25,7 @@ import com.google.gson.reflect.TypeToken;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
@@ -42,11 +43,11 @@ import java.util.TreeMap;
 public class EmailValue extends AbstractValue implements StoredValue
 {
     //key is locale identifier
-    final Map<String, EmailItemBean> values;
+    private final Map<String, EmailItemBean> values;
 
     EmailValue( final Map<String, EmailItemBean> values )
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
 
     public static StoredValueFactory factory( )
@@ -99,7 +100,7 @@ public class EmailValue extends AbstractValue implements StoredValue
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Map.Entry<String, EmailItemBean> entry : values.entrySet() )

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

@@ -20,11 +20,12 @@
 
 package password.pwm.config.value;
 
+import lombok.Builder;
 import lombok.Value;
-import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
-import password.pwm.error.PwmOperationalException;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.util.java.JsonUtil;
@@ -45,36 +46,23 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 
 public class FileValue extends AbstractValue implements StoredValue
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( FileValue.class );
 
-    private Map<FileInformation, FileContent> values = new LinkedHashMap<>();
+    private static final int ENCODING_LINE_LENGTH = 120;
+    private static final String XML_ELEMENT_FILE_INFORMATION = "FileInformation";
+    private static final String XML_ELEMENT_FILE_CONTENT = "FileContent";
 
+    private final Map<FileInformation, FileContent> values;
+
+    @Value
     public static class FileInformation implements Serializable
     {
         private String filename;
         private String filetype;
-
-        public FileInformation(
-                final String filename,
-                final String filetype
-        )
-        {
-            this.filename = filename;
-            this.filetype = filetype;
-        }
-
-        public String getFilename( )
-        {
-            return filename;
-        }
-
-        public String getFiletype( )
-        {
-            return filetype;
-        }
     }
 
     @Value
@@ -82,30 +70,24 @@ public class FileValue extends AbstractValue implements StoredValue
     {
         private ImmutableByteArray contents;
 
-
-        public static FileContent fromEncodedString( final String input )
+        static FileContent fromEncodedString( final String input )
                 throws IOException
         {
-            final byte[] convertedBytes = StringUtil.base64Decode( input );
+            final String whitespaceStrippedInput = StringUtil.stripAllWhitespace( input );
+            final byte[] convertedBytes = StringUtil.base64Decode( whitespaceStrippedInput );
             return new FileContent( ImmutableByteArray.of( convertedBytes ) );
         }
 
-        public String toEncodedString( )
+        String toEncodedString( )
                 throws IOException
         {
             return StringUtil.base64Encode( contents.copyOf(), StringUtil.Base64Options.GZIP );
         }
 
-        public String md5sum( )
-                throws PwmUnrecoverableException
-        {
-            return SecureEngine.hash( new ByteArrayInputStream( contents.copyOf() ), PwmHashAlgorithm.MD5 );
-        }
-
-        public String sha1sum( )
+        String sha512sum( )
                 throws PwmUnrecoverableException
         {
-            return SecureEngine.hash( new ByteArrayInputStream( contents.copyOf() ), PwmHashAlgorithm.SHA1 );
+            return SecureEngine.hash( new ByteArrayInputStream( contents.copyOf() ), PwmHashAlgorithm.SHA512 );
         }
 
         public int size( )
@@ -116,7 +98,7 @@ public class FileValue extends AbstractValue implements StoredValue
 
     public FileValue( final Map<FileInformation, FileContent> values )
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
 
     public static StoredValueFactory factory( )
@@ -125,30 +107,29 @@ public class FileValue extends AbstractValue implements StoredValue
         {
 
             public FileValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
-                    throws PwmOperationalException
             {
-                final List<XmlElement> valueElements = settingElement.getChildren( "value" );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final Map<FileInformation, FileContent> values = new LinkedHashMap<>();
                 for ( final XmlElement loopValueElement : valueElements )
                 {
-                    final XmlElement loopFileInformation = loopValueElement.getChild( "FileInformation" );
-                    if ( loopFileInformation != null )
+                    final Optional<XmlElement> loopFileInformation = loopValueElement.getChild( XML_ELEMENT_FILE_INFORMATION );
+                    if ( loopFileInformation.isPresent() )
                     {
-                        final String loopFileInformationJson = loopFileInformation.getText();
+                        final String loopFileInformationJson = loopFileInformation.get().getText();
                         final FileInformation fileInformation = JsonUtil.deserialize( loopFileInformationJson,
                                 FileInformation.class );
 
-                        final XmlElement loopFileContentElement = loopValueElement.getChild( "FileContent" );
-                        if ( loopFileContentElement != null )
+                        final Optional<XmlElement> loopFileContentElement = loopValueElement.getChild( XML_ELEMENT_FILE_CONTENT );
+                        if ( loopFileContentElement.isPresent() )
                         {
-                            final String fileContentString = loopFileContentElement.getText();
+                            final String fileContentString = loopFileContentElement.get().getText();
                             final FileContent fileContent;
                             try
                             {
                                 fileContent = FileContent.fromEncodedString( fileContentString );
                                 values.put( fileInformation, fileContent );
                             }
-                            catch ( IOException e )
+                            catch ( final IOException e )
                             {
                                 LOGGER.error( "error reading file contents item: " + e.getMessage(), e );
                             }
@@ -165,7 +146,7 @@ public class FileValue extends AbstractValue implements StoredValue
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Map.Entry<FileInformation, FileContent> entry : this.values.entrySet() )
@@ -174,16 +155,19 @@ public class FileValue extends AbstractValue implements StoredValue
             final FileContent fileContent = entry.getValue();
             final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
 
-            final XmlElement fileInformationElement = XmlFactory.getFactory().newElement( "FileInformation" );
+            final XmlElement fileInformationElement = XmlFactory.getFactory().newElement( XML_ELEMENT_FILE_INFORMATION );
             fileInformationElement.addText( JsonUtil.serialize( fileInformation ) );
             valueElement.addContent( fileInformationElement );
 
-            final XmlElement fileContentElement = XmlFactory.getFactory().newElement( "FileContent" );
+            final XmlElement fileContentElement = XmlFactory.getFactory().newElement( XML_ELEMENT_FILE_CONTENT );
+
             try
             {
-                fileContentElement.addText( fileContent.toEncodedString() );
+                final String encodedLineBreaks = StringUtil.insertRepeatedLineBreaks(
+                        fileContent.toEncodedString(), ENCODING_LINE_LENGTH );
+                fileContentElement.addText( encodedLineBreaks );
             }
-            catch ( IOException e )
+            catch ( final IOException e )
             {
                 LOGGER.error( "unexpected error writing setting to xml, IO error during base64 encoding: " + e.getMessage() );
             }
@@ -221,7 +205,7 @@ public class FileValue extends AbstractValue implements StoredValue
         return ( Serializable ) asMetaData();
     }
 
-    public List<Map<String, Object>> asMetaData( )
+    List<Map<String, Object>> asMetaData( )
     {
         final List<Map<String, Object>> output = new ArrayList<>();
         for ( final Map.Entry<FileInformation, FileContent> entry : this.values.entrySet() )
@@ -234,9 +218,9 @@ public class FileValue extends AbstractValue implements StoredValue
             details.put( "size", fileContent.size() );
             try
             {
-                details.put( "md5sum", fileContent.md5sum() );
+                details.put( "sha512sum", fileContent.sha512sum() );
             }
-            catch ( PwmUnrecoverableException e )
+            catch ( final PwmUnrecoverableException e )
             {
                 LOGGER.trace( () -> "error generating file hash" );
             }
@@ -256,65 +240,30 @@ public class FileValue extends AbstractValue implements StoredValue
         {
             final FileValue.FileInformation fileInformation = entry.getKey();
             final FileContent fileContent = entry.getValue();
-            final FileInfo loopInfo = new FileInfo();
-            loopInfo.name = fileInformation.getFilename();
-            loopInfo.type = fileInformation.getFiletype();
-            loopInfo.size = fileContent.size();
             try
             {
-                loopInfo.md5sum = fileContent.md5sum();
-                loopInfo.sha1sum = fileContent.sha1sum();
+                returnObj.add( FileInfo.builder()
+                        .name( fileInformation.getFilename() )
+                        .type( fileInformation.getFiletype() )
+                        .size( fileContent.size() )
+                        .sha512sum( fileContent.sha512sum() )
+                        .build() );
             }
-            catch ( PwmUnrecoverableException e )
+            catch ( final PwmUnrecoverableException e )
             {
-                LOGGER.warn( "error generating hash for certificate: " + e.getMessage() );
+                throw new IllegalStateException( e );
             }
-            returnObj.add( loopInfo );
         }
         return Collections.unmodifiableList( returnObj );
     }
 
-    @Override
-    public String valueHash( ) throws PwmUnrecoverableException
-    {
-        return SecureEngine.hash( JsonUtil.serializeCollection( toInfoMap() ), PwmConstants.SETTING_CHECKSUM_HASH_METHOD );
-    }
-
+    @Value
+    @Builder
     public static class FileInfo implements Serializable
     {
-        public String name;
-        public String type;
-        public int size;
-        public String md5sum;
-        public String sha1sum;
-
-        private FileInfo( )
-        {
-        }
-
-        public String getName( )
-        {
-            return name;
-        }
-
-        public String getType( )
-        {
-            return type;
-        }
-
-        public int getSize( )
-        {
-            return size;
-        }
-
-        public String getMd5sum( )
-        {
-            return md5sum;
-        }
-
-        public String getSha1sum( )
-        {
-            return sha1sum;
-        }
+        private String name;
+        private String type;
+        private long size;
+        private String sha512sum;
     }
 }

+ 7 - 14
server/src/main/java/password/pwm/config/value/FormValue.java

@@ -24,6 +24,7 @@ import com.google.gson.reflect.TypeToken;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.util.java.JavaHelper;
@@ -42,13 +43,11 @@ import java.util.Set;
 
 public class FormValue extends AbstractValue implements StoredValue
 {
-    final List<FormConfiguration> values;
-
-    private boolean needsXmlUpdate;
+    private final List<FormConfiguration> values;
 
     public FormValue( final List<FormConfiguration> values )
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
     }
 
     public static StoredValueFactory factory( )
@@ -59,14 +58,14 @@ public class FormValue extends AbstractValue implements StoredValue
             {
                 if ( input == null )
                 {
-                    return new FormValue( Collections.<FormConfiguration>emptyList() );
+                    return new FormValue( Collections.emptyList() );
                 }
                 else
                 {
                     List<FormConfiguration> srcList = JsonUtil.deserialize( input, new TypeToken<List<FormConfiguration>>()
                     {
                     } );
-                    srcList = srcList == null ? Collections.<FormConfiguration>emptyList() : srcList;
+                    srcList = srcList == null ? Collections.emptyList() : srcList;
                     while ( srcList.contains( null ) )
                     {
                         srcList.remove( null );
@@ -98,13 +97,12 @@ public class FormValue extends AbstractValue implements StoredValue
                     }
                 }
                 final FormValue formValue = new FormValue( values );
-                formValue.needsXmlUpdate = oldType;
                 return formValue;
             }
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final FormConfiguration value : values )
@@ -147,7 +145,7 @@ public class FormValue extends AbstractValue implements StoredValue
             {
                 loopConfig.validate();
             }
-            catch ( PwmOperationalException e )
+            catch ( final PwmOperationalException e )
             {
                 return Collections.singletonList( "format error: " + e.getErrorInformation().toDebugStr() );
             }
@@ -156,11 +154,6 @@ public class FormValue extends AbstractValue implements StoredValue
         return Collections.emptyList();
     }
 
-    public boolean isNeedsXmlUpdate( )
-    {
-        return needsXmlUpdate;
-    }
-
     public String toDebugString( final Locale locale )
     {
         if ( values != null && !values.isEmpty() )

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

@@ -23,6 +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.XmlOutputProcessData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
@@ -40,11 +41,11 @@ import java.util.regex.Pattern;
 
 public class LocalizedStringArrayValue extends AbstractValue implements StoredValue
 {
-    final Map<String, List<String>> values;
+    private final Map<String, List<String>> values;
 
     LocalizedStringArrayValue( final Map<String, List<String>> values )
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
 
     public static StoredValueFactory factory( )
@@ -89,7 +90,7 @@ public class LocalizedStringArrayValue extends AbstractValue implements StoredVa
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Map.Entry<String, List<String>> entry : values.entrySet() )

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

@@ -23,6 +23,8 @@ 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.XmlOutputProcessData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
@@ -40,11 +42,11 @@ import java.util.regex.Pattern;
 
 public class LocalizedStringValue extends AbstractValue implements StoredValue
 {
-    final Map<String, String> value;
+    private final Map<String, String> value;
 
     public LocalizedStringValue( final Map<String, String> values )
     {
-        this.value = Collections.unmodifiableMap( values );
+        this.value = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
 
     public static StoredValueFactory factory( )
@@ -69,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( "value" );
+                final List<XmlElement> elements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final Map<String, String> values = new TreeMap<>();
                 for ( final XmlElement loopValueElement : elements )
                 {
-                    final String localeString = loopValueElement.getAttributeValue( "locale" );
+                    final String localeString = loopValueElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE );
                     final String value = loopValueElement.getText();
                     values.put( localeString == null ? "" : localeString, value );
                 }
@@ -82,7 +84,7 @@ public class LocalizedStringValue extends AbstractValue implements StoredValue
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Map.Entry<String, String> entry : value.entrySet() )

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

@@ -24,6 +24,8 @@ 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.XmlOutputProcessData;
 import password.pwm.config.value.data.NamedSecretData;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -31,6 +33,7 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
@@ -45,23 +48,27 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 
 public class NamedSecretValue implements StoredValue
 {
+
+    private final transient LazySupplier<String> valueHashSupplier = new LazySupplier<>( () -> AbstractValue.valueHashComputer( NamedSecretValue.this ) );
+
     private static final String ELEMENT_NAME = "name";
     private static final String ELEMENT_PASSWORD = "password";
     private static final String ELEMENT_USAGE = "usage";
 
-    private Map<String, NamedSecretData> values;
+    private final Map<String, NamedSecretData> values;
 
     NamedSecretValue( )
     {
+        values = Collections.emptyMap();
     }
 
-
     public NamedSecretValue( final Map<String, NamedSecretData> values )
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
 
     public static StoredValue.StoredValueFactory factory( )
@@ -78,7 +85,7 @@ public class NamedSecretValue implements StoredValue
                     final Map<String, NamedSecretData> linkedValues = new LinkedHashMap<>( values );
                     return new NamedSecretValue( linkedValues );
                 }
-                catch ( Exception e )
+                catch ( final Exception e )
                 {
                     throw new IllegalStateException(
                             "NamedPasswordValue can not be json de-serialized: " + e.getMessage() );
@@ -93,34 +100,33 @@ public class NamedSecretValue implements StoredValue
                     throws PwmOperationalException, PwmUnrecoverableException
             {
                 final Map<String, NamedSecretData> values = new LinkedHashMap<>();
-                final List<XmlElement> valueElements = settingElement.getChildren( "value" );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
 
                 try
                 {
-                    if ( valueElements != null )
+                    for ( final XmlElement value : valueElements )
                     {
-                        for ( final XmlElement value : valueElements )
+                        final Optional<XmlElement> nameElement = value.getChild( ELEMENT_NAME );
+                        final Optional<XmlElement> passwordElement = value.getChild( ELEMENT_PASSWORD );
+                        if ( nameElement.isPresent() && passwordElement.isPresent() )
                         {
-                            if ( value.getChild( ELEMENT_NAME ) != null && value.getChild( ELEMENT_PASSWORD ) != null )
+                            final String name = nameElement.get().getText();
+                            final String encodedValue = passwordElement.get().getText();
+                            final PasswordData passwordData = new PasswordData( SecureEngine.decryptStringValue( encodedValue, key, PwmBlockAlgorithm.CONFIG ) );
+                            final List<XmlElement> usages = value.getChildren( ELEMENT_USAGE );
+                            final List<String> strUsages = new ArrayList<>();
+                            if ( usages != null )
                             {
-                                final String name = value.getChild( ELEMENT_NAME ).getText();
-                                final String encodedValue = value.getChild( ELEMENT_PASSWORD ).getText();
-                                final PasswordData passwordData = new PasswordData( SecureEngine.decryptStringValue( encodedValue, key, PwmBlockAlgorithm.CONFIG ) );
-                                final List<XmlElement> usages = value.getChildren( ELEMENT_USAGE );
-                                final List<String> strUsages = new ArrayList<>();
-                                if ( usages != null )
+                                for ( final XmlElement usageElement : usages )
                                 {
-                                    for ( final XmlElement usageElement : usages )
-                                    {
-                                        strUsages.add( usageElement.getText() );
-                                    }
+                                    strUsages.add( usageElement.getText() );
                                 }
-                                values.put( name, new NamedSecretData( passwordData, Collections.unmodifiableList( strUsages ) ) );
                             }
+                            values.put( name, new NamedSecretData( passwordData, Collections.unmodifiableList( strUsages ) ) );
                         }
                     }
                 }
-                catch ( Exception e )
+                catch ( final Exception e )
                 {
                     final String errorMsg = "unable to decode encrypted password value for setting: " + e.getMessage();
                     final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
@@ -154,7 +160,7 @@ public class NamedSecretValue implements StoredValue
         return 0;
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey key )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         if ( values == null )
         {
@@ -168,7 +174,7 @@ public class NamedSecretValue implements StoredValue
             {
                 final String name = entry.getKey();
                 final PasswordData passwordData = entry.getValue().getPassword();
-                final String encodedValue = SecureEngine.encryptToString( passwordData.getStringValue(), key, PwmBlockAlgorithm.CONFIG );
+                final String encodedValue = SecureEngine.encryptToString( passwordData.getStringValue(), xmlOutputProcessData.getPwmSecurityKey(), PwmBlockAlgorithm.CONFIG );
                 final XmlElement newValueElement = XmlFactory.getFactory().newElement( "value" );
                 final XmlElement nameElement = XmlFactory.getFactory().newElement( ELEMENT_NAME );
                 nameElement.addText( name );
@@ -189,7 +195,7 @@ public class NamedSecretValue implements StoredValue
                 valuesElement.add( newValueElement );
             }
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
         }
@@ -239,21 +245,15 @@ public class NamedSecretValue implements StoredValue
             }
             return copiedValues;
         }
-        catch ( PwmUnrecoverableException e )
+        catch ( final PwmUnrecoverableException e )
         {
             throw new IllegalStateException( e.getErrorInformation().toDebugStr() );
         }
     }
 
-    public boolean requiresStoredUpdate( )
-    {
-        return false;
-    }
-
     @Override
-    public String valueHash( ) throws PwmUnrecoverableException
+    public String valueHash()
     {
-        return values == null ? "" : SecureEngine.hash( JsonUtil.serializeMap( values ), PwmConstants.SETTING_CHECKSUM_HASH_METHOD );
+        return valueHashSupplier.get();
     }
-
 }

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

@@ -22,6 +22,7 @@ package password.pwm.config.value;
 
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
@@ -38,11 +39,11 @@ import java.util.stream.Collectors;
 
 public class NumericArrayValue extends AbstractValue implements StoredValue
 {
-    List<Long> values;
+    private final List<Long> values;
 
     public NumericArrayValue( final List<Long> values )
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
     }
 
     public static StoredValueFactory factory( )
@@ -72,7 +73,7 @@ public class NumericArrayValue extends AbstractValue implements StoredValue
     }
 
     @Override
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Long value : this.values )

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

@@ -23,6 +23,8 @@ 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.XmlOutputProcessData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
@@ -30,10 +32,11 @@ import password.pwm.util.secure.PwmSecurityKey;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 public class NumericValue extends AbstractValue implements StoredValue
 {
-    long value;
+    private final long value;
 
     public NumericValue( final long value )
     {
@@ -51,9 +54,16 @@ public class NumericValue extends AbstractValue implements StoredValue
 
             public NumericValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             {
-                final XmlElement valueElement = settingElement.getChild( "value" );
-                final String value = valueElement.getText();
-                return new NumericValue( normalizeValue( pwmSetting, Long.parseLong( value ) ) );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                if ( valueElement.isPresent() )
+                {
+                    final String value = valueElement.get().getText();
+                    return new NumericValue( normalizeValue( pwmSetting, Long.parseLong( value ) ) );
+                }
+                else
+                {
+                    return new NumericValue( 0 );
+                }
             }
         };
     }
@@ -77,7 +87,7 @@ public class NumericValue extends AbstractValue implements StoredValue
     }
 
     @Override
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         valueElement.addText( Long.toString( value ) );

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

@@ -23,6 +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.XmlOutputProcessData;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
@@ -39,11 +40,11 @@ import java.util.TreeSet;
 
 public class OptionListValue extends AbstractValue implements StoredValue
 {
-    final Set<String> values;
+    private final Set<String> values;
 
     public OptionListValue( final Set<String> values )
     {
-        this.values = new TreeSet( values );
+        this.values = values == null ? Collections.emptySet() : Collections.unmodifiableSet( new TreeSet<>( values ) );
     }
 
     public static StoredValueFactory factory( )
@@ -88,7 +89,7 @@ public class OptionListValue extends AbstractValue implements StoredValue
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final String value : values )

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

@@ -24,33 +24,36 @@ 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.XmlOutputProcessData;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
-import password.pwm.util.secure.PwmBlockAlgorithm;
 import password.pwm.util.secure.PwmSecurityKey;
-import password.pwm.util.secure.SecureEngine;
 
 import java.io.Serializable;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 
 public class PasswordValue implements StoredValue
 {
-    private PasswordData value;
+    private final transient LazySupplier<String> valueHashSupplier = new LazySupplier<>( () -> AbstractValue.valueHashComputer( PasswordValue.this ) );
+
+    private final PasswordData value;
 
     PasswordValue( )
     {
+        value = null;
     }
 
-    boolean requiresStoredUpdate;
-
     public PasswordValue( final PasswordData passwordData )
     {
         value = passwordData;
@@ -69,7 +72,7 @@ public class PasswordValue implements StoredValue
                     {
                         return new PasswordValue( new PasswordData( strValue ) );
                     }
-                    catch ( PwmUnrecoverableException e )
+                    catch ( final PwmUnrecoverableException e )
                     {
                         throw new IllegalStateException(
                                 "PasswordValue can not be json de-serialized: " + e.getMessage() );
@@ -85,50 +88,54 @@ public class PasswordValue implements StoredValue
             )
                     throws PwmOperationalException, PwmUnrecoverableException
             {
-                final XmlElement valueElement = settingElement.getChild( "value" );
-                final String rawValue = valueElement.getText();
-
-                final PasswordValue newPasswordValue = new PasswordValue();
-                if ( rawValue == null || rawValue.isEmpty() )
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                if ( valueElement.isPresent() )
                 {
-                    return newPasswordValue;
-                }
-
-                final boolean plainTextSetting;
-                {
-                    final String plainTextAttributeStr = valueElement.getAttributeValue( "plaintext" );
-                    plainTextSetting = plainTextAttributeStr != null && Boolean.parseBoolean( plainTextAttributeStr );
-                }
+                    final String rawValue = valueElement.get().getText();
 
-                if ( plainTextSetting )
-                {
-                    newPasswordValue.value = new PasswordData( rawValue );
-                    newPasswordValue.requiresStoredUpdate = true;
-                }
-                else
-                {
-                    try
+                    final PasswordValue newPasswordValue = new PasswordValue();
+                    if ( rawValue == null || rawValue.isEmpty() )
                     {
-                        newPasswordValue.value = new PasswordData( SecureEngine.decryptStringValue( rawValue, key, PwmBlockAlgorithm.CONFIG ) );
                         return newPasswordValue;
                     }
-                    catch ( Exception e )
+
+                    final boolean plainTextSetting;
                     {
-                        final String errorMsg = "unable to decode encrypted password value for setting: " + e.getMessage();
-                        final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
-                        throw new PwmOperationalException( errorInfo );
+                        final String plainTextAttributeStr = valueElement.get().getAttributeValue( "plaintext" );
+                        plainTextSetting = plainTextAttributeStr != null && Boolean.parseBoolean( plainTextAttributeStr );
+                    }
+
+                    if ( plainTextSetting )
+                    {
+                        return new PasswordValue( new PasswordData( rawValue ) );
+                    }
+                    else
+                    {
+                        try
+                        {
+                            final Optional<String> encodedValue = StoredValueEncoder.decode( rawValue, StoredValueEncoder.Mode.CONFIG_PW, key );
+                            if ( encodedValue.isPresent() )
+                            {
+                                return new PasswordValue( new PasswordData( encodedValue.get() ) );
+                            }
+                            else
+                            {
+                                return new PasswordValue( new PasswordData( "" ) );
+                            }
+                        }
+                        catch ( final Exception e )
+                        {
+                            final String errorMsg = "unable to decode encrypted password value for setting: " + e.getMessage();
+                            final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                            throw new PwmOperationalException( errorInfo );
+                        }
                     }
                 }
-                return newPasswordValue;
+                return new PasswordValue();
             }
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName )
-    {
-        throw new IllegalStateException( "password xml output requires hash key" );
-    }
-
     @Override
     public Object toNativeObject( )
     {
@@ -147,7 +154,7 @@ public class PasswordValue implements StoredValue
         return 0;
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey key )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         if ( value == null )
         {
@@ -157,10 +164,14 @@ public class PasswordValue implements StoredValue
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         try
         {
-            final String encodedValue = SecureEngine.encryptToString( value.getStringValue(), key, PwmBlockAlgorithm.CONFIG );
+            final String encodedValue = StoredValueEncoder.encode(
+                    value.getStringValue(),
+                    xmlOutputProcessData.getStoredValueEncoderMode(),
+                    xmlOutputProcessData.getPwmSecurityKey() );
+
             valueElement.addText( encodedValue );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
         }
@@ -184,14 +195,9 @@ public class PasswordValue implements StoredValue
         return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
     }
 
-    public boolean requiresStoredUpdate( )
-    {
-        return requiresStoredUpdate;
-    }
-
     @Override
-    public String valueHash( ) throws PwmUnrecoverableException
+    public String valueHash()
     {
-        return value == null ? "" : SecureEngine.hash( JsonUtil.serialize( value.getStringValue() ), PwmConstants.SETTING_CHECKSUM_HASH_METHOD );
+        return valueHashSupplier.get();
     }
 }

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

@@ -23,14 +23,14 @@ 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.XmlOutputProcessData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmBlockAlgorithm;
 import password.pwm.util.secure.PwmSecurityKey;
-import password.pwm.util.secure.SecureEngine;
 import password.pwm.util.secure.X509Utils;
 
 import java.io.Serializable;
@@ -44,6 +44,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 
 public class PrivateKeyValue extends AbstractValue
 {
@@ -52,7 +53,7 @@ public class PrivateKeyValue extends AbstractValue
     private static final String ELEMENT_NAME_CERTIFICATE = "certificate";
     private static final String ELEMENT_NAME_KEY = "key";
 
-    private PrivateKeyCertificate privateKeyCertificate;
+    private final PrivateKeyCertificate privateKeyCertificate;
 
     public static StoredValue.StoredValueFactory factory( )
     {
@@ -60,14 +61,14 @@ public class PrivateKeyValue extends AbstractValue
         {
             public PrivateKeyValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
-                if ( settingElement != null && settingElement.getChild( "value" ) != null )
+                if ( settingElement != null && settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE  ).isPresent() )
                 {
 
-                    final XmlElement valueElement = settingElement.getChild( "value" );
-                    if ( valueElement != null )
+                    final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                    if ( valueElement.isPresent() )
                     {
                         final List<X509Certificate> certificates = new ArrayList<>();
-                        for ( final XmlElement certificateElement : valueElement.getChildren( ELEMENT_NAME_CERTIFICATE ) )
+                        for ( final XmlElement certificateElement : valueElement.get().getChildren( ELEMENT_NAME_CERTIFICATE ) )
                         {
                             try
                             {
@@ -75,7 +76,7 @@ public class PrivateKeyValue extends AbstractValue
                                 final X509Certificate cert = X509Utils.certificateFromBase64( b64Text );
                                 certificates.add( cert );
                             }
-                            catch ( Exception e )
+                            catch ( final Exception e )
                             {
                                 LOGGER.error( "error reading certificate: " + e.getMessage(), e );
                             }
@@ -86,15 +87,25 @@ public class PrivateKeyValue extends AbstractValue
                         PrivateKey privateKey = null;
                         try
                         {
-                            final XmlElement keyElement = valueElement.getChild( ELEMENT_NAME_KEY );
-                            final String encryptedText = keyElement.getText();
-                            final String decryptedText = SecureEngine.decryptStringValue( encryptedText, key, PwmBlockAlgorithm.CONFIG );
-                            final byte[] privateKeyBytes = StringUtil.base64Decode( decryptedText );
-                            privateKey = KeyFactory.getInstance( "RSA" ).generatePrivate( new PKCS8EncodedKeySpec( privateKeyBytes ) );
+                            final Optional<XmlElement> keyElement = valueElement.get().getChild( ELEMENT_NAME_KEY );
+                            if ( keyElement.isPresent() )
+                            {
+                                final String encryptedText = keyElement.get().getText();
+                                final Optional<String> decryptedText = StoredValueEncoder.decode( encryptedText, StoredValueEncoder.Mode.CONFIG_PW, key );
+                                if ( decryptedText.isPresent() )
+                                {
+                                    final byte[] privateKeyBytes = StringUtil.base64Decode( decryptedText.get() );
+                                    privateKey = KeyFactory.getInstance( "RSA" ).generatePrivate( new PKCS8EncodedKeySpec( privateKeyBytes ) );
+                                }
+                            }
+                            else
+                            {
+                                LOGGER.error( "error reading privateKey for setting: '" + pwmSetting.getKey() + "': missing 'value' element" );
+                            }
                         }
-                        catch ( Exception e )
+                        catch ( final Exception e )
                         {
-                            LOGGER.error( "error reading privateKey: " + e.getMessage(), e );
+                            LOGGER.error( "error reading privateKey for setting: '" + pwmSetting.getKey() + "': " + e.getMessage(), e );
                         }
 
                         if ( !certificates.isEmpty() && privateKey != null )
@@ -143,9 +154,9 @@ public class PrivateKeyValue extends AbstractValue
         return 0;
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey key )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
-        final XmlElement valueElement = XmlFactory.getFactory().newElement( "value" );
+        final XmlElement valueElement = XmlFactory.getFactory().newElement( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
         if ( privateKeyCertificate != null )
         {
             try
@@ -161,12 +172,16 @@ public class PrivateKeyValue extends AbstractValue
                 {
                     final XmlElement keyElement = XmlFactory.getFactory().newElement( ELEMENT_NAME_KEY );
                     final String b64EncodedKey = StringUtil.base64Encode( privateKeyCertificate.getKey().getEncoded() );
-                    final String encryptedKey = SecureEngine.encryptToString( b64EncodedKey, key, PwmBlockAlgorithm.CONFIG );
+                    final String encryptedKey = StoredValueEncoder.encode(
+                            b64EncodedKey,
+                            xmlOutputProcessData.getStoredValueEncoderMode(),
+                            xmlOutputProcessData.getPwmSecurityKey() );
+
                     keyElement.addText( encryptedKey );
                     valueElement.addContent( keyElement );
                 }
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
             }

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

@@ -24,6 +24,8 @@ 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.XmlOutputProcessData;
 import password.pwm.config.value.data.RemoteWebServiceConfiguration;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.util.java.JsonUtil;
@@ -43,6 +45,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 public class RemoteWebServiceValue extends AbstractValue implements StoredValue
@@ -74,7 +77,7 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
                             }
                     );
 
-                    srcList = srcList == null ? Collections.emptyList() : srcList;
+                    srcList = srcList == null ? new ArrayList<>() : srcList;
                     srcList.removeIf( Objects::isNull );
                     return new RemoteWebServiceValue( Collections.unmodifiableList( srcList ) );
                 }
@@ -87,7 +90,7 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
             )
                     throws PwmOperationalException
             {
-                final List<XmlElement> valueElements = settingElement.getChildren( "value" );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final List<RemoteWebServiceConfiguration> values = new ArrayList<>();
                 for ( final XmlElement loopValueElement : valueElements )
                 {
@@ -95,8 +98,15 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
                     if ( value != null && value.length() > 0 )
                     {
                         final RemoteWebServiceConfiguration parsedValue = JsonUtil.deserialize( value, RemoteWebServiceConfiguration.class );
-                        parsedValue.setPassword( decryptPwValue( parsedValue.getPassword(), pwmSecurityKey ) );
-                        values.add( parsedValue );
+                        final Optional<String> decodedValue = StoredValueEncoder.decode(
+                                parsedValue.getPassword(),
+                                StoredValueEncoder.Mode.ENCODED,
+                                pwmSecurityKey
+                        );
+                        decodedValue.ifPresent( ( s ) ->
+                        {
+                            values.add( parsedValue.toBuilder().password( s ).build() );
+                        } );
                     }
                 }
                 return new RemoteWebServiceValue( values );
@@ -104,22 +114,27 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final RemoteWebServiceConfiguration value : values )
         {
             final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
-            final RemoteWebServiceConfiguration clonedValue = JsonUtil.cloneUsingJson( value, RemoteWebServiceConfiguration.class );
+
+            String encodedValue = value.getPassword();
             try
             {
-                clonedValue.setPassword( encryptPwValue( clonedValue.getPassword(), pwmSecurityKey ) );
+                encodedValue = StoredValueEncoder.encode(
+                        value.getPassword(),
+                        xmlOutputProcessData.getStoredValueEncoderMode(),
+                        xmlOutputProcessData.getPwmSecurityKey() );
             }
-            catch ( PwmOperationalException e )
+            catch ( final PwmOperationalException e )
             {
                 LOGGER.warn( "error decoding stored pw value: " + e.getMessage() );
             }
 
+            final RemoteWebServiceConfiguration clonedValue = value.toBuilder().password( encodedValue ).build();
             valueElement.addText( JsonUtil.serialize( clonedValue ) );
             returnList.add( valueElement );
         }
@@ -200,12 +215,14 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
         final ArrayList<RemoteWebServiceConfiguration> output = new ArrayList<>();
         for ( final RemoteWebServiceConfiguration remoteWebServiceConfiguration : values )
         {
-            final RemoteWebServiceConfiguration clone = JsonUtil.cloneUsingJson( remoteWebServiceConfiguration, RemoteWebServiceConfiguration.class );
-            if ( !StringUtil.isEmpty( clone.getPassword() ) )
+            if ( !StringUtil.isEmpty( remoteWebServiceConfiguration.getPassword() ) )
+            {
+                output.add( remoteWebServiceConfiguration.toBuilder().password( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT ).build() );
+            }
+            else
             {
-                clone.setPassword( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT );
+                output.add( remoteWebServiceConfiguration );
             }
-            output.add( clone );
         }
         return output;
     }

+ 268 - 0
server/src/main/java/password/pwm/config/value/StoredValueEncoder.java

@@ -0,0 +1,268 @@
+/*
+ * 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.value;
+
+import lombok.Value;
+import password.pwm.PwmConstants;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.PwmBlockAlgorithm;
+import password.pwm.util.secure.PwmRandom;
+import password.pwm.util.secure.PwmSecurityKey;
+import password.pwm.util.secure.SecureEngine;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+public abstract class StoredValueEncoder
+{
+    private StoredValueEncoder()
+    {
+    }
+
+    public enum Mode
+    {
+        PLAIN( new PlaintextModeEngine(), "PLAIN" + DELIMITER, "PLAINTEXT" + DELIMITER, "RAW" + DELIMITER ),
+        STRIPPED( new StrippedModeEngine(), "REMOVED" + DELIMITER ),
+        CONFIG_PW( new ConfigPwModeEngine(), "CONFIG-PW" + DELIMITER ),
+        ENCODED( new EncodedModeEngine(), "ENC-PW" + DELIMITER, "ENCODED" + DELIMITER ),;
+
+        private final List<String> prefixes;
+        private final SecureOutputEngine secureOutputEngine;
+
+        Mode( final SecureOutputEngine secureOutputEngine, final String... prefixes )
+        {
+            this.secureOutputEngine = secureOutputEngine;
+            this.prefixes = Collections.unmodifiableList( Arrays.asList( prefixes ) );
+        }
+
+        public List<String> getPrefixes()
+        {
+            return prefixes;
+        }
+
+        public String getPrefix()
+        {
+            return prefixes.iterator().next();
+        }
+
+        public SecureOutputEngine getSecureOutputEngine()
+        {
+            return secureOutputEngine;
+        }
+    }
+
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredValueEncoder.class );
+    private static final String DELIMITER = ":";
+
+    public static Optional<String> decode(
+            final String input,
+            final Mode modeHint,
+            final PwmSecurityKey pwmSecurityKey
+    )
+            throws PwmOperationalException
+    {
+        if ( StringUtil.isEmpty( input ) )
+        {
+            return Optional.empty();
+        }
+
+        final ParsedInput parsedInput = ParsedInput.parseInput( input );
+        final Mode requestedMode = modeHint == null ? Mode.PLAIN : modeHint;
+        final Mode effectiveMode = parsedInput.getMode() == null
+                ? requestedMode
+                : parsedInput.getMode();
+        return Optional.ofNullable( effectiveMode.getSecureOutputEngine().decode( parsedInput, pwmSecurityKey ) );
+    }
+
+    public static String encode( final String realValue, final Mode mode, final PwmSecurityKey pwmSecurityKey )
+            throws PwmOperationalException
+    {
+        return mode.getSecureOutputEngine().encode( realValue, pwmSecurityKey );
+    }
+
+    @Value
+    private static class ParsedInput
+    {
+        private Mode mode;
+        private String value;
+
+        static ParsedInput parseInput( final String value )
+        {
+            if ( !StringUtil.isEmpty( value ) )
+            {
+                for ( final Mode mode : Mode.values() )
+                {
+                    for ( final String prefix : mode.getPrefixes() )
+                    {
+                        if ( value.startsWith( prefix ) )
+                        {
+                            return new ParsedInput( mode, value.substring( prefix.length() ) );
+                        }
+                    }
+                }
+            }
+
+            return new ParsedInput( null, value );
+        }
+    }
+
+    private interface SecureOutputEngine
+    {
+        String encode( String rawOutput, PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException;
+
+        String decode( ParsedInput input, PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException;
+    }
+
+
+    private static class PlaintextModeEngine implements SecureOutputEngine
+    {
+        @Override
+        public String encode( final String rawValue, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            return Mode.PLAIN.getPrefix() + rawValue;
+        }
+
+        @Override
+        public String decode( final ParsedInput input, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            return input.getValue();
+        }
+    }
+
+    private static class StrippedModeEngine implements SecureOutputEngine
+    {
+        @Override
+        public String encode( final String rawValue, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
+        }
+
+        @Override
+        public String decode( final ParsedInput input, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
+        }
+    }
+
+    private static class ConfigPwModeEngine implements SecureOutputEngine
+    {
+        @Override
+        public String encode( final String rawValue, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            try
+            {
+                final String encryptedValue = SecureEngine.encryptToString( rawValue, pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
+                return Mode.CONFIG_PW + encryptedValue;
+            }
+            catch ( final Exception e )
+            {
+                final String errorMsg = "unable to encrypt config-password value for setting: " + e.getMessage();
+                final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                throw new PwmOperationalException( errorInfo );
+            }
+        }
+
+        @Override
+        public String decode( final ParsedInput input, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            try
+            {
+                return SecureEngine.decryptStringValue( input.getValue(), pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
+            }
+            catch ( final Exception e )
+            {
+                final String errorMsg = "unable to decrypt config password value for setting: " + e.getMessage();
+                final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                LOGGER.warn( errorInfo.toDebugStr() );
+                throw new PwmOperationalException( errorInfo );
+            }
+        }
+    }
+
+    private static class EncodedModeEngine implements SecureOutputEngine
+    {
+        @Override
+        public String encode( final String rawValue, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            if ( rawValue == null )
+            {
+                return Mode.ENCODED.getPrefix();
+            }
+
+            // make sure value isn't already encoded
+            if ( ParsedInput.parseInput( rawValue ).getMode() == null )
+            {
+                try
+                {
+                    final String salt = PwmRandom.getInstance().alphaNumericString( 32 );
+                    final StoredPwData storedPwData = new StoredPwData( salt, rawValue );
+                    final String jsonData = JsonUtil.serialize( storedPwData );
+                    final String encryptedValue = SecureEngine.encryptToString( jsonData, pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
+                    return Mode.ENCODED.getPrefix() + encryptedValue;
+                }
+                catch ( final Exception e )
+                {
+                    final String errorMsg = "unable to encrypt password value for setting: " + e.getMessage();
+                    final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                    throw new PwmOperationalException( errorInfo );
+                }
+            }
+
+            return rawValue;
+        }
+
+        @Override
+        public String decode( final ParsedInput input, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            try
+            {
+                final String pwValueSuffix = input.getValue( );
+                final String decryptedValue = SecureEngine.decryptStringValue( pwValueSuffix, pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
+                final StoredPwData storedPwData = JsonUtil.deserialize( decryptedValue, StoredPwData.class );
+                return storedPwData.getValue();
+            }
+            catch ( final Exception e )
+            {
+                final String errorMsg = "unable to decrypt password value for setting: " + e.getMessage();
+                final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                LOGGER.warn( errorInfo.toDebugStr() );
+                throw new PwmOperationalException( errorInfo );
+            }
+        }
+    }
+
+    @Value
+    private static class StoredPwData implements Serializable
+    {
+        private String salt;
+        private String value;
+    }
+
+}

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

@@ -22,6 +22,8 @@ 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.XmlOutputProcessData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
@@ -37,11 +39,11 @@ import java.util.regex.Pattern;
 
 public class StringArrayValue extends AbstractValue implements StoredValue
 {
-    final List<String> values;
+    private final List<String> values;
 
     public StringArrayValue( final List<String> values )
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
     }
 
     public static StoredValueFactory factory( )
@@ -68,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( "value" );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final List<String> values = new ArrayList<>();
                 for ( final XmlElement loopValueElement : valueElements )
                 {
@@ -80,7 +82,7 @@ public class StringArrayValue extends AbstractValue implements StoredValue
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final String value : this.values )

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

@@ -23,6 +23,8 @@ 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.XmlOutputProcessData;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
@@ -32,6 +34,7 @@ import password.pwm.util.secure.PwmSecurityKey;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -60,13 +63,14 @@ public class StringValue extends AbstractValue implements StoredValue
 
             public StringValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
-                final XmlElement valueElement = settingElement.getChild( "value" );
-                return new StringValue( valueElement == null ? "" : valueElement.getText() );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final String value = valueElement.map( XmlElement::getText ).orElse( "" );
+                return new StringValue( value );
             }
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         valueElement.addText( value );

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

@@ -24,7 +24,8 @@ 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.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.i18n.Display;
@@ -40,13 +41,13 @@ import java.util.Locale;
 
 public class UserPermissionValue extends AbstractValue implements StoredValue
 {
-    final List<UserPermission> values;
+    private final List<UserPermission> values;
 
     private boolean needsXmlUpdate;
 
     public UserPermissionValue( final List<UserPermission> values )
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
     }
 
     public static StoredValueFactory factory( )
@@ -57,14 +58,14 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
             {
                 if ( input == null )
                 {
-                    return new UserPermissionValue( Collections.<UserPermission>emptyList() );
+                    return new UserPermissionValue( Collections.emptyList() );
                 }
                 else
                 {
                     List<UserPermission> srcList = JsonUtil.deserialize( input, new TypeToken<List<UserPermission>>()
                     {
                     } );
-                    srcList = srcList == null ? Collections.<UserPermission>emptyList() : srcList;
+                    srcList = srcList == null ? Collections.emptyList() : srcList;
                     while ( srcList.contains( null ) )
                     {
                         srcList.remove( null );
@@ -77,7 +78,7 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
                     throws PwmOperationalException
             {
                 final boolean newType = "2".equals(
-                        settingElement.getAttributeValue( StoredConfigurationImpl.XML_ATTRIBUTE_SYNTAX_VERSION ) );
+                        settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION ) );
                 final List<XmlElement> valueElements = settingElement.getChildren( "value" );
                 final List<UserPermission> values = new ArrayList<>();
                 for ( final XmlElement loopValueElement : valueElements )
@@ -92,7 +93,10 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
                         }
                         else
                         {
-                            values.add( new UserPermission( UserPermission.Type.ldapQuery, null, value, null ) );
+                            values.add( UserPermission.builder()
+                                    .type( UserPermission.Type.ldapQuery )
+                                    .ldapQuery( value )
+                                    .build() );
                         }
                     }
                 }
@@ -103,7 +107,7 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
         };
     }
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final UserPermission value : values )
@@ -129,7 +133,7 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
             {
                 validateLdapSearchFilter( userPermission.getLdapQuery() );
             }
-            catch ( IllegalArgumentException e )
+            catch ( final IllegalArgumentException e )
             {
                 returnObj.add( e.getMessage() + " for filter " + userPermission.getLdapQuery() );
             }

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

@@ -39,10 +39,10 @@ public class ValueFactory
     {
         try
         {
-            final StoredValue.StoredValueFactory factory = setting.getSyntax().getStoredValueImpl();
+            final StoredValue.StoredValueFactory factory = setting.getSyntax().getFactory();
             return factory.fromJson( input );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             final StringBuilder errorMsg = new StringBuilder();
             errorMsg.append( "error parsing value stored configuration value: " ).append( e.getMessage() );
@@ -59,10 +59,10 @@ public class ValueFactory
     {
         try
         {
-            final StoredValue.StoredValueFactory factory = setting.getSyntax().getStoredValueImpl();
+            final StoredValue.StoredValueFactory factory = setting.getSyntax().getFactory();
             return factory.fromXmlElement( setting, settingElement, key );
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
             final StringBuilder errorMsg = new StringBuilder();
             errorMsg.append( "error parsing stored configuration value: " ).append( e.getMessage() );
@@ -71,7 +71,7 @@ public class ValueFactory
                 errorMsg.append( ", cause: " ).append( e.getCause().getMessage() );
             }
             LOGGER.error( errorMsg, e );
-            throw new IllegalStateException( "unable to read xml element '" + settingElement.getName() + "' from setting '" + setting.getKey() + "' error: " + e.getMessage() );
+            throw new IllegalStateException( "unable to read xml element '" + settingElement.getName() + "' from setting '" + setting.getKey() + "' error: " + e.getMessage(), e );
         }
     }
 }

+ 19 - 11
server/src/main/java/password/pwm/config/value/VerificationMethodValue.java

@@ -24,6 +24,8 @@ 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.XmlOutputProcessData;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.i18n.Display;
 import password.pwm.util.i18n.LocaleHelper;
@@ -36,17 +38,17 @@ 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;
 import java.util.Map;
+import java.util.Optional;
 
 public class VerificationMethodValue extends AbstractValue implements StoredValue
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( VerificationMethodValue.class );
 
-    private VerificationMethodSettings value = new VerificationMethodSettings();
+    private final VerificationMethodSettings value;
 
 
     public enum EnabledState
@@ -58,16 +60,18 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
 
     public static class VerificationMethodSettings implements Serializable
     {
-        private Map<IdentityVerificationMethod, VerificationMethodSetting> methodSettings = new HashMap<>();
-        private int minOptionalRequired;
+        private final Map<IdentityVerificationMethod, VerificationMethodSetting> methodSettings;
+        private final int minOptionalRequired;
 
         public VerificationMethodSettings( )
         {
+            methodSettings = Collections.emptyMap();
+            minOptionalRequired = 0;
         }
 
         public VerificationMethodSettings( final Map<IdentityVerificationMethod, VerificationMethodSetting> methodSettings, final int minOptionalRequired )
         {
-            this.methodSettings = methodSettings;
+            this.methodSettings = methodSettings == null ? Collections.emptyMap() : Collections.unmodifiableMap( methodSettings );
             this.minOptionalRequired = minOptionalRequired;
         }
 
@@ -87,7 +91,7 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
     @Value
     public static class VerificationMethodSetting implements Serializable
     {
-        private final EnabledState enabledState;
+        private EnabledState enabledState;
     }
 
     public VerificationMethodValue( )
@@ -127,16 +131,20 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
             public VerificationMethodValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
                     throws PwmOperationalException
             {
-                final XmlElement valueElement = settingElement.getChild( "value" );
-                final String inputStr = valueElement.getText();
-                final VerificationMethodSettings settings = JsonUtil.deserialize( inputStr, VerificationMethodSettings.class );
-                return new VerificationMethodValue( settings );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                if ( valueElement.isPresent() )
+                {
+                    final String inputStr = valueElement.get().getText();
+                    final VerificationMethodSettings settings = JsonUtil.deserialize( inputStr, VerificationMethodSettings.class );
+                    return new VerificationMethodValue( settings );
+                }
+                return  new VerificationMethodValue(  );
             }
         };
     }
 
     @Override
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         valueElement.addText( JsonUtil.serialize( value ) );

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

@@ -22,8 +22,11 @@ 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.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.logging.PwmLogger;
@@ -47,7 +50,7 @@ import java.util.Map;
 public class X509CertificateValue extends AbstractValue implements StoredValue
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( X509CertificateValue.class );
-    private X509Certificate[] certificates;
+    private final X509Certificate[] certificates;
 
     public static StoredValueFactory factory( )
     {
@@ -56,7 +59,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( "value" );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 for ( final XmlElement loopValueElement : valueElements )
                 {
                     final String b64encodedStr = loopValueElement.getText();
@@ -64,7 +67,7 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
                     {
                         certificates.add( X509Utils.certificateFromBase64( b64encodedStr ) );
                     }
-                    catch ( Exception e )
+                    catch ( final Exception e )
                     {
                         LOGGER.error( "error decoding certificate: " + e.getMessage() );
                     }
@@ -99,12 +102,12 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
         {
             throw new NullPointerException( "certificates cannot be null" );
         }
-        this.certificates = certificates.toArray( new X509Certificate[ certificates.size() ] );
+        this.certificates = certificates.toArray( new X509Certificate[0] );
     }
 
 
     @Override
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final X509Certificate value : certificates )
@@ -112,9 +115,11 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
             final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
             try
             {
-                valueElement.addText( X509Utils.certificateToBase64( value ) );
+                final String b64Value = X509Utils.certificateToBase64( value );
+                final String splitValue = StringUtil.insertRepeatedLineBreaks( b64Value, 80 );
+                valueElement.addText( splitValue );
             }
-            catch ( CertificateEncodingException e )
+            catch ( final CertificateEncodingException e )
             {
                 LOGGER.error( "error encoding certificate: " + e.getMessage() );
             }
@@ -154,7 +159,7 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
                 sb.append( " SHA1 Hash: " ).append( SecureEngine.hash( new ByteArrayInputStream( cert.getEncoded() ),
                         PwmHashAlgorithm.SHA1 ) ).append( "\n" );
             }
-            catch ( PwmUnrecoverableException | CertificateEncodingException e )
+            catch ( final PwmUnrecoverableException | CertificateEncodingException e )
             {
                 LOGGER.warn( "error generating hash for certificate: " + e.getMessage() );
             }

+ 144 - 0
server/src/main/java/password/pwm/config/value/data/ActionConfiguration.java

@@ -123,4 +123,148 @@ public class ActionConfiguration implements Serializable
             }
         }
     }
+
+    @Value
+    @Builder( toBuilder = true )
+    public static class ActionConfigurationOldVersion1 implements Serializable
+    {
+        public enum Type
+        {
+            webservice,
+            ldap,;
+        }
+
+        public enum WebMethod
+        {
+            delete( ActionConfiguration.WebMethod.delete ),
+            get( ActionConfiguration.WebMethod.get ),
+            post( ActionConfiguration.WebMethod.post ),
+            put( ActionConfiguration.WebMethod.put ),
+            patch( ActionConfiguration.WebMethod.patch ),;
+
+            private final ActionConfiguration.WebMethod newMethod;
+
+            WebMethod( final ActionConfiguration.WebMethod newMethod )
+            {
+                this.newMethod = newMethod;
+            }
+
+            public ActionConfiguration.WebMethod getNewMethod( )
+            {
+                return newMethod;
+            }
+        }
+
+        public enum LdapMethod
+        {
+            replace( ActionConfiguration.LdapMethod.replace ),
+            add( ActionConfiguration.LdapMethod.add ),
+            remove( ActionConfiguration.LdapMethod.remove ),;
+
+            private final ActionConfiguration.LdapMethod newMethod;
+
+            LdapMethod( final ActionConfiguration.LdapMethod newType )
+            {
+                this.newMethod = newType;
+            }
+
+            public ActionConfiguration.LdapMethod getNewMethod( )
+            {
+                return newMethod;
+            }
+        }
+
+        private String name;
+        private String description;
+
+        @Builder.Default
+        private Type type = Type.webservice;
+
+        @Builder.Default
+        private WebMethod method = WebMethod.get;
+
+        private Map<String, String> headers;
+        private String url;
+        private String body;
+        private String username;
+        private String password;
+        private List<X509Certificate> certificates;
+
+        private LdapMethod ldapMethod = LdapMethod.replace;
+        private String attributeName;
+        private String attributeValue;
+
+        public static ActionConfigurationOldVersion1 parseOldConfigString( final String value )
+        {
+            final String[] splitString = value.split( "=" );
+            final String attributeName = splitString[ 0 ];
+            final String attributeValue = splitString[ 1 ];
+            return ActionConfigurationOldVersion1.builder()
+                    .name( attributeName )
+                    .description( attributeName )
+                    .type( Type.ldap )
+                    .attributeName( attributeName )
+                    .attributeValue( attributeValue )
+                    .build();
+        }
+
+        public void validate( ) throws PwmOperationalException
+        {
+            if ( this.getName() == null || this.getName().length() < 1 )
+            {
+                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                        {
+                                " form field name is required",
+                        }
+                ) );
+            }
+
+            if ( this.getType() == null )
+            {
+                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                        {
+                                " type is required for field " + this.getName(),
+                        }
+                ) );
+            }
+
+            if ( this.getType() == Type.webservice )
+            {
+                if ( this.getMethod() == null )
+                {
+                    throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                            {
+                                    " method for webservice action " + this.getName() + " is required",
+                            }
+                    ) );
+                }
+                if ( this.getUrl() == null || this.getUrl().length() < 1 )
+                {
+                    throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                            {
+                                    " url for webservice action " + this.getName() + " is required",
+                            } ) );
+                }
+            }
+            else if ( this.getType() == Type.ldap )
+            {
+                if ( this.getAttributeName() == null || this.getAttributeName().length() < 1 )
+                {
+                    throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                            {
+                                    " attribute name for ldap action " + this.getName() + " is required",
+                            }
+                    ) );
+                }
+                if ( this.getAttributeValue() == null || this.getAttributeValue().length() < 1 )
+                {
+                    throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                            {
+                                    " attribute value for ldap action " + this.getName() + " is required",
+                            }
+                    ) );
+                }
+            }
+        }
+    }
 }

+ 0 - 180
server/src/main/java/password/pwm/config/value/data/ActionConfigurationOldVersion1.java

@@ -1,180 +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.value.data;
-
-import lombok.Data;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmOperationalException;
-import password.pwm.util.java.JsonUtil;
-
-import java.io.Serializable;
-import java.security.cert.X509Certificate;
-import java.util.List;
-import java.util.Map;
-
-@Data
-public class ActionConfigurationOldVersion1 implements Serializable
-{
-    public enum Type
-    {
-        webservice,
-        ldap,;
-    }
-
-    public enum WebMethod
-    {
-        delete( ActionConfiguration.WebMethod.delete ),
-        get( ActionConfiguration.WebMethod.get ),
-        post( ActionConfiguration.WebMethod.post ),
-        put( ActionConfiguration.WebMethod.put ),
-        patch( ActionConfiguration.WebMethod.patch ),;
-
-        private final ActionConfiguration.WebMethod newMethod;
-
-        WebMethod( final ActionConfiguration.WebMethod newMethod )
-        {
-            this.newMethod = newMethod;
-        }
-
-        public ActionConfiguration.WebMethod getNewMethod( )
-        {
-            return newMethod;
-        }
-    }
-
-    public enum LdapMethod
-    {
-        replace( ActionConfiguration.LdapMethod.replace ),
-        add( ActionConfiguration.LdapMethod.add ),
-        remove( ActionConfiguration.LdapMethod.remove ),;
-
-        private final ActionConfiguration.LdapMethod newMethod;
-
-        LdapMethod( final ActionConfiguration.LdapMethod newType )
-        {
-            this.newMethod = newType;
-        }
-
-        public ActionConfiguration.LdapMethod getNewMethod( )
-        {
-            return newMethod;
-        }
-    }
-
-    private String name;
-    private String description;
-
-    private ActionConfigurationOldVersion1.Type type = ActionConfigurationOldVersion1.Type.webservice;
-
-    private ActionConfigurationOldVersion1.WebMethod method = ActionConfigurationOldVersion1.WebMethod.get;
-    private Map<String, String> headers;
-    private String url;
-    private String body;
-    private String username;
-    private String password;
-    private List<X509Certificate> certificates;
-
-
-    private ActionConfigurationOldVersion1.LdapMethod ldapMethod = ActionConfigurationOldVersion1.LdapMethod.replace;
-    private String attributeName;
-    private String attributeValue;
-
-    public static ActionConfigurationOldVersion1 parseOldConfigString( final String value )
-    {
-        final String[] splitString = value.split( "=" );
-        final String attributeName = splitString[ 0 ];
-        final String attributeValue = splitString[ 1 ];
-        final ActionConfigurationOldVersion1 actionConfiguration = new ActionConfigurationOldVersion1();
-        actionConfiguration.name = attributeName;
-        actionConfiguration.description = attributeName;
-        actionConfiguration.type = ActionConfigurationOldVersion1.Type.ldap;
-        actionConfiguration.attributeName = attributeName;
-        actionConfiguration.attributeValue = attributeValue;
-        return actionConfiguration;
-    }
-
-    public void validate( ) throws PwmOperationalException
-    {
-        if ( this.getName() == null || this.getName().length() < 1 )
-        {
-            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                    {
-                            " form field name is required",
-                    }
-            ) );
-        }
-
-        if ( this.getType() == null )
-        {
-            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                    {
-                            " type is required for field " + this.getName(),
-                    }
-            ) );
-        }
-
-        if ( this.getType() == ActionConfigurationOldVersion1.Type.webservice )
-        {
-            if ( this.getMethod() == null )
-            {
-                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                        {
-                                " method for webservice action " + this.getName() + " is required",
-                        }
-                ) );
-            }
-            if ( this.getUrl() == null || this.getUrl().length() < 1 )
-            {
-                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                        {
-                                " url for webservice action " + this.getName() + " is required",
-                        } ) );
-            }
-        }
-        else if ( this.getType() == ActionConfigurationOldVersion1.Type.ldap )
-        {
-            if ( this.getAttributeName() == null || this.getAttributeName().length() < 1 )
-            {
-                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                        {
-                                " attribute name for ldap action " + this.getName() + " is required",
-                        }
-                ) );
-            }
-            if ( this.getAttributeValue() == null || this.getAttributeValue().length() < 1 )
-            {
-                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                        {
-                                " attribute value for ldap action " + this.getName() + " is required",
-                        }
-                ) );
-            }
-        }
-    }
-
-    public ActionConfigurationOldVersion1 copyWithNewCertificate( final List<X509Certificate> certificates )
-    {
-        final ActionConfigurationOldVersion1 clone = JsonUtil.cloneUsingJson( this, ActionConfigurationOldVersion1.class );
-        clone.certificates = certificates;
-        return clone;
-    }
-}

+ 4 - 15
server/src/main/java/password/pwm/config/value/data/ChallengeItemConfiguration.java

@@ -20,11 +20,13 @@
 
 package password.pwm.config.value.data;
 
-import lombok.Getter;
+import lombok.Builder;
+import lombok.Value;
 
 import java.io.Serializable;
 
-@Getter
+@Value
+@Builder
 public class ChallengeItemConfiguration implements Serializable
 {
     private String text;
@@ -37,17 +39,4 @@ public class ChallengeItemConfiguration implements Serializable
     private int points;
     private String setupGuide;
     private String regex;
-
-    public ChallengeItemConfiguration(
-            final String challengeText,
-            final int minimumLength,
-            final int maximumLength,
-            final boolean adminDefined
-    )
-    {
-        this.text = challengeText;
-        this.minLength = minimumLength;
-        this.maxLength = maximumLength;
-        this.adminDefined = adminDefined;
-    }
 }

+ 3 - 3
server/src/main/java/password/pwm/config/CustomLinkConfiguration.java → server/src/main/java/password/pwm/config/value/data/CustomLinkConfiguration.java

@@ -18,9 +18,9 @@
  * limitations under the License.
  */
 
-package password.pwm.config;
+package password.pwm.config.value.data;
 
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
 
@@ -32,7 +32,7 @@ import java.util.Map;
 /**
  * @author Richard A. Keil
  */
-@Getter
+@Value
 public class CustomLinkConfiguration implements Serializable
 {
 

+ 18 - 31
server/src/main/java/password/pwm/config/value/data/FormConfiguration.java

@@ -20,10 +20,8 @@
 
 package password.pwm.config.value.data;
 
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
 import lombok.Builder;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
@@ -51,15 +49,10 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
-/**
- * @author Jason D. Rivard
- */
-@Getter
-@Builder
-@AllArgsConstructor( access = AccessLevel.PRIVATE )
+@Value
+@Builder( toBuilder = true )
 public class FormConfiguration implements Serializable
 {
-
     public enum Type
     {
         text,
@@ -150,23 +143,23 @@ public class FormConfiguration implements Serializable
             throw new NullPointerException( "config can not be null" );
         }
 
-        final FormConfiguration formItem = new FormConfiguration();
+        final FormConfiguration.FormConfigurationBuilder builder = FormConfiguration.builder();
         final StringTokenizer st = new StringTokenizer( config, ":" );
 
         // attribute name
-        formItem.name = st.nextToken();
+        builder.name( st.nextToken() );
 
         // label
-        formItem.labels = Collections.singletonMap( "", st.nextToken() );
+        builder.labels( Collections.singletonMap( "", st.nextToken() ) );
 
         // type
         {
             final String typeStr = st.nextToken();
             try
             {
-                formItem.type = Type.valueOf( typeStr.toLowerCase() );
+                builder.type( Type.valueOf( typeStr.toLowerCase() ) );
             }
-            catch ( IllegalArgumentException e )
+            catch ( final IllegalArgumentException e )
             {
                 throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
                         "unknown type for form config: " + typeStr,
@@ -177,9 +170,9 @@ public class FormConfiguration implements Serializable
         //minimum length
         try
         {
-            formItem.minimumLength = Integer.parseInt( st.nextToken() );
+            builder.minimumLength( Integer.parseInt( st.nextToken() ) );
         }
-        catch ( NumberFormatException e )
+        catch ( final NumberFormatException e )
         {
             throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
                     "invalid minimum length type for form config: " + e.getMessage(),
@@ -189,9 +182,9 @@ public class FormConfiguration implements Serializable
         //maximum length
         try
         {
-            formItem.maximumLength = Integer.parseInt( st.nextToken() );
+            builder.maximumLength( Integer.parseInt( st.nextToken() ) );
         }
-        catch ( NumberFormatException e )
+        catch ( final NumberFormatException e )
         {
             throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
                     "invalid maximum length type for form config: " + e.getMessage(),
@@ -199,17 +192,17 @@ public class FormConfiguration implements Serializable
         }
 
         //required
-        formItem.required = Boolean.TRUE.toString().equalsIgnoreCase( st.nextToken() );
+        builder.required( Boolean.TRUE.toString().equalsIgnoreCase( st.nextToken() ) );
 
         //confirmation
-        formItem.confirmationRequired = Boolean.TRUE.toString().equalsIgnoreCase( st.nextToken() );
+        builder.confirmationRequired( Boolean.TRUE.toString().equalsIgnoreCase( st.nextToken() ) );
 
-        return formItem;
+        return builder.build();
     }
 
     public void validate( ) throws PwmOperationalException
     {
-        if ( this.getName() == null || this.getName().length() < 1 )
+        if ( StringUtil.isEmpty( this.getName() ) )
         {
             throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
                     " form field name is required",
@@ -236,7 +229,7 @@ public class FormConfiguration implements Serializable
             {
                 Pattern.compile( this.getRegex() );
             }
-            catch ( PatternSyntaxException e )
+            catch ( final PatternSyntaxException e )
             {
                 throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
                         " regular expression for '" + this.getName() + " ' is not valid: " + e.getMessage(),
@@ -255,12 +248,6 @@ public class FormConfiguration implements Serializable
         }
     }
 
-    public FormConfiguration( )
-    {
-        labels = Collections.singletonMap( "", "" );
-        regexErrors = Collections.singletonMap( "", "" );
-    }
-
     public String getLabel( final Locale locale )
     {
         return LocaleHelper.resolveStringKeyLocaleMap( locale, labels );
@@ -341,7 +328,7 @@ public class FormConfiguration implements Serializable
                     {
                         new BigInteger( value );
                     }
-                    catch ( NumberFormatException e )
+                    catch ( final NumberFormatException e )
                     {
                         final ErrorInformation error = new ErrorInformation( PwmError.ERROR_FIELD_NOT_A_NUMBER, null, new String[] {
                                 getLabel( locale ),

+ 2 - 4
server/src/main/java/password/pwm/config/value/data/NamedSecretData.java

@@ -20,15 +20,13 @@
 
 package password.pwm.config.value.data;
 
-import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.util.PasswordData;
 
 import java.io.Serializable;
 import java.util.List;
 
-@Getter
-@AllArgsConstructor
+@Value
 public class NamedSecretData implements Serializable
 {
     private PasswordData password;

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

@@ -20,16 +20,16 @@
 
 package password.pwm.config.value.data;
 
-import lombok.Getter;
-import lombok.Setter;
+import lombok.Builder;
+import lombok.Value;
 
 import java.io.Serializable;
 import java.security.cert.X509Certificate;
 import java.util.List;
 import java.util.Map;
 
-@Getter
-@Setter
+@Value
+@Builder( toBuilder = true )
 public class RemoteWebServiceConfiguration implements Serializable
 {
 

+ 4 - 6
server/src/main/java/password/pwm/config/value/data/ShortcutItem.java

@@ -20,15 +20,13 @@
 
 package password.pwm.config.value.data;
 
-import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.util.logging.PwmLogger;
 
 import java.io.Serializable;
 import java.net.URI;
 
-@Getter
-@AllArgsConstructor
+@Value
 public class ShortcutItem implements Serializable
 {
 
@@ -63,7 +61,7 @@ public class ShortcutItem implements Serializable
                         splitSettings[ 3 ]
                 );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.warn( "malformed ShortcutItem configuration value of '" + input + "', " + e.getMessage() );
             }
@@ -86,7 +84,7 @@ public class ShortcutItem implements Serializable
                         splitSettings[ 2 ]
                 );
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
                 LOGGER.warn( "malformed ShortcutItem configuration value of '" + input + "', " + e.getMessage() );
             }

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff