瀏覽代碼

Merge branch 'master' into enh-domainadmin

# Conflicts:
#	server/src/main/java/password/pwm/http/SessionManager.java
#	server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java
#	server/src/main/java/password/pwm/http/servlet/command/CommandServlet.java
#	server/src/main/java/password/pwm/svc/report/ReportCsvUtility.java
#	server/src/main/java/password/pwm/svc/report/ReportRecordLocalDBStorageService.java
#	server/src/main/java/password/pwm/svc/report/ReportService.java
#	server/src/main/java/password/pwm/util/debug/LdapRecentUserDebugGenerator.java
Jason Rivard 3 年之前
父節點
當前提交
2fe190199b
共有 100 個文件被更改,包括 15214 次插入1432 次删除
  1. 0 117
      .mvn/wrapper/MavenWrapperDownloader.java
  2. 二進制
      .mvn/wrapper/maven-wrapper.jar
  3. 4 4
      .mvn/wrapper/maven-wrapper.properties
  4. 1 1
      README.md
  5. 1 0
      build/checkstyle.xml
  6. 13 2
      build/spotbugs-exclude.xml
  7. 13620 1
      client/angular/package-lock.json
  8. 21 14
      client/angular/package.json
  9. 3 3
      client/pom.xml
  10. 11 31
      data-service/pom.xml
  11. 3 0
      data-service/src/main/java/password/pwm/receiver/ContextManager.java
  12. 6 13
      data-service/src/main/java/password/pwm/receiver/Logger.java
  13. 61 0
      data-service/src/main/java/password/pwm/receiver/PublishVersionServlet.java
  14. 56 0
      data-service/src/main/java/password/pwm/receiver/ReceiverUtil.java
  15. 37 5
      data-service/src/main/java/password/pwm/receiver/Settings.java
  16. 2 2
      data-service/src/main/java/password/pwm/receiver/SummaryBean.java
  17. 16 8
      data-service/src/main/java/password/pwm/receiver/TelemetryRestReceiver.java
  18. 5 1
      data-service/src/main/java/password/pwm/receiver/TelemetryViewerServlet.java
  19. 31 0
      data-service/src/main/webapp/WEB-INF/classes/logback.xml
  20. 0 22
      lib-data/pom.xml
  21. 2 2
      lib-data/src/main/java/password/pwm/bean/PhotoDataBean.java
  22. 89 0
      lib-data/src/main/java/password/pwm/bean/VersionNumber.java
  23. 1 2
      lib-data/src/main/java/password/pwm/bean/pub/PublicUserInfoBean.java
  24. 11 7
      lib-data/src/main/java/password/pwm/bean/pub/PublishVersionBean.java
  25. 3 0
      lib-data/src/main/java/password/pwm/bean/pub/PublishedBean.java
  26. 1 1
      lib-data/src/main/java/password/pwm/data/ImmutableByteArray.java
  27. 67 0
      lib-data/src/test/java/password/pwm/bean/VersionNumberTest.java
  28. 6 22
      lib-util/pom.xml
  29. 11 2
      lib-util/src/main/java/password/pwm/util/ProgressInfoCalculator.java
  30. 34 0
      lib-util/src/main/java/password/pwm/util/java/CollectionUtil.java
  31. 0 46
      lib-util/src/main/java/password/pwm/util/java/CrcChecksumInputStream.java
  32. 0 45
      lib-util/src/main/java/password/pwm/util/java/CrcChecksumOutputStream.java
  33. 16 11
      lib-util/src/main/java/password/pwm/util/java/JavaHelper.java
  34. 10 0
      lib-util/src/main/java/password/pwm/util/java/LazySupplier.java
  35. 9 8
      lib-util/src/main/java/password/pwm/util/java/StringUtil.java
  36. 78 0
      lib-util/src/main/java/password/pwm/util/java/ThresholdInputStream.java
  37. 31 60
      mvnw
  38. 6 7
      mvnw.cmd
  39. 1 1
      onejar/pom.xml
  40. 1 1
      onejar/src/main/java/password/pwm/onejar/ArgumentParser.java
  41. 2 1
      onejar/src/main/java/password/pwm/onejar/TomcatOnejarRunner.java
  42. 51 8
      pom.xml
  43. 2 2
      rest-test-service/pom.xml
  44. 3 49
      server/pom.xml
  45. 2 2
      server/src/main/java/password/pwm/AppAttribute.java
  46. 14 1
      server/src/main/java/password/pwm/AppProperty.java
  47. 9 16
      server/src/main/java/password/pwm/PwmAboutProperty.java
  48. 40 47
      server/src/main/java/password/pwm/PwmApplication.java
  49. 45 25
      server/src/main/java/password/pwm/PwmApplicationUtil.java
  50. 1 1
      server/src/main/java/password/pwm/PwmConstants.java
  51. 28 14
      server/src/main/java/password/pwm/PwmDomain.java
  52. 37 26
      server/src/main/java/password/pwm/PwmDomainUtil.java
  53. 44 10
      server/src/main/java/password/pwm/PwmEnvironment.java
  54. 1 1
      server/src/main/java/password/pwm/VerificationMethodSystem.java
  55. 1 1
      server/src/main/java/password/pwm/bean/LocalSessionStateBean.java
  56. 1 2
      server/src/main/java/password/pwm/bean/RemoteVerificationRequestBean.java
  57. 1 2
      server/src/main/java/password/pwm/bean/RemoteVerificationResponseBean.java
  58. 2 0
      server/src/main/java/password/pwm/bean/SessionLabel.java
  59. 1 1
      server/src/main/java/password/pwm/bean/TokenDestinationItem.java
  60. 24 7
      server/src/main/java/password/pwm/config/AppConfig.java
  61. 103 50
      server/src/main/java/password/pwm/config/PwmSetting.java
  62. 1 2
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  63. 1 1
      server/src/main/java/password/pwm/config/PwmSettingFlag.java
  64. 244 0
      server/src/main/java/password/pwm/config/PwmSettingMetaData.java
  65. 0 430
      server/src/main/java/password/pwm/config/PwmSettingMetaDataReader.java
  66. 3 2
      server/src/main/java/password/pwm/config/PwmSettingStats.java
  67. 1 1
      server/src/main/java/password/pwm/config/PwmSettingTemplate.java
  68. 5 15
      server/src/main/java/password/pwm/config/PwmSettingTemplateSet.java
  69. 9 18
      server/src/main/java/password/pwm/config/PwmSettingXml.java
  70. 3 3
      server/src/main/java/password/pwm/config/StoredSettingReader.java
  71. 70 0
      server/src/main/java/password/pwm/config/TemplateSetReference.java
  72. 2 4
      server/src/main/java/password/pwm/config/option/WebServiceUsage.java
  73. 2 2
      server/src/main/java/password/pwm/config/profile/LdapProfile.java
  74. 35 35
      server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  75. 60 98
      server/src/main/java/password/pwm/config/profile/PwmPasswordRule.java
  76. 11 10
      server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java
  77. 18 17
      server/src/main/java/password/pwm/config/stored/ConfigurationFileManager.java
  78. 1 1
      server/src/main/java/password/pwm/config/stored/StoredConfigXmlSerializer.java
  79. 2 2
      server/src/main/java/password/pwm/config/stored/StoredConfigZipJsonSerializer.java
  80. 0 3
      server/src/main/java/password/pwm/config/stored/StoredConfigurationFactory.java
  81. 4 2
      server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java
  82. 25 41
      server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java
  83. 1 1
      server/src/main/java/password/pwm/config/value/AbstractValue.java
  84. 1 1
      server/src/main/java/password/pwm/config/value/ActionValue.java
  85. 1 1
      server/src/main/java/password/pwm/config/value/BooleanValue.java
  86. 1 1
      server/src/main/java/password/pwm/config/value/ChallengeValue.java
  87. 5 9
      server/src/main/java/password/pwm/config/value/CustomLinkValue.java
  88. 7 7
      server/src/main/java/password/pwm/config/value/EmailValue.java
  89. 2 2
      server/src/main/java/password/pwm/config/value/FileValue.java
  90. 1 1
      server/src/main/java/password/pwm/config/value/FormValue.java
  91. 1 1
      server/src/main/java/password/pwm/config/value/LocalizedStringArrayValue.java
  92. 1 1
      server/src/main/java/password/pwm/config/value/LocalizedStringValue.java
  93. 3 2
      server/src/main/java/password/pwm/config/value/NamedSecretValue.java
  94. 1 1
      server/src/main/java/password/pwm/config/value/NumericArrayValue.java
  95. 1 1
      server/src/main/java/password/pwm/config/value/NumericValue.java
  96. 3 5
      server/src/main/java/password/pwm/config/value/OptionListValue.java
  97. 3 2
      server/src/main/java/password/pwm/config/value/PasswordValue.java
  98. 3 2
      server/src/main/java/password/pwm/config/value/PrivateKeyValue.java
  99. 1 1
      server/src/main/java/password/pwm/config/value/RemoteWebServiceValue.java
  100. 1 1
      server/src/main/java/password/pwm/config/value/StoredValue.java

+ 0 - 117
.mvn/wrapper/MavenWrapperDownloader.java

@@ -1,117 +0,0 @@
-/*
- * Copyright 2007-present the original author or authors.
- *
- * 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.
- */
-import java.net.*;
-import java.io.*;
-import java.nio.channels.*;
-import java.util.Properties;
-
-public class MavenWrapperDownloader {
-
-    private static final String WRAPPER_VERSION = "0.5.6";
-    /**
-     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
-     */
-    private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
-        + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
-
-    /**
-     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
-     * use instead of the default one.
-     */
-    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
-            ".mvn/wrapper/maven-wrapper.properties";
-
-    /**
-     * Path where the maven-wrapper.jar will be saved to.
-     */
-    private static final String MAVEN_WRAPPER_JAR_PATH =
-            ".mvn/wrapper/maven-wrapper.jar";
-
-    /**
-     * Name of the property which should be used to override the default download url for the wrapper.
-     */
-    private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
-
-    public static void main(String args[]) {
-        System.out.println("- Downloader started");
-        File baseDirectory = new File(args[0]);
-        System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
-
-        // If the maven-wrapper.properties exists, read it and check if it contains a custom
-        // wrapperUrl parameter.
-        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
-        String url = DEFAULT_DOWNLOAD_URL;
-        if(mavenWrapperPropertyFile.exists()) {
-            FileInputStream mavenWrapperPropertyFileInputStream = null;
-            try {
-                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
-                Properties mavenWrapperProperties = new Properties();
-                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
-                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
-            } catch (IOException e) {
-                System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
-            } finally {
-                try {
-                    if(mavenWrapperPropertyFileInputStream != null) {
-                        mavenWrapperPropertyFileInputStream.close();
-                    }
-                } catch (IOException e) {
-                    // Ignore ...
-                }
-            }
-        }
-        System.out.println("- Downloading from: " + url);
-
-        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
-        if(!outputFile.getParentFile().exists()) {
-            if(!outputFile.getParentFile().mkdirs()) {
-                System.out.println(
-                        "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
-            }
-        }
-        System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
-        try {
-            downloadFileFromURL(url, outputFile);
-            System.out.println("Done");
-            System.exit(0);
-        } catch (Throwable e) {
-            System.out.println("- Error downloading");
-            e.printStackTrace();
-            System.exit(1);
-        }
-    }
-
-    private static void downloadFileFromURL(String urlString, File destination) throws Exception {
-        if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
-            String username = System.getenv("MVNW_USERNAME");
-            char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
-            Authenticator.setDefault(new Authenticator() {
-                @Override
-                protected PasswordAuthentication getPasswordAuthentication() {
-                    return new PasswordAuthentication(username, password);
-                }
-            });
-        }
-        URL website = new URL(urlString);
-        ReadableByteChannel rbc;
-        rbc = Channels.newChannel(website.openStream());
-        FileOutputStream fos = new FileOutputStream(destination);
-        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
-        fos.close();
-        rbc.close();
-    }
-
-}

二進制
.mvn/wrapper/maven-wrapper.jar


+ 4 - 4
.mvn/wrapper/maven-wrapper.properties

@@ -5,9 +5,9 @@
 # to you 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
-# 
+#
+#   https://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
@@ -15,4 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
-wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar

+ 1 - 1
README.md

@@ -120,7 +120,7 @@ docker load --input=pwm-docker-image-v2.0.0.tar
 1. Create docker image named _mypwm_, map to the server's 8443 port, and set the config volume to use the server's
 local file system _/home/user/pwm-config_ folder:
 ```
-docker create --name mypwm pwm/pwm-webapp -p '8443:8443' -v '/config:/home/user/pwm-config
+docker create --name mypwm -p '8443:8443' pwm/pwm-webapp -v '/config:/home/user/pwm-config'
 ```
 
 1. Start the _mypwm_ container:

+ 1 - 0
build/checkstyle.xml

@@ -162,6 +162,7 @@
         <module name="UnusedImports"/>
         <module name="ImportControl">
             <property name="file" value="${basedir}/build/checkstyle-import.xml"/>
+            <property name="path" value="^.*[\\/]src[\\/]main[\\/].*$"/>
         </module>
 
 

+ 13 - 2
build/spotbugs-exclude.xml

@@ -62,9 +62,20 @@
         <Bug pattern="MRC_METHOD_RETURNS_CONSTANT" />
     </Match>
 
+    <Match>
+        <!-- issue with spotbugs 4.7.0 -->
+        <!-- https://github.com/spotbugs/spotbugs/issues/2040 -->
+        <Bug pattern="THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" />
+    </Match>
 
 
-
-
+    <!-- jsp exclusions -->
+    <Match>
+        <Or>
+            <Class name="~^jsp\..*_jsp$" />
+        </Or>
+        <!-- pretty much every line in jsp throws throwable :) -->
+        <Bug pattern="THROWS_METHOD_THROWS_CLAUSE_THROWABLE" />
+    </Match>
 
 </FindBugsFilter>

文件差異過大導致無法顯示
+ 13620 - 1
client/angular/package-lock.json


+ 21 - 14
client/angular/package.json

@@ -17,15 +17,15 @@
     "author": "",
     "license": "ISC",
     "dependencies": {
-        "@microfocus/ias-icons": "1.0.4",
+        "@microfocus/ias-icons": "1.1.3",
         "@microfocus/ng-ias": "1.0.1",
-        "@microfocus/ux-ias": "1.1.2",
-        "@uirouter/angularjs": "1.0.29",
-        "angular": "1.8.2",
-        "angular-aria": "1.8.2",
-        "angular-sanitize": "1.8.2",
+        "@microfocus/ux-ias": "1.1.3",
+        "@uirouter/angularjs": "1.0.30",
+        "angular": "1.8.3",
+        "angular-aria": "1.8.3",
+        "angular-sanitize": "1.8.3",
         "angular-translate": "2.19.0",
-        "core-js": "3.18.2",
+        "core-js": "3.22.8",
         "textangular": "1.5.16"
     },
     "devDependencies": {
@@ -49,23 +49,22 @@
         "jshint": "^2.13.4",
         "jshint-loader": "0.8.4",
         "json-loader": "0.5.7",
-        "karma": "^6.3.16",
+        "karma": "^6.3.20",
         "karma-chrome-launcher": "2.2.0",
         "karma-jasmine": "1.1.2",
         "karma-jasmine-html-reporter": "1.3.1",
-        "karma-phantomjs-launcher": "1.0.4",
         "karma-sourcemap-loader": "0.3.7",
         "karma-spec-reporter": "0.0.32",
-        "karma-webpack": "3.0.5",
-        "moment": "2.21.0",
+        "karma-webpack": "5.0.0",
+        "lodash": ">=4.17.21",
+        "moment": "^2.29.3",
         "ngtemplate-loader": "2.0.1",
         "node-sass": "^7.0.0",
-        "phantomjs": "2.1.7",
-        "phantomjs-prebuilt": "2.1.16",
         "postcss-loader": "2.1.1",
         "raw-loader": "0.5.1",
         "rimraf": "2.6.2",
         "sass-loader": "6.0.7",
+        "serialize-javascript": "4.0.0",
         "string-replace-loader": "2.1.1",
         "style-loader": "0.20.3",
         "trim-newlines": ">=3.0.1",
@@ -78,8 +77,16 @@
         "url-loader": "1.0.1",
         "webpack": "^4.46.0",
         "webpack-cli": "^3.3.10",
-        "webpack-dev-server": "3.1.14",
+        "webpack-dev-server": "3.11.3",
         "webpack-merge": "4.1.2",
         "write-file-webpack-plugin": "4.2.0"
+    },
+    "overrides": {
+        "jshint-loader": {
+            "lodash": ">=4.17.21"
+        },
+        "rcloader": {
+            "lodash": ">=4.17.21"
+        }
     }
 }

+ 3 - 3
client/pom.xml

@@ -13,8 +13,8 @@
     <packaging>jar</packaging>
 
     <properties>
-        <node.version>v14.18.3</node.version>
-        <npm.version>6.14.15</npm.version>
+        <node.version>v16.15.1</node.version>
+        <npm.version>8.12.1</npm.version>
     </properties>
 
     <name>PWM Password Self Service: Angular Client JAR</name>
@@ -109,7 +109,7 @@
                             <goal>npm</goal>
                         </goals>
                         <configuration>
-                            <arguments>install</arguments>
+                            <arguments>install --legacy-peer-deps</arguments>
                         </configuration>
                     </execution>
                     <execution>

+ 11 - 31
data-service/pom.xml

@@ -126,7 +126,7 @@
                                 <appRoot>/usr/local/tomcat/webapps/ROOT</appRoot>
                                 <jvmFlags>
                                     <jvmFlag>-server</jvmFlag>
-                                    <jvmFlag>-Xmx512m</jvmFlag>
+                                    <jvmFlag>-Xmx256m</jvmFlag>
                                 </jvmFlags>
                                 <environment>
                                     <DATA_SERVICE_PROPS>/config/data-service.properties</DATA_SERVICE_PROPS>
@@ -178,8 +178,8 @@
         </dependency>
         <dependency>
             <groupId>javax.servlet.jsp</groupId>
-            <artifactId>jsp-api</artifactId>
-            <version>2.2.1-b03</version>
+            <artifactId>javax.servlet.jsp-api</artifactId>
+            <version>2.3.3</version>
             <scope>provided</scope>
         </dependency>
         <!-- / container dependencies -->
@@ -195,39 +195,19 @@
             <version>1.9.0</version>
         </dependency>
         <dependency>
-            <groupId>org.apache.commons</groupId>
-            <artifactId>commons-lang3</artifactId>
-            <version>3.12.0</version>
-        </dependency>
-        <dependency>
-            <groupId>com.sun.mail</groupId>
-            <artifactId>jakarta.mail</artifactId>
+            <groupId>org.jetbrains.xodus</groupId>
+            <artifactId>xodus-environment</artifactId>
             <version>2.0.1</version>
         </dependency>
         <dependency>
-            <groupId>org.apache.httpcomponents</groupId>
-            <artifactId>httpclient</artifactId>
-            <version>4.5.13</version>
-        </dependency>
-        <dependency>
-            <groupId>ch.qos.reload4j</groupId>
-            <artifactId>reload4j</artifactId>
-            <version>1.2.20</version>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>2.0.0-alpha7</version>
         </dependency>
         <dependency>
-            <groupId>org.jdom</groupId>
-            <artifactId>jdom2</artifactId>
-            <version>2.0.6.1</version>
-        </dependency>
-        <dependency>
-            <groupId>com.google.code.gson</groupId>
-            <artifactId>gson</artifactId>
-            <version>2.9.0</version>
-        </dependency>
-        <dependency>
-            <groupId>org.jetbrains.xodus</groupId>
-            <artifactId>xodus-environment</artifactId>
-            <version>2.0.1</version>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.3.0-alpha16</version>
         </dependency>
     </dependencies>
 </project>

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

@@ -28,6 +28,7 @@ import javax.servlet.annotation.WebListener;
 @WebListener
 public class ContextManager implements ServletContextListener
 {
+    private static final Logger LOGGER = Logger.createLogger( ContextManager.class );
     private static final String CONTEXT_ATTR = "contextManager";
     private PwmReceiverApp app;
 
@@ -36,6 +37,7 @@ public class ContextManager implements ServletContextListener
     {
         app = new PwmReceiverApp();
         sce.getServletContext().setAttribute( CONTEXT_ATTR, this );
+        LOGGER.info( "open for bidness" );
     }
 
     @Override
@@ -43,6 +45,7 @@ public class ContextManager implements ServletContextListener
     {
         app.close();
         app = null;
+        LOGGER.info( "cya!" );
     }
 
     public PwmReceiverApp getApp( )

+ 6 - 13
data-service/src/main/java/password/pwm/receiver/Logger.java

@@ -20,16 +20,13 @@
 
 package password.pwm.receiver;
 
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
 public class Logger
 {
-    private final String name;
+    private final org.slf4j.Logger logger;
 
     private Logger( final Class<?> classname )
     {
-        this.name = classname.getName();
+        this.logger = org.slf4j.LoggerFactory.getLogger( classname );
     }
 
     public static Logger createLogger( final Class<?> classname )
@@ -37,17 +34,13 @@ public class Logger
         return new Logger( classname );
     }
 
-    public void info( final CharSequence input )
+    public void info( final String input )
     {
-        System.out.println( "PwmReceiver: "
-                + Instant.now().truncatedTo( ChronoUnit.SECONDS ).toString()
-                + ", INFO , " + name + ", " + input );
+        logger.info( input );
     }
 
-    public void debug( final CharSequence input )
+    public void debug( final String input )
     {
-        System.out.println( "PwmReceiver: "
-                + Instant.now().toString()
-                + ", DEBUG, " + name + ", " + input );
+        logger.debug( input );
     }
 }

+ 61 - 0
data-service/src/main/java/password/pwm/receiver/PublishVersionServlet.java

@@ -0,0 +1,61 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 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.receiver;
+
+import password.pwm.bean.pub.PublishVersionBean;
+import password.pwm.util.java.AtomicLoopIntIncrementer;
+import password.pwm.ws.server.RestResultBean;
+
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Collections;
+
+@WebServlet(
+        urlPatterns = {
+                "/version",
+        }
+)
+public class PublishVersionServlet extends HttpServlet
+{
+    private static final Logger LOGGER = Logger.createLogger( PublishVersionServlet.class );
+    private static final AtomicLoopIntIncrementer REQ_COUNTER = new AtomicLoopIntIncrementer();
+
+
+    @Override
+    protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
+            throws IOException
+    {
+        final int requestId = REQ_COUNTER.next();
+        LOGGER.debug( "http request #" + requestId + " for version" );
+
+        final ContextManager contextManager = ContextManager.getContextManager( req.getServletContext() );
+        final PwmReceiverApp app = contextManager.getApp();
+        final PublishVersionBean publishVersionBean = new PublishVersionBean(
+                Collections.singletonMap( PublishVersionBean.VersionKey.current, app.getSettings().getCurrentVersionInfo() ) );
+
+        final RestResultBean<PublishVersionBean> restResultBean = RestResultBean.withData( publishVersionBean, PublishVersionBean.class );
+
+        ReceiverUtil.outputJsonResponse( req, resp, restResultBean );
+    }
+}

+ 56 - 0
data-service/src/main/java/password/pwm/receiver/ReceiverUtil.java

@@ -0,0 +1,56 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 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.receiver;
+
+import password.pwm.http.PwmHttpRequestWrapper;
+import password.pwm.ws.server.RestResultBean;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class ReceiverUtil
+{
+    static <T> void outputJsonResponse(
+
+            final HttpServletRequest request,
+            final HttpServletResponse response,
+            final RestResultBean<T> restResultBean
+    )
+            throws IOException
+    {
+        final boolean jsonPrettyPrint = PwmHttpRequestWrapper.isPrettyPrintJsonParameterTrue( request );
+        response.setHeader( "Content", "application/json" );
+        response.getWriter().print( restResultBean.toJson( jsonPrettyPrint ) );
+    }
+
+    static int silentIntParser( final String input )
+    {
+        try
+        {
+            return Integer.parseInt( input );
+        }
+        catch ( final NumberFormatException e )
+        {
+            return 0;
+        }
+    }
+}

+ 37 - 5
data-service/src/main/java/password/pwm/receiver/Settings.java

@@ -20,6 +20,8 @@
 
 package password.pwm.receiver;
 
+import password.pwm.bean.VersionNumber;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 
@@ -30,13 +32,14 @@ import java.io.Reader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.EnumSet;
 import java.util.Map;
 import java.util.Properties;
 import java.util.stream.Collectors;
 
 public class Settings
 {
+    private static final Logger LOGGER = Logger.createLogger( Setting.class );
+
     enum Setting
     {
         ftpMode( FtpMode.ftp.name() ),
@@ -45,7 +48,8 @@ public class Settings
         ftpPassword( null ),
         ftpReadPath( null ),
         storagePath( null ),
-        maxInstanceSeconds( Long.toString( TimeDuration.of( 14, TimeDuration.Unit.DAYS ).as( TimeDuration.Unit.SECONDS ) ) ),;
+        maxInstanceSeconds( Long.toString( TimeDuration.of( 14, TimeDuration.Unit.DAYS ).as( TimeDuration.Unit.SECONDS ) ) ),
+        currentVersion( null ),;
 
         private final String defaultValue;
 
@@ -68,9 +72,12 @@ public class Settings
 
     private final Map<Setting, String> settings;
 
+    private final VersionNumber versionNumber;
+
     private Settings( final Map<Setting, String> settings )
     {
         this.settings = settings;
+        this.versionNumber = parseCurrentVersionInfo();
     }
 
     static Settings readFromFile( final String filename ) throws IOException
@@ -80,10 +87,10 @@ public class Settings
         try ( Reader reader = new InputStreamReader( Files.newInputStream( path ), StandardCharsets.UTF_8 ) )
         {
             properties.load( reader );
-            final Map<Setting, String> returnMap = EnumSet.allOf( Setting.class ).stream()
+            final Map<Setting, String> returnMap = CollectionUtil.enumStream( Setting.class )
                     .collect( Collectors.toUnmodifiableMap(
-                      setting -> setting,
-                      setting -> properties.getProperty( setting.name(), setting.getDefaultValue() )
+                            setting -> setting,
+                            setting -> properties.getProperty( setting.name(), setting.getDefaultValue() )
                     ) );
 
             return new Settings( returnMap );
@@ -100,4 +107,29 @@ public class Settings
         final String value = settings.get( Setting.ftpSite );
         return StringUtil.notEmpty( value );
     }
+
+    public VersionNumber getCurrentVersionInfo()
+    {
+        return versionNumber;
+    }
+
+    private VersionNumber parseCurrentVersionInfo()
+    {
+        final String stringVersion = getSetting( Setting.currentVersion );
+
+        if ( stringVersion == null || stringVersion.isEmpty() )
+        {
+            return VersionNumber.ZERO;
+        }
+
+        try
+        {
+            return VersionNumber.parse( stringVersion );
+        }
+        catch ( final Exception e )
+        {
+            LOGGER.info( "error parsing version string from setting properties: " + e.getMessage() );
+            return VersionNumber.ZERO;
+        }
+    }
 }

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

@@ -90,7 +90,7 @@ public class SummaryBean
                         .osVersion( bean.getAbout().get( PwmAboutProperty.java_osVersion.name() ) )
                         .servletName( bean.getAbout().get( PwmAboutProperty.java_appServerInfo.name() ) )
                         .dbVendor( dbVendor )
-                        .appliance( Boolean.parseBoolean( bean.getAbout().get( PwmAboutProperty.app_mode_appliance.name() ) ) )
+                        .platform( bean.getAbout().get( PwmAboutProperty.app_deployment_type.name() ) )
                         .javaVm( javaVmInfo( bean, "n/a" ) )
                         .build();
 
@@ -200,6 +200,6 @@ public class SummaryBean
         private String servletName;
         private String dbVendor;
         private String javaVm;
-        private boolean appliance;
+        private String platform;
     }
 }

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

@@ -24,9 +24,9 @@ import password.pwm.bean.TelemetryPublishBean;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.PwmHttpRequestWrapper;
+import password.pwm.http.ServletUtility;
 import password.pwm.i18n.Message;
-import password.pwm.util.ServletUtility;
+import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.ws.server.RestResultBean;
 
@@ -38,7 +38,6 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 
 @WebServlet(
-        name = "TelemetryRestReceiver",
         urlPatterns = {
                 "/telemetry",
         }
@@ -46,28 +45,37 @@ import java.io.IOException;
 
 public class TelemetryRestReceiver extends HttpServlet
 {
+    private static final Logger LOGGER = Logger.createLogger( TelemetryViewerServlet.class );
+    private static final AtomicLoopIntIncrementer REQ_COUNTER = new AtomicLoopIntIncrementer();
+
+
     @Override
     protected void doPost( final HttpServletRequest req, final HttpServletResponse resp )
             throws ServletException, IOException
     {
-        final boolean jsonPrettyPrint = PwmHttpRequestWrapper.isPrettyPrintJsonParameterTrue( req );
         try
         {
-            resp.setHeader( "Content", "application/json" );
+            final int requestId = REQ_COUNTER.next();
+            LOGGER.debug( "http rest request #" + requestId + " for telemetry update" );
+
             final String input = ServletUtility.readRequestBodyAsString( req, 1024 * 1024 );
             final TelemetryPublishBean telemetryPublishBean = JsonFactory.get().deserialize( input, TelemetryPublishBean.class );
             final Storage storage = ContextManager.getContextManager( this.getServletContext() ).getApp().getStorage();
             storage.store( telemetryPublishBean );
-            resp.getWriter().print( RestResultBean.forSuccessMessage( null, null, null, Message.Success_Unknown ).toJson( jsonPrettyPrint ) );
+
+            final RestResultBean restResultBean = RestResultBean.forSuccessMessage( null, null, null, Message.Success_Unknown );
+            ReceiverUtil.outputJsonResponse( req, resp, restResultBean );
+            LOGGER.debug( "http rest request #" + requestId + " received from " + telemetryPublishBean.getSiteDescription() );
         }
         catch ( final PwmUnrecoverableException e )
         {
-            resp.getWriter().print( RestResultBean.fromError( e.getErrorInformation() ).toJson( jsonPrettyPrint ) );
+            final RestResultBean restResultBean = RestResultBean.fromError( e.getErrorInformation() );
+            ReceiverUtil.outputJsonResponse( req, resp, restResultBean );
         }
         catch ( final Exception e )
         {
             final RestResultBean restResultBean = RestResultBean.fromError( new ErrorInformation( PwmError.ERROR_INTERNAL, e.getMessage() ) );
-            resp.getWriter().print( restResultBean.toJson( jsonPrettyPrint ) );
+            ReceiverUtil.outputJsonResponse( req, resp, restResultBean );
         }
     }
 }

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

@@ -20,6 +20,7 @@
 
 package password.pwm.receiver;
 
+import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.StringUtil;
 
 import javax.servlet.ServletException;
@@ -41,14 +42,17 @@ public class TelemetryViewerServlet extends HttpServlet
 {
     private static final Logger LOGGER = Logger.createLogger( TelemetryViewerServlet.class );
     private static final String PARAM_DAYS = "days";
+    private static final AtomicLoopIntIncrementer REQ_COUNTER = new AtomicLoopIntIncrementer();
 
     public static final String SUMMARY_ATTR = "SummaryBean";
 
+
     @Override
     protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
             throws ServletException, IOException
     {
-        LOGGER.debug( "http request for viewer" );
+        final int requestId = REQ_COUNTER.next();
+        LOGGER.debug( "http request #" + requestId + " for viewer" );
         final String daysString = req.getParameter( PARAM_DAYS );
         final int days = StringUtil.isEmpty( daysString ) ? 30 : Integer.parseInt( daysString );
         final ContextManager contextManager = ContextManager.getContextManager( req.getServletContext() );

+ 31 - 0
data-service/src/main/webapp/WEB-INF/classes/logback.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2021 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.
+  -->
+
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{yyyy-MM-dd'T'HH:mm:ss'Z'}, %-5level, %logger{36}, %m%n</pattern>
+        </encoder>
+    </appender>
+    <root level="trace">
+        <appender-ref ref="STDOUT" />
+    </root>
+</configuration>

+ 0 - 22
lib-data/pom.xml

@@ -37,28 +37,6 @@
     </profiles>
     <build>
         <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <version>3.0.0-M6</version>
-                <executions>
-                    <execution>
-                        <id>default-test</id>
-                        <goals>
-                            <goal>test</goal>
-                        </goals>
-                        <phase>test</phase>
-                        <configuration>
-                            <trimStackTrace>false</trimStackTrace>
-                            <skipTests>${skipTests}</skipTests>
-                            <excludes>
-                                <exclude>**/ExtendedTest*.java</exclude>
-                                <exclude>**/*ExtendedTest*.java</exclude>
-                            </excludes>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>

+ 2 - 2
server/src/main/java/password/pwm/ldap/PhotoDataBean.java → lib-data/src/main/java/password/pwm/bean/PhotoDataBean.java

@@ -18,10 +18,10 @@
  * limitations under the License.
  */
 
-package password.pwm.ldap;
+package password.pwm.bean;
 
 import lombok.Value;
-import password.pwm.util.java.ImmutableByteArray;
+import password.pwm.data.ImmutableByteArray;
 
 import java.io.Serializable;
 

+ 89 - 0
lib-data/src/main/java/password/pwm/bean/VersionNumber.java

@@ -0,0 +1,89 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 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.bean;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Value;
+
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+@Value
+@AllArgsConstructor( access = AccessLevel.PRIVATE )
+public class VersionNumber implements Comparable<VersionNumber>
+{
+    private static final String VERSION_DELIMITER = ".";
+    private static final String VERSION_PREFIX = "v";
+
+    // split by , . or -
+    private static final Pattern PARSER_PATTERN = Pattern.compile( "\\.|-|," );
+
+    public static final VersionNumber ZERO = VersionNumber.of( 0, 0, 0 );
+
+    private static final Comparator<VersionNumber> COMPARATOR = Comparator
+            .comparingInt( VersionNumber::getMajor )
+            .thenComparingInt( VersionNumber::getMinor )
+            .thenComparingInt( VersionNumber::getPatch );
+
+    private final int major;
+    private final int minor;
+    private final int patch;
+
+    @Override
+    public int compareTo( final VersionNumber o )
+    {
+        return COMPARATOR.compare( this, o );
+    }
+
+    public String toString()
+    {
+        return prettyVersionString();
+    }
+
+    public String prettyVersionString()
+    {
+        return VERSION_PREFIX + major + VERSION_DELIMITER + minor + VERSION_DELIMITER + patch;
+    }
+
+    public static VersionNumber parse( final String input )
+    {
+        Objects.requireNonNull( input );
+
+        final String prefixStripped = input.toLowerCase().startsWith( VERSION_PREFIX.toLowerCase() )
+                ? input.substring( VERSION_PREFIX.length() )
+                : input;
+
+        final String[] split = PARSER_PATTERN.split( prefixStripped );
+
+        final int major = Integer.parseInt( split[0] );
+        final int minor = split.length > 1 ? Integer.parseInt( split[1] ) : 0;
+        final int patch = split.length > 2 ? Integer.parseInt( split[2] ) : 0;
+
+        return new VersionNumber( major, minor, patch );
+    }
+
+    public static VersionNumber of( final int major, final int minor, final int patch )
+    {
+        return new VersionNumber( major, minor, patch );
+    }
+}

+ 1 - 2
lib-data/src/main/java/password/pwm/bean/pub/PublicUserInfoBean.java

@@ -24,14 +24,13 @@ import lombok.Builder;
 import lombok.Value;
 import password.pwm.bean.PasswordStatus;
 
-import java.io.Serializable;
 import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 
 @Value
 @Builder
-public class PublicUserInfoBean implements Serializable
+public class PublicUserInfoBean implements PublishedBean
 {
     private String userDN;
     private String ldapProfile;

+ 11 - 7
server/src/test/java/password/pwm/config/PwmSettingMetaDataReaderTest.java → lib-data/src/main/java/password/pwm/bean/pub/PublishVersionBean.java

@@ -18,16 +18,20 @@
  * limitations under the License.
  */
 
-package password.pwm.config;
+package password.pwm.bean.pub;
 
-import org.junit.Test;
+import lombok.Value;
+import password.pwm.bean.VersionNumber;
 
-public class PwmSettingMetaDataReaderTest
+import java.util.Map;
+
+@Value
+public class PublishVersionBean implements PublishedBean
 {
-    @Test
-    public void initCache()
+    private final Map<VersionKey, VersionNumber> versions;
+
+    public enum VersionKey
     {
-        // just checking that no exceptions are thrown
-        PwmSettingMetaDataReader.initCache();
+        current,
     }
 }

+ 3 - 0
lib-data/src/main/java/password/pwm/bean/pub/PublishedBean.java

@@ -20,6 +20,9 @@
 
 package password.pwm.bean.pub;
 
+/**
+ * Marker interface to indicate a class is being used in public services and must retain backward compatibility.
+ */
 public interface PublishedBean
 {
 }

+ 1 - 1
lib-util/src/main/java/password/pwm/util/java/ImmutableByteArray.java → lib-data/src/main/java/password/pwm/data/ImmutableByteArray.java

@@ -18,7 +18,7 @@
  * limitations under the License.
  */
 
-package password.pwm.util.java;
+package password.pwm.data;
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;

+ 67 - 0
lib-data/src/test/java/password/pwm/bean/VersionNumberTest.java

@@ -0,0 +1,67 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 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.bean;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class VersionNumberTest
+{
+    @Test
+    public void testParse()
+    {
+        {
+            final VersionNumber versionNumber = VersionNumber.parse( "v1.2.3" );
+            Assert.assertEquals( VersionNumber.of( 1, 2, 3 ), versionNumber );
+        }
+
+        {
+            final VersionNumber versionNumber = VersionNumber.parse( "v1.2" );
+            Assert.assertEquals( VersionNumber.of( 1, 2, 0 ), versionNumber );
+        }
+    }
+
+    @Test
+    public void testComparator()
+    {
+        final List<VersionNumber> list = new ArrayList<>();
+
+        list.add( VersionNumber.of( 3, 2, 3  ) );
+        list.add( VersionNumber.of( 1, 4, 5  ) );
+        list.add( VersionNumber.of( 1, 3, 5  ) );
+        list.add( VersionNumber.of( 1, 3, 3  ) );
+        list.add( VersionNumber.of( 42, 2, 1  ) );
+        list.add( VersionNumber.of( 7, 0, 11  ) );
+
+        Collections.sort( list );
+
+        Assert.assertEquals( VersionNumber.of( 1, 3, 3 ), list.get( 0 ) );
+        Assert.assertEquals( VersionNumber.of( 1, 3, 5 ), list.get( 1 ) );
+        Assert.assertEquals( VersionNumber.of( 1, 4, 5 ), list.get( 2 ) );
+        Assert.assertEquals( VersionNumber.of( 3, 2, 3 ), list.get( 3 ) );
+        Assert.assertEquals( VersionNumber.of( 7, 0, 11 ), list.get( 4 ) );
+        Assert.assertEquals( VersionNumber.of( 42, 2, 1 ), list.get( 5 ) );
+    }
+}

+ 6 - 22
lib-util/pom.xml

@@ -37,28 +37,6 @@
     </profiles>
     <build>
         <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <version>3.0.0-M6</version>
-                <executions>
-                    <execution>
-                        <id>default-test</id>
-                        <goals>
-                            <goal>test</goal>
-                        </goals>
-                        <phase>test</phase>
-                        <configuration>
-                            <trimStackTrace>false</trimStackTrace>
-                            <skipTests>${skipTests}</skipTests>
-                            <excludes>
-                                <exclude>**/ExtendedTest*.java</exclude>
-                                <exclude>**/*ExtendedTest*.java</exclude>
-                            </excludes>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
@@ -89,6 +67,12 @@
     </build>
 
     <dependencies>
+        <dependency>
+            <groupId>org.pwm-project</groupId>
+            <artifactId>pwm-lib-data</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>

+ 11 - 2
server/src/main/java/password/pwm/util/ProgressInfo.java → lib-util/src/main/java/password/pwm/util/ProgressInfoCalculator.java

@@ -29,14 +29,14 @@ import java.math.MathContext;
 import java.text.NumberFormat;
 import java.time.Instant;
 
-public class ProgressInfo implements Serializable
+public class ProgressInfoCalculator implements Serializable
 {
     private final Instant startTime;
     private final Instant nowTime;
     private final long totalItems;
     private final long nowItems;
 
-    public ProgressInfo(
+    private ProgressInfoCalculator(
             final Instant startTime,
             final long totalItems,
             final long nowItems
@@ -48,6 +48,15 @@ public class ProgressInfo implements Serializable
         this.nowItems = nowItems;
     }
 
+    public static ProgressInfoCalculator createProgressInfo(
+            final Instant startTime,
+            final long totalItems,
+            final long nowItems
+    )
+    {
+        return new ProgressInfoCalculator( startTime, totalItems, nowItems );
+    }
+
     public Percent percentComplete( )
     {
         return Percent.of( nowItems, totalItems );

+ 34 - 0
lib-util/src/main/java/password/pwm/util/java/CollectionUtil.java

@@ -61,6 +61,18 @@ public class CollectionUtil
                 .collect( Collectors.toUnmodifiableList() );
     }
 
+    public static <V> Set<V> stripNulls( final Set<V> input )
+    {
+        if ( input == null )
+        {
+            return Collections.emptySet();
+        }
+
+        return input.stream()
+                .filter( Objects::nonNull )
+                .collect( Collectors.toUnmodifiableSet() );
+    }
+
     public static <K, V> Map<K, V> stripNulls( final Map<K, V> input )
     {
         if ( input == null )
@@ -189,4 +201,26 @@ public class CollectionUtil
                 LinkedHashMap::new
         );
     }
+
+    public static <T, K extends Enum<K>, U> Collector<T, ?, Map<K, U>> collectorToEnumMap(
+            final Class<K> keyClass,
+            final Function<? super T, ? extends K> keyMapper,
+            final Function<? super T, ? extends U> valueMapper
+    )
+    {
+        return Collectors.toMap(
+                keyMapper,
+                valueMapper,
+                ( key1, key2 ) ->
+                {
+                    throw new IllegalStateException( "Duplicate key " + key1 );
+                },
+                () -> new EnumMap<>( keyClass )
+        );
+    }
+
+    public static <E extends Enum<E>> Stream<E> enumStream( final Class<E> enumClass )
+    {
+        return EnumSet.allOf( enumClass ).stream();
+    }
 }

+ 0 - 46
lib-util/src/main/java/password/pwm/util/java/CrcChecksumInputStream.java

@@ -1,46 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2021 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.util.java;
-
-import java.io.InputStream;
-
-public class CrcChecksumInputStream extends CopyingInputStream
-{
-    private final CrcChecksumConsumer crcChecksumConsumer;
-
-    private CrcChecksumInputStream( final InputStream realStream, final CrcChecksumConsumer crcChecksumConsumer )
-    {
-        super( realStream, crcChecksumConsumer );
-        this.crcChecksumConsumer = crcChecksumConsumer;
-    }
-
-    public static CrcChecksumInputStream newChecksumInputStream( final InputStream wrappedStream )
-    {
-        final CrcChecksumConsumer crcChecksumConsumer = new CrcChecksumConsumer();
-        return new CrcChecksumInputStream( wrappedStream, crcChecksumConsumer );
-    }
-
-    public long checksum()
-    {
-        return crcChecksumConsumer.checksum();
-    }
-
-}

+ 0 - 45
lib-util/src/main/java/password/pwm/util/java/CrcChecksumOutputStream.java

@@ -1,45 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2021 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.util.java;
-
-import java.io.OutputStream;
-
-public class CrcChecksumOutputStream extends CopyingOutputStream
-{
-    private final CrcChecksumConsumer crcChecksumConsumer;
-
-    private CrcChecksumOutputStream( final OutputStream realStream, final CrcChecksumConsumer crcChecksumConsumer )
-    {
-        super( realStream, crcChecksumConsumer );
-        this.crcChecksumConsumer = crcChecksumConsumer;
-    }
-
-    public static CrcChecksumOutputStream newChecksumOutputStream( final OutputStream wrappedStream )
-    {
-        final CrcChecksumConsumer crcChecksumConsumer = new CrcChecksumConsumer();
-        return new CrcChecksumOutputStream( wrappedStream, crcChecksumConsumer );
-    }
-
-    public long checksum()
-    {
-        return crcChecksumConsumer.checksum();
-    }
-}

+ 16 - 11
lib-util/src/main/java/password/pwm/util/java/JavaHelper.java

@@ -21,6 +21,7 @@
 package password.pwm.util.java;
 
 import org.apache.commons.io.IOUtils;
+import password.pwm.data.ImmutableByteArray;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -42,7 +43,6 @@ import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
@@ -99,7 +99,7 @@ public class JavaHelper
             return Optional.empty();
         }
 
-        return EnumSet.allOf( enumClass ).stream().filter( match ).findFirst();
+        return CollectionUtil.enumStream( enumClass ).filter( match ).findFirst();
     }
 
     public static <E extends Enum<E>> Set<E> readEnumsFromPredicate( final Class<E> enumClass, final Predicate<E> match )
@@ -114,7 +114,7 @@ public class JavaHelper
             return Collections.emptySet();
         }
 
-        return EnumSet.allOf( enumClass ).stream().filter( match ).collect( Collectors.toUnmodifiableSet() );
+        return CollectionUtil.enumStream( enumClass ).filter( match ).collect( Collectors.toUnmodifiableSet() );
     }
 
     public static <E extends Enum<E>> Optional<E> readEnumFromString( final Class<E> enumClass, final String input )
@@ -215,12 +215,14 @@ public class JavaHelper
             return Optional.empty();
         }
         final StringWriter stringWriter = new StringWriter();
-        final InputStreamReader reader = new InputStreamReader( input, charset );
-        IOUtils.copyLarge( reader, stringWriter, 0, maximumLength );
-        final String value = stringWriter.toString();
-        return ( value.length() > 0 )
-                ? Optional.of( value )
-                : Optional.empty();
+        try ( InputStreamReader reader = new InputStreamReader( ThresholdInputStream.newThresholdInputStream( input, maximumLength ), charset ) )
+        {
+            IOUtils.copyLarge( reader, stringWriter, 0, maximumLength );
+            final String value = stringWriter.toString();
+            return ( value.length() > 0 )
+                    ? Optional.of( value )
+                    : Optional.empty();
+        }
     }
 
     public static void closeQuietly( final Closeable closable )
@@ -231,8 +233,11 @@ public class JavaHelper
     public static ImmutableByteArray copyToBytes( final InputStream inputStream, final int maxLength )
             throws IOException
     {
-        final byte[] bytes = IOUtils.toByteArray( inputStream, maxLength );
-        return ImmutableByteArray.of( bytes );
+        try ( InputStream limitedInputStream = ThresholdInputStream.newThresholdInputStream( inputStream, maxLength ) )
+        {
+            final byte[] bytes = IOUtils.toByteArray( limitedInputStream );
+            return ImmutableByteArray.of( bytes );
+        }
     }
 
     public static void copy( final String input, final OutputStream output, final Charset charset )

+ 10 - 0
lib-util/src/main/java/password/pwm/util/java/LazySupplier.java

@@ -20,6 +20,8 @@
 
 package password.pwm.util.java;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
 import java.util.function.Supplier;
 
 /**
@@ -51,6 +53,12 @@ public class LazySupplier<T> implements Supplier<T>
         return value;
     }
 
+    public boolean isSupplied()
+    {
+        return supplied;
+    }
+
+    @SuppressFBWarnings( "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" )
     public interface CheckedSupplier<T, E extends Exception>
     {
         T call() throws E;
@@ -61,6 +69,7 @@ public class LazySupplier<T> implements Supplier<T>
         return new LazyCheckedSupplier<>( lazySupplier );
     }
 
+    @SuppressFBWarnings( "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" )
     private static class LazyCheckedSupplier<T, E extends Exception> implements CheckedSupplier<T, E>
     {
         private boolean supplied = false;
@@ -73,6 +82,7 @@ public class LazySupplier<T> implements Supplier<T>
         }
 
         @Override
+        @SuppressFBWarnings( "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" )
         public T call() throws E
         {
             if ( !supplied )

+ 9 - 8
lib-util/src/main/java/password/pwm/util/java/StringUtil.java

@@ -20,7 +20,6 @@
 
 package password.pwm.util.java;
 
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.apache.commons.codec.binary.Base32;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.text.StringEscapeUtils;
@@ -279,7 +278,6 @@ public abstract class StringUtil
         return StringEscapeUtils.escapeXml11( input );
     }
 
-    @SuppressFBWarnings( "EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS" )
     public static String urlEncode( final String input )
     {
         try
@@ -288,12 +286,12 @@ public abstract class StringUtil
         }
         catch ( final UnsupportedEncodingException e )
         {
-            throw new RuntimeException( "unexpected error during url encoding: " + e.getMessage(), e );
+            throw new IllegalStateException( "unexpected error during url encoding: " + e.getMessage(), e );
         }
     }
 
-    @SuppressFBWarnings( "EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS" )
     public static String urlDecode( final String input )
+            throws IOException
     {
         try
         {
@@ -301,7 +299,7 @@ public abstract class StringUtil
         }
         catch ( final UnsupportedEncodingException e )
         {
-            throw new RuntimeException( "unexpected error during url decoding: " + e.getMessage(), e );
+            throw new IOException( "unexpected error during url decoding: " + e.getMessage(), e );
         }
     }
 
@@ -312,7 +310,6 @@ public abstract class StringUtil
         return new String( base32.encode( input ), STRING_UTIL_CHARSET.toString() );
     }
 
-    @SuppressFBWarnings( "EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS" )
     public static byte[] base64Decode( final CharSequence input, final StringUtil.Base64Options... options )
             throws IOException
     {
@@ -341,7 +338,6 @@ public abstract class StringUtil
         }
     }
 
-    @SuppressFBWarnings( "EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS" )
     public static String base64Encode( final byte[] input, final StringUtil.Base64Options... options )
     {
         final byte[] compressedBytes;
@@ -353,7 +349,7 @@ public abstract class StringUtil
             }
             catch ( final IOException e )
             {
-                throw new RuntimeException( "unexpected error during base64 decoding: " + e, e );
+                throw new IllegalStateException( "unexpected error during base64 decoding: " + e, e );
             }
         }
         else
@@ -546,6 +542,11 @@ public abstract class StringUtil
         return isEmpty( input ) || input.trim().length() == 0;
     }
 
+    public static boolean notTrimEmpty( final String input )
+    {
+        return input != null && !input.trim().isEmpty();
+    }
+
     public static String defaultString( final String input, final String defaultStr )
     {
         return StringUtils.defaultString( input, defaultStr );

+ 78 - 0
lib-util/src/main/java/password/pwm/util/java/ThresholdInputStream.java

@@ -0,0 +1,78 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 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.util.java;
+
+import org.apache.commons.io.input.ObservableInputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ThresholdInputStream extends ObservableInputStream
+{
+    private ThresholdInputStream( final InputStream inputStream, final Observer observer )
+    {
+        super( inputStream, observer );
+    }
+
+    public static ThresholdInputStream newThresholdInputStream( final InputStream inputStream, final long maxBytes )
+    {
+        return new ThresholdInputStream( inputStream, new ThresholdObserver( maxBytes ) );
+    }
+
+    private static class ThresholdObserver extends Observer
+    {
+        private final long maxBytes;
+
+        private long lengthCount = 0;
+
+        ThresholdObserver( final long maxBytes )
+        {
+            this.maxBytes = maxBytes;
+        }
+
+        private void checkLength( final long addLength )
+                throws IOException
+        {
+            if ( addLength > 0 )
+            {
+                this.lengthCount += addLength;
+                if ( lengthCount > maxBytes )
+                {
+                    throw new IOException( "maximum input length exceeded" );
+                }
+            }
+        }
+
+        @Override
+        public void data( final int input )
+                throws IOException
+        {
+            checkLength( 1 );
+        }
+
+        @Override
+        public void data( final byte[] input, final int offset, final int length )
+                throws IOException
+        {
+            checkLength( length - offset );
+        }
+    }
+}

+ 31 - 60
mvnw

@@ -19,7 +19,7 @@
 # ----------------------------------------------------------------------------
 
 # ----------------------------------------------------------------------------
-# Maven Start Up Batch script
+# Apache Maven Wrapper startup batch script, version 3.1.1
 #
 # Required ENV vars:
 # ------------------
@@ -27,7 +27,6 @@
 #
 # Optional ENV vars
 # -----------------
-#   M2_HOME - location of maven2's installed home dir
 #   MAVEN_OPTS - parameters passed to the Java VM when running Maven
 #     e.g. to debug Maven itself, use
 #       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@@ -62,9 +61,9 @@ case "`uname`" in
     # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
     if [ -z "$JAVA_HOME" ]; then
       if [ -x "/usr/libexec/java_home" ]; then
-        export JAVA_HOME="`/usr/libexec/java_home`"
+        JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME
       else
-        export JAVA_HOME="/Library/Java/Home"
+        JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
       fi
     fi
     ;;
@@ -76,36 +75,8 @@ if [ -z "$JAVA_HOME" ] ; then
   fi
 fi
 
-if [ -z "$M2_HOME" ] ; then
-  ## resolve links - $0 may be a link to maven's home
-  PRG="$0"
-
-  # need this for relative symlinks
-  while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-      PRG="$link"
-    else
-      PRG="`dirname "$PRG"`/$link"
-    fi
-  done
-
-  saveddir=`pwd`
-
-  M2_HOME=`dirname "$PRG"`/..
-
-  # make it fully qualified
-  M2_HOME=`cd "$M2_HOME" && pwd`
-
-  cd "$saveddir"
-  # echo Using m2 at $M2_HOME
-fi
-
 # For Cygwin, ensure paths are in UNIX format before anything is touched
 if $cygwin ; then
-  [ -n "$M2_HOME" ] &&
-    M2_HOME=`cygpath --unix "$M2_HOME"`
   [ -n "$JAVA_HOME" ] &&
     JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
   [ -n "$CLASSPATH" ] &&
@@ -114,8 +85,6 @@ fi
 
 # For Mingw, ensure paths are in UNIX format before anything is touched
 if $mingw ; then
-  [ -n "$M2_HOME" ] &&
-    M2_HOME="`(cd "$M2_HOME"; pwd)`"
   [ -n "$JAVA_HOME" ] &&
     JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
 fi
@@ -163,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then
   echo "Warning: JAVA_HOME environment variable is not set."
 fi
 
-CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
-
 # traverses directory structure from process work directory to filesystem root
 # first directory with .mvn subdirectory is considered project base directory
 find_maven_basedir() {
-
   if [ -z "$1" ]
   then
     echo "Path not specified to find_maven_basedir"
@@ -188,7 +154,7 @@ find_maven_basedir() {
     fi
     # end of workaround
   done
-  echo "${basedir}"
+  printf '%s' "$(cd "$basedir"; pwd)"
 }
 
 # concatenates all lines of a file
@@ -198,11 +164,16 @@ concat_lines() {
   fi
 }
 
-BASE_DIR=`find_maven_basedir "$(pwd)"`
+BASE_DIR=$(find_maven_basedir "$(dirname $0)")
 if [ -z "$BASE_DIR" ]; then
   exit 1;
 fi
 
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+
 ##########################################################################################
 # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
 # This allows using the maven wrapper in projects that prohibit checking in binary data.
@@ -216,16 +187,16 @@ else
       echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
     fi
     if [ -n "$MVNW_REPOURL" ]; then
-      jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+      wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
     else
-      jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+      wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
     fi
     while IFS="=" read key value; do
-      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;;
       esac
     done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
     if [ "$MVNW_VERBOSE" = true ]; then
-      echo "Downloading from: $jarUrl"
+      echo "Downloading from: $wrapperUrl"
     fi
     wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
     if $cygwin; then
@@ -233,42 +204,49 @@ else
     fi
 
     if command -v wget > /dev/null; then
+        QUIET="--quiet"
         if [ "$MVNW_VERBOSE" = true ]; then
           echo "Found wget ... using wget"
+          QUIET=""
         fi
         if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
-            wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+            wget $QUIET "$wrapperUrl" -O "$wrapperJarPath"
         else
-            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+            wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath"
         fi
+        [ $? -eq 0 ] || rm -f "$wrapperJarPath"
     elif command -v curl > /dev/null; then
+        QUIET="--silent"
         if [ "$MVNW_VERBOSE" = true ]; then
           echo "Found curl ... using curl"
+          QUIET=""
         fi
         if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
-            curl -o "$wrapperJarPath" "$jarUrl" -f
+            curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L
         else
-            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+            curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L
         fi
-
+        [ $? -eq 0 ] || rm -f "$wrapperJarPath"
     else
         if [ "$MVNW_VERBOSE" = true ]; then
           echo "Falling back to using Java to download"
         fi
-        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class"
         # For Cygwin, switch paths to Windows format before running javac
         if $cygwin; then
+          javaSource=`cygpath --path --windows "$javaSource"`
           javaClass=`cygpath --path --windows "$javaClass"`
         fi
-        if [ -e "$javaClass" ]; then
-            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+        if [ -e "$javaSource" ]; then
+            if [ ! -e "$javaClass" ]; then
                 if [ "$MVNW_VERBOSE" = true ]; then
                   echo " - Compiling MavenWrapperDownloader.java ..."
                 fi
                 # Compiling the Java class
-                ("$JAVA_HOME/bin/javac" "$javaClass")
+                ("$JAVA_HOME/bin/javac" "$javaSource")
             fi
-            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+            if [ -e "$javaClass" ]; then
                 # Running the downloader
                 if [ "$MVNW_VERBOSE" = true ]; then
                   echo " - Running MavenWrapperDownloader.java ..."
@@ -282,16 +260,10 @@ fi
 # End of extension
 ##########################################################################################
 
-export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
-if [ "$MVNW_VERBOSE" = true ]; then
-  echo $MAVEN_PROJECTBASEDIR
-fi
 MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
 
 # For Cygwin, switch paths to Windows format before running java
 if $cygwin; then
-  [ -n "$M2_HOME" ] &&
-    M2_HOME=`cygpath --path --windows "$M2_HOME"`
   [ -n "$JAVA_HOME" ] &&
     JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
   [ -n "$CLASSPATH" ] &&
@@ -311,6 +283,5 @@ exec "$JAVACMD" \
   $MAVEN_OPTS \
   $MAVEN_DEBUG_OPTS \
   -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
-  "-Dmaven.home=${M2_HOME}" \
   "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
   ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 6 - 7
mvnw.cmd

@@ -18,13 +18,12 @@
 @REM ----------------------------------------------------------------------------
 
 @REM ----------------------------------------------------------------------------
-@REM Maven Start Up Batch script
+@REM Apache Maven Wrapper startup batch script, version 3.1.1
 @REM
 @REM Required ENV vars:
 @REM JAVA_HOME - location of a JDK home dir
 @REM
 @REM Optional ENV vars
-@REM M2_HOME - location of maven2's installed home dir
 @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
 @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
 @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@@ -120,10 +119,10 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
 set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
 set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
 
-set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
 
 FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
-    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+    IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
 )
 
 @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@@ -134,11 +133,11 @@ if exist %WRAPPER_JAR% (
     )
 ) else (
     if not "%MVNW_REPOURL%" == "" (
-        SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+        SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar"
     )
     if "%MVNW_VERBOSE%" == "true" (
         echo Couldn't find %WRAPPER_JAR%, downloading it ...
-        echo Downloading from: %DOWNLOAD_URL%
+        echo Downloading from: %WRAPPER_URL%
     )
 
     powershell -Command "&{"^
@@ -146,7 +145,7 @@ if exist %WRAPPER_JAR% (
 		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
 		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
 		"}"^
-		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
 		"}"
     if "%MVNW_VERBOSE%" == "true" (
         echo Finished downloading %WRAPPER_JAR%

+ 1 - 1
onejar/pom.xml

@@ -16,7 +16,7 @@
     <name>PWM Password Self Service: Executable Server JAR</name>
 
     <properties>
-        <tomcat.version>9.0.62</tomcat.version>
+        <tomcat.version>9.0.64</tomcat.version>
     </properties>
 
     <build>

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

@@ -114,7 +114,7 @@ public class ArgumentParser
     private Map<Argument, String> mapFromProperties( final String filename ) throws ArgumentParserException
     {
         final Properties props = new Properties();
-        try ( InputStream is = new FileInputStream( new File( filename ) ) )
+        try ( InputStream is = new FileInputStream( filename ) )
         {
             props.load( is );
         }

+ 2 - 1
onejar/src/main/java/password/pwm/onejar/TomcatOnejarRunner.java

@@ -255,8 +255,9 @@ public class TomcatOnejarRunner
     {
         final String envVarPrefix = Resource.envVarPrefix.getValue();
         System.setProperty( envVarPrefix + "_APPLICATIONPATH", onejarConfig.getApplicationPath().getAbsolutePath() );
-        System.setProperty( envVarPrefix + "_APPLICATIONFLAGS", "ManageHttps" );
+        System.setProperty( envVarPrefix + "_APPLICATIONFLAGS", "[\"ManageHttps\",\"Onejar\"]" );
         System.setProperty( envVarPrefix + "_APPLICATIONPARAMFILE", onejarConfig.getPwmAppPropertiesFile().getAbsolutePath() );
+        System.setProperty( "ONEJAR_ENV", "TRUE" );
     }
 
     private void outputPwmAppProperties( final OnejarConfig onejarConfig ) throws IOException

+ 51 - 8
pom.xml

@@ -214,7 +214,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-enforcer-plugin</artifactId>
-                <version>3.0.0</version>
+                <version>3.1.0</version>
                 <executions>
                     <execution>
                         <id>enforce-maven</id>
@@ -255,7 +255,7 @@
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
-                        <version>10.2</version>
+                        <version>10.3.1</version>
                     </dependency>
                 </dependencies>
                 <executions>
@@ -334,12 +334,12 @@
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>4.6.0.0</version>
+                <version>4.7.0.0</version>
                 <dependencies>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
-                        <version>4.6.0</version>
+                        <version>4.7.1</version>
                     </dependency>
                 </dependencies>
                 <configuration>
@@ -397,7 +397,7 @@
             <plugin>
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>7.1.0</version>
+                <version>7.1.1</version>
                 <executions>
                     <execution>
                         <goals>
@@ -444,9 +444,52 @@
                 <artifactId>maven-dependency-plugin</artifactId>
                 <version>3.3.0</version>
             </plugin>
+
         </plugins>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>3.0.0-M7</version>
+                    <executions>
+                        <execution>
+                            <id>default-test</id>
+                            <goals>
+                                <goal>test</goal>
+                            </goals>
+                            <phase>test</phase>
+                            <configuration>
+                                <trimStackTrace>false</trimStackTrace>
+                                <skipTests>${skipTests}</skipTests>
+                                <excludes>
+                                    <exclude>**/ExtendedTest*.java</exclude>
+                                    <exclude>**/*ExtendedTest*.java</exclude>
+                                </excludes>
+                            </configuration>
+                        </execution>
+                        <execution>
+                            <id>extended-test</id>
+                            <goals>
+                                <goal>test</goal>
+                            </goals>
+                            <phase>test</phase>
+                            <configuration>
+                                <skipTests>${skipExtendedTests}</skipTests>
+                                <includes>
+                                    <include>**/ExtendedTest*.java</include>
+                                    <include>**/*ExtendedTest*.java</include>
+                                </includes>
+                            </configuration>
+                        </execution>
+                    </executions>
+                </plugin>
+            </plugins>
+
+        </pluginManagement>
     </build>
 
+
     <!-- common dependencies -->
     <dependencies>
         <dependency>
@@ -458,7 +501,7 @@
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>4.6.0</version>
+            <version>4.7.1</version>
             <scope>provided</scope>
         </dependency>
 
@@ -472,13 +515,13 @@
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
-            <version>4.5.1</version>
+            <version>4.6.1</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
-            <version>3.22.0</version>
+            <version>3.23.1</version>
             <scope>test</scope>
         </dependency>
         <dependency>

+ 2 - 2
rest-test-service/pom.xml

@@ -82,9 +82,9 @@
             <version>2.9.0</version>
         </dependency>
         <dependency>
-            <groupId>org.apache.commons</groupId>
+            <groupId>commons-io</groupId>
             <artifactId>commons-io</artifactId>
-            <version>1.3.2</version>
+            <version>2.11.0</version>
         </dependency>
         <dependency>
             <groupId>org.pwm-project</groupId>

+ 3 - 49
server/pom.xml

@@ -38,42 +38,6 @@
     </profiles>
     <build>
         <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <version>3.0.0-M6</version>
-                <executions>
-                    <execution>
-                        <id>default-test</id>
-                        <goals>
-                            <goal>test</goal>
-                        </goals>
-                        <phase>test</phase>
-                        <configuration>
-                            <trimStackTrace>false</trimStackTrace>
-                            <skipTests>${skipTests}</skipTests>
-                            <excludes>
-                                <exclude>**/ExtendedTest*.java</exclude>
-                                <exclude>**/*ExtendedTest*.java</exclude>
-                            </excludes>
-                        </configuration>
-                    </execution>
-                    <execution>
-                        <id>extended-test</id>
-                        <goals>
-                            <goal>test</goal>
-                        </goals>
-                        <phase>test</phase>
-                        <configuration>
-                            <skipTests>${skipExtendedTests}</skipTests>
-                            <includes>
-                                <include>**/ExtendedTest*.java</include>
-                                <include>**/*ExtendedTest*.java</include>
-                            </includes>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
             <plugin>
                 <artifactId>maven-resources-plugin</artifactId>
                 <version>3.2.0</version>
@@ -256,7 +220,7 @@
         <dependency>
             <groupId>ch.qos.reload4j</groupId>
             <artifactId>reload4j</artifactId>
-            <version>1.2.20</version>
+            <version>1.2.21</version>
         </dependency>
         <dependency>
             <groupId>org.jasig.cas.client</groupId>
@@ -279,16 +243,6 @@
             <artifactId>bcpkix-jdk15on</artifactId>
             <version>1.70</version>
         </dependency>
-        <dependency>
-            <groupId>jaxen</groupId>
-            <artifactId>jaxen</artifactId>
-            <version>1.2.0</version>
-        </dependency>
-        <dependency>
-            <groupId>org.jdom</groupId>
-            <artifactId>jdom2</artifactId>
-            <version>2.0.6.1</version>
-        </dependency>
         <dependency>
             <groupId>org.xeustechnologies</groupId>
             <artifactId>jcl-core</artifactId>
@@ -318,12 +272,12 @@
         <dependency>
             <groupId>org.webjars</groupId>
             <artifactId>webjars-locator-core</artifactId>
-            <version>0.50</version>
+            <version>0.51</version>
         </dependency>
         <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
-            <version>3.1.0</version>
+            <version>3.1.1</version>
         </dependency>
         <dependency>
             <groupId>com.nulab-inc</groupId>

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

@@ -37,8 +37,8 @@ public enum AppAttribute
     HTTPS_SELF_CERT( "https.selfCert" ),
     CONFIG_LOGIN_HISTORY( "config.loginHistory" ),
     LOCALDB_LOGGER_STORAGE_FORMAT( "localdb.logger.storage.format" ),
-
-    TELEMETRY_LAST_PUBLISH_TIMESTAMP( "telemetry.lastPublish.timestamp" );
+    TELEMETRY_LAST_PUBLISH_TIMESTAMP( "telemetry.lastPublish.timestamp" ),
+    VERSION_CHECK_CACHE( "versionCheckInfoCache" ),;
 
     private final String key;
 

+ 14 - 1
server/src/main/java/password/pwm/AppProperty.java

@@ -20,6 +20,10 @@
 
 package password.pwm;
 
+import password.pwm.util.java.CollectionUtil;
+
+import java.util.Objects;
+import java.util.Optional;
 import java.util.ResourceBundle;
 
 /**
@@ -223,7 +227,6 @@ public enum AppProperty
     LDAP_CHAI_SETTINGS                              ( "ldap.chaiSettings" ),
     LDAP_PROXY_CONNECTION_PER_PROFILE               ( "ldap.proxy.connectionsPerProfile" ),
     LDAP_PROXY_MAX_CONNECTIONS                      ( "ldap.proxy.maxConnections" ),
-    LDAP_PROXY_USE_THREAD_LOCAL                     ( "ldap.proxy.useThreadLocal" ),
     LDAP_PROXY_IDLE_THREAD_LOCAL_TIMEOUT_MS         ( "ldap.proxy.idleThreadLocal.timeoutMS" ),
     LDAP_EXTENSIONS_NMAS_ENABLE                     ( "ldap.extensions.nmas.enable" ),
     LDAP_CONNECTION_TIMEOUT                         ( "ldap.connection.timeoutMS" ),
@@ -375,6 +378,9 @@ public enum AppProperty
 
     /** Regular expression to be used for matching URLs to be shortened by the URL Shortening Service Class. */
     URL_SHORTNER_URL_REGEX                          ( "urlshortener.url.regex" ),
+    VERSION_CHECK_URL                               ( "versionCheck.url" ),
+    VERSION_CHECK_CHECK_INTERVAL_SECONDS            ( "versionCheck.checkIntervalSeconds" ),
+    VERSION_CHECK_CHECK_INTERVAL_ERROR_SECONDS      ( "versionCheck.checkIntervalErrorSeconds" ),
     WORDLIST_BUILTIN_PATH                           ( "wordlist.builtin.path" ),
     WORDLIST_CHAR_LENGTH_MAX                        ( "wordlist.maxCharLength" ),
     WORDLIST_CHAR_LENGTH_MIN                        ( "wordlist.minCharLength" ),
@@ -430,4 +436,11 @@ public enum AppProperty
     {
         return ResourceBundle.getBundle( AppProperty.class.getName() ).getString( key );
     }
+
+    public static Optional<AppProperty> forKey( final String key )
+    {
+        return CollectionUtil.enumStream( AppProperty.class )
+                .filter( loopProperty -> Objects.equals( loopProperty.getKey(), key ) )
+                .findFirst();
+    }
 }

+ 9 - 16
server/src/main/java/password/pwm/PwmAboutProperty.java

@@ -23,9 +23,10 @@ package password.pwm;
 import lombok.Value;
 import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Display;
-import password.pwm.ldap.LdapConnectionService;
+import password.pwm.ldap.LdapDomainService;
 import password.pwm.svc.db.DatabaseService;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
@@ -37,17 +38,13 @@ import java.nio.charset.Charset;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.util.Collections;
-import java.util.EnumMap;
-import java.util.EnumSet;
 import java.util.Map;
 import java.util.Optional;
 import java.util.TreeMap;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 
 public enum PwmAboutProperty
 {
-
     app_version( "App Version", pwmApplication -> PwmConstants.SERVLET_VERSION ),
     app_chaiApiVersion( "App Chai Version", pwmApplication -> PwmConstants.CHAI_API_VERSION ),
     app_currentTime( "App Current Time", pwmApplication -> format( Instant.now() ) ),
@@ -56,8 +53,7 @@ public enum PwmAboutProperty
     app_siteUrl( "App Site URL", pwmApplication -> pwmApplication.getConfig().readSettingAsString( PwmSetting.PWM_SITE_URL ) ),
     app_instanceID( "App InstanceID", PwmApplication::getInstanceID ),
     app_trialMode( null, pwmApplication -> Boolean.toString( PwmConstants.TRIAL_MODE ) ),
-    app_mode_appliance( null, pwmApplication -> Boolean.toString( pwmApplication.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.Appliance ) ) ),
-    app_mode_docker( null, pwmApplication -> Boolean.toString( pwmApplication.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.Docker ) ) ),
+    app_deployment_type( null, pwmApplication -> pwmApplication.getPwmEnvironment().getDeploymentPlatform().name() ),
     app_mode_manageHttps( null, pwmApplication -> Boolean.toString( pwmApplication.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.ManageHttps ) ) ),
     app_applicationPath( null, pwmApplication -> pwmApplication.getPwmEnvironment().getApplicationPath().getAbsolutePath() ),
     app_environmentFlags( null, pwmApplication -> StringUtil.collectionToString( pwmApplication.getPwmEnvironment().getFlags() ) ),
@@ -77,10 +73,10 @@ public enum PwmAboutProperty
     app_configurationRestartCounter( null, pwmApplication -> Integer.toString( pwmApplication.getPwmEnvironment().getContextManager().getRestartCount() ) ),
     app_secureBlockAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultBlockAlgorithm().getLabel() ),
     app_secureHashAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultHashAlgorithm().toString() ),
-    app_ldapProfileCount( null, pwmApplication -> Integer.toString( LdapConnectionService.totalLdapProfileCount( pwmApplication ) ) ),
-    app_ldapConnectionCount( null, pwmApplication -> Long.toString( LdapConnectionService.totalLdapConnectionCount( pwmApplication ) ) ),
+    app_ldapProfileCount( null, pwmApplication -> Integer.toString( LdapDomainService.totalLdapProfileCount( pwmApplication ) ) ),
+    app_ldapConnectionCount( null, pwmApplication -> Long.toString( LdapDomainService.totalLdapConnectionCount( pwmApplication ) ) ),
     app_activeSessionCount( "App Active Session Count", pwmApplication -> Integer.toString( pwmApplication.getSessionTrackService().sessionCount() ) ),
-    app_activeRequestCount( "App Active Request Count", pwmApplication -> Integer.toString( pwmApplication.getActiveServletRequests().get() ) ),
+    app_activeRequestCount( "App Active Request Count", pwmApplication -> Integer.toString( pwmApplication.getTotalActiveServletRequests() ) ),
     app_definedDomainCount( "App Defined Domain Count", pwmApplication -> Integer.toString( pwmApplication.domains().size() ) ),
 
     build_Time( "Build Time", pwmApplication -> PwmConstants.BUILD_TIME ),
@@ -141,15 +137,12 @@ public enum PwmAboutProperty
             final PwmApplication pwmApplication
     )
     {
-        return Collections.unmodifiableMap( EnumSet.allOf( PwmAboutProperty.class )
-                .stream()
+        return Collections.unmodifiableMap( CollectionUtil.enumStream( PwmAboutProperty.class )
                 .map( aboutProp -> new Pair<>( aboutProp, readAboutValue( pwmApplication, aboutProp ) ) )
                 .filter( entry -> entry.getValue().isPresent() )
-                .collect( Collectors.toMap(
+                .collect( CollectionUtil.collectorToEnumMap( PwmAboutProperty.class,
                         Pair::getKey,
-                        entry -> entry.getValue().get(),
-                        ( k, k2 ) -> k,
-                        () -> new EnumMap<>( PwmAboutProperty.class ) ) ) );
+                        entry -> entry.getValue().get() ) ) );
 
     }
 

+ 40 - 47
server/src/main/java/password/pwm/PwmApplication.java

@@ -25,7 +25,6 @@ import password.pwm.bean.DomainID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.config.AppConfig;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.PwmSettingMetaDataReader;
 import password.pwm.config.PwmSettingScope;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -67,13 +66,10 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.logging.LocalDBLogger;
-import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogManager;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmRandom;
 
 import java.io.File;
-import java.io.Serializable;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -87,7 +83,6 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -97,10 +92,8 @@ public class PwmApplication
 
     static final int DOMAIN_STARTUP_THREADS = 10;
 
-    private final AtomicInteger activeServletRequests = new AtomicInteger( 0 );
-
     private volatile Map<DomainID, PwmDomain> domains = new HashMap<>();
-    private String runtimeNonce = PwmRandom.getInstance().randomUUID().toString();
+    private String runtimeNonce = PwmApplicationUtil.makeRuntimeNonce();
 
     private final SessionLabel sessionLabel;
     private final PwmServiceManager pwmServiceManager;
@@ -145,7 +138,7 @@ public class PwmApplication
 
     private void initRuntimeNonce()
     {
-        runtimeNonce = PwmRandom.getInstance().randomUUID().toString();
+        runtimeNonce = PwmApplicationUtil.makeRuntimeNonce();
     }
 
     private void initialize()
@@ -218,10 +211,6 @@ public class PwmApplication
         installTime = fetchInstallDate( startupTime );
         LOGGER.debug( sessionLabel, () -> "this application instance first installed on " + StringUtil.toIsoDate( installTime ) );
 
-        LOGGER.debug( sessionLabel, () -> "application environment flags: " + JsonFactory.get().serializeCollection( pwmEnvironment.getFlags() ) );
-        LOGGER.debug( sessionLabel, () -> "application environment parameters: "
-                + JsonFactory.get().serializeMap( pwmEnvironment.getParameters(), PwmEnvironment.ApplicationParameter.class, String.class ) );
-
         pwmScheduler = new PwmScheduler( this );
 
         domains = PwmDomainUtil.createDomainInstances( this );
@@ -242,14 +231,13 @@ public class PwmApplication
 
             pwmScheduler.immediateExecuteRunnableInNewThread( this::postInitTasks, sessionLabel, this.getClass().getSimpleName() + " postInit tasks" );
         }
-
     }
 
     public void reInit( final PwmEnvironment pwmEnvironment )
             throws PwmException
     {
         final Instant startTime = Instant.now();
-        LOGGER.debug( sessionLabel, () -> "beginning application restart" );
+        LOGGER.trace( sessionLabel, () -> "beginning application restart" );
         final AppConfig oldConfig = this.pwmEnvironment.getConfig();
         this.pwmEnvironment = pwmEnvironment;
         final AppConfig newConfig = this.pwmEnvironment.getConfig();
@@ -258,12 +246,16 @@ public class PwmApplication
         {
             processPwmAppRestart( );
         }
+        else
+        {
+            LOGGER.debug( sessionLabel, () -> "no system-level settings have been changed, restart of system services is not required" );
+        }
 
-        domains = PwmDomainUtil.reInitDomains( this, newConfig, oldConfig );
+        PwmDomainUtil.reInitDomains( this, newConfig, oldConfig );
 
-        runtimeNonce = PwmRandom.getInstance().randomUUID().toString();
+        runtimeNonce = PwmApplicationUtil.makeRuntimeNonce();
 
-        LOGGER.debug( sessionLabel, () -> "completed application restart", () -> TimeDuration.fromCurrent( startTime ) );
+        LOGGER.debug( sessionLabel, () -> "completed application restart with " + domains().size() + " domains", TimeDuration.fromCurrent( startTime ) );
     }
 
     private void postInitTasks()
@@ -282,32 +274,22 @@ public class PwmApplication
             LOGGER.warn( sessionLabel, () -> "error while clearing config manager-intruder-username from intruder table: " + e.getMessage() );
         }
 
-        if ( !pwmEnvironment.isInternalRuntimeInstance() )
-        {
-            PwmApplicationUtil.outputKeystore( this );
-            PwmApplicationUtil.outputTomcatConf( this );
-        }
+        PwmApplicationUtil.outputKeystore( this );
+        PwmApplicationUtil.outputTomcatConf( this );
 
-        if ( Boolean.parseBoolean( getConfig().readAppProperty( AppProperty.LOGGING_OUTPUT_CONFIGURATION ) )
-                && LOGGER.isEnabled( PwmLogLevel.TRACE )
-        )
-        {
-            PwmApplicationUtil.outputApplicationInfoToLog( this );
-            PwmApplicationUtil.outputConfigurationToLog( this, DomainID.systemId() );
-            PwmApplicationUtil.outputNonDefaultPropertiesToLog( this );
-        }
+        LOGGER.debug( sessionLabel, () -> "application environment flags: " + StringUtil.collectionToString( pwmEnvironment.getFlags() ) );
+        LOGGER.debug( sessionLabel, () -> "application environment parameters: "
+                + StringUtil.mapToString( pwmEnvironment.getParameters() ) );
+
+        PwmApplicationUtil.outputApplicationInfoToLog( this );
+        PwmApplicationUtil.outputConfigurationToLog( this, DomainID.systemId() );
+        PwmApplicationUtil.outputNonDefaultPropertiesToLog( this );
 
         MBeanUtility.registerMBean( this );
 
-        getPwmScheduler().immediateExecuteRunnableInNewThread( () ->
-                {
-                    PwmSettingMetaDataReader.initCache();
-                    UserAgentUtils.initializeCache();
-                },
-                sessionLabel,
-                "initialize useragent cache" );
+        UserAgentUtils.initializeCache();
 
-        LOGGER.trace( sessionLabel, () -> "completed post init tasks", () -> TimeDuration.fromCurrent( startTime ) );
+        LOGGER.trace( sessionLabel, () -> "completed post init tasks", TimeDuration.fromCurrent( startTime ) );
     }
 
     public static PwmApplication createPwmApplication( final PwmEnvironment pwmEnvironment )
@@ -326,6 +308,11 @@ public class PwmApplication
         return domains;
     }
 
+    protected void setDomains( final Map<DomainID, PwmDomain> domains )
+    {
+        this.domains = Map.copyOf( domains );
+    }
+
     public AppConfig getConfig()
     {
         return pwmEnvironment.getConfig();
@@ -336,9 +323,10 @@ public class PwmApplication
         return pwmEnvironment;
     }
 
-    public AtomicInteger getActiveServletRequests( )
+    public int getTotalActiveServletRequests( )
     {
-        return activeServletRequests;
+        return domains().values().stream().map( domain -> domain.getActiveServletRequests().get() )
+                .reduce( 0, Integer::sum );
     }
 
     public PwmApplicationMode getApplicationMode( )
@@ -389,20 +377,24 @@ public class PwmApplication
 
         MBeanUtility.unregisterMBean( this );
 
-        pwmServiceManager.shutdownAllServices();
-
         try
         {
             final List<Callable<Object>> callables = domains.values().stream()
                     .map( pwmDomain -> Executors.callable( pwmDomain::shutdown ) )
                     .collect( Collectors.toList() );
+
+            final Instant startDomainShutdown = Instant.now();
+            LOGGER.trace( sessionLabel, () -> "beginning shutdown of " + callables.size() + " running domains" );
             pwmScheduler.executeImmediateThreadPerJobAndAwaitCompletion( DOMAIN_STARTUP_THREADS, callables, sessionLabel, PwmApplication.class );
+            LOGGER.trace( sessionLabel, () -> "shutdown of " + callables.size() + " running domains completed", TimeDuration.fromCurrent( startDomainShutdown ) );
         }
         catch ( final PwmUnrecoverableException e )
         {
             LOGGER.error( sessionLabel, () -> "error shutting down domain services: " + e.getMessage(), e );
         }
 
+        pwmServiceManager.shutdownAllServices();
+
         if ( localDBLogger != null )
         {
             try
@@ -426,7 +418,7 @@ public class PwmApplication
                 final TimeDuration closeLocalDbDuration = TimeDuration.fromCurrent( startCloseDbTime );
                 if ( closeLocalDbDuration.isLongerThan( TimeDuration.SECONDS_10 ) )
                 {
-                    LOGGER.info( sessionLabel, () -> "completed close of LocalDB", () -> closeLocalDbDuration );
+                    LOGGER.info( sessionLabel, () -> "completed close of LocalDB", closeLocalDbDuration );
                 }
             }
             catch ( final Exception e )
@@ -444,7 +436,7 @@ public class PwmApplication
         pwmScheduler.shutdown();
 
         LOGGER.info( sessionLabel, () -> PwmConstants.PWM_APP_NAME + " " + PwmConstants.SERVLET_VERSION
-                + " closed for bidness, cya!", () -> TimeDuration.fromCurrent( startTime ) );
+                + " closed for bidness, cya!", TimeDuration.fromCurrent( startTime ) );
     }
 
     public String getInstanceID( )
@@ -453,7 +445,7 @@ public class PwmApplication
     }
 
 
-    public <T extends Serializable> Optional<T> readAppAttribute( final AppAttribute appAttribute, final Class<T> returnClass )
+    public <T> Optional<T> readAppAttribute( final AppAttribute appAttribute, final Class<T> returnClass )
     {
         final LocalDB localDB = getLocalDB();
 
@@ -568,7 +560,7 @@ public class PwmApplication
     }
 
 
-    public void writeAppAttribute( final AppAttribute appAttribute, final Serializable value )
+    public void writeAppAttribute( final AppAttribute appAttribute, final Object value )
     {
         final LocalDB localDB = getLocalDB();
 
@@ -880,4 +872,5 @@ public class PwmApplication
         return conditions.stream().allMatch( ( c ) -> c.matches( this ) );
     }
 
+
 }

+ 45 - 25
server/src/main/java/password/pwm/PwmApplicationUtil.java

@@ -285,6 +285,11 @@ class PwmApplicationUtil
 
     static void outputConfigurationToLog( final PwmApplication pwmApplication, final DomainID domainID )
     {
+        if ( !checkIfOutputDumpingEnabled( pwmApplication ) )
+        {
+            return;
+        }
+
         final Instant startTime = Instant.now();
 
         final Function<Map.Entry<String, String>, String> valueFormatter = entry ->
@@ -310,46 +315,61 @@ class PwmApplicationUtil
 
         final long itemCount = debugStrings.size();
         LOGGER.trace( pwmApplication.getSessionLabel(), () -> "--end current configuration output of " + itemCount + " items --",
-                () -> TimeDuration.fromCurrent( startTime ) );
+                TimeDuration.fromCurrent( startTime ) );
     }
 
     static void outputNonDefaultPropertiesToLog( final PwmApplication pwmApplication )
     {
-        final Instant startTime = Instant.now();
+        final Map<String, String> data = pwmApplication.getConfig().readAllNonDefaultAppProperties().entrySet().stream()
+                .collect( CollectionUtil.collectorToLinkedMap(
+                        entry -> "AppProperty: " + entry.getKey().getKey(),
+                        Map.Entry::getValue ) );
 
-        final Map<AppProperty, String> nonDefaultProperties = pwmApplication.getConfig().readAllNonDefaultAppProperties();
-        if ( !CollectionUtil.isEmpty( nonDefaultProperties ) )
-        {
-            LOGGER.trace( pwmApplication.getSessionLabel(), () -> "--begin non-default app properties output--" );
-            nonDefaultProperties.entrySet().stream()
-                    .map( entry -> "AppProperty: " + entry.getKey().getKey() + " -> " + entry.getValue() )
-                    .map( s -> ( Supplier<CharSequence> ) () -> s )
-                    .forEach( s -> LOGGER.trace( pwmApplication.getSessionLabel(), s ) );
-            LOGGER.trace( pwmApplication.getSessionLabel(), () -> "--end non-default app properties output--", () -> TimeDuration.fromCurrent( startTime ) );
-        }
-        else
-        {
-            LOGGER.trace( pwmApplication.getSessionLabel(), () -> "no non-default app properties in configuration" );
-        }
+        outputMapToLog( pwmApplication, data, "non-default app properties" );
     }
 
     static void outputApplicationInfoToLog( final PwmApplication pwmApplication )
     {
-        final Instant startTime = Instant.now();
+        final Map<String, String> data = PwmAboutProperty.makeInfoBean( pwmApplication ).entrySet().stream()
+                .collect( CollectionUtil.collectorToLinkedMap(
+                        entry -> "AboutProperty: " + entry.getKey().getLabel(),
+                        Map.Entry::getValue ) );
+
+        outputMapToLog( pwmApplication, data, "about property info" );
+    }
+
+    private static void outputMapToLog(
+            final PwmApplication pwmApplication,
+            final Map<String, String> input,
+            final String label
+    )
+    {
+        LOGGER.trace( pwmApplication.getSessionLabel(), () -> "--begin " + label + "--" );
 
-        final Map<PwmAboutProperty, String> aboutProperties = PwmAboutProperty.makeInfoBean( pwmApplication );
-        if ( !CollectionUtil.isEmpty( aboutProperties ) )
+        if ( !CollectionUtil.isEmpty( input ) )
         {
-            LOGGER.trace( pwmApplication.getSessionLabel(), () -> "--begin application info--" );
-            aboutProperties.entrySet().stream()
-                    .map( entry -> "AppProperty: " + entry.getKey().getLabel() + " -> " + entry.getValue() )
-                    .map( s -> ( Supplier<CharSequence> ) () -> s )
+            final String separator = " -> ";
+            input.entrySet().stream()
+                    .map( entry -> ( Supplier<CharSequence> ) () -> entry.getKey() + separator + entry.getValue() )
                     .forEach( s -> LOGGER.trace( pwmApplication.getSessionLabel(), s ) );
-            LOGGER.trace( pwmApplication.getSessionLabel(), () -> "--end application info--", () -> TimeDuration.fromCurrent( startTime ) );
         }
         else
         {
-            LOGGER.trace( pwmApplication.getSessionLabel(), () -> "no non-default app properties in configuration" );
+            LOGGER.trace( pwmApplication.getSessionLabel(), () -> "no " + label + " values" );
         }
+
+        LOGGER.trace( pwmApplication.getSessionLabel(), () -> "--end " + label + "--" );
+    }
+
+    private static boolean checkIfOutputDumpingEnabled( final PwmApplication pwmApplication )
+    {
+        return LOGGER.isEnabled( PwmLogLevel.TRACE )
+                && !pwmApplication.getPwmEnvironment().isInternalRuntimeInstance()
+                && Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.LOGGING_OUTPUT_CONFIGURATION ) );
+    }
+
+    static String makeRuntimeNonce()
+    {
+        return PwmRandom.getInstance().randomUUID().toString();
     }
 }

+ 1 - 1
server/src/main/java/password/pwm/PwmConstants.java

@@ -74,7 +74,7 @@ public abstract class PwmConstants
     public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
     public static final List<String> HIGHLIGHT_LOCALES = StringUtil.splitAndTrim( readPwmConstantsBundle( "locale.highlightList" ), "," );
 
-    public static final CSVFormat DEFAULT_CSV_FORMAT = CSVFormat.DEFAULT;
+    public static final CSVFormat DEFAULT_CSV_FORMAT = CSVFormat.Builder.create( CSVFormat.DEFAULT ).setCommentMarker( '#' ).build();
 
     public static final String DEFAULT_DATETIME_FORMAT_STR = readPwmConstantsBundle( "locale.defaultDateTimeFormat" );
     public static final TimeZone DEFAULT_TIMEZONE = TimeZone.getTimeZone( readPwmConstantsBundle( "locale.defaultTimeZone" ) );

+ 28 - 14
server/src/main/java/password/pwm/PwmDomain.java

@@ -32,7 +32,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.servlet.peoplesearch.PeopleSearchService;
 import password.pwm.http.servlet.resource.ResourceServletService;
 import password.pwm.http.state.SessionStateService;
-import password.pwm.ldap.LdapConnectionService;
+import password.pwm.ldap.LdapDomainService;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmServiceEnum;
@@ -50,16 +50,18 @@ import password.pwm.svc.stats.StatisticsService;
 import password.pwm.svc.token.TokenService;
 import password.pwm.svc.userhistory.UserHistoryService;
 import password.pwm.svc.wordlist.SharedHistoryService;
+import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
 import java.time.Instant;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
- * A repository for objects common to the servlet context.  A singleton
- * of this object is stored in the servlet context.
+ * A representation of a domain in the PwmDomain.  Domains contain settings, functionality and behavior that is 'site' specific.
+ * Domains are sometimes referred to as a "tenant" in other systems.
  *
  * @author Jason D. Rivard
  */
@@ -72,6 +74,13 @@ public class PwmDomain
     private final PwmServiceManager pwmServiceManager;
     private final SessionLabel sessionLabel;
 
+    // for debugging
+    private final int instanceId = DOMAIN_INCREMENTER.next();
+
+    private final AtomicInteger activeServletRequests = new AtomicInteger( 0 );
+
+    private static final AtomicLoopIntIncrementer DOMAIN_INCREMENTER = new AtomicLoopIntIncrementer();
+
     public PwmDomain( final PwmApplication pwmApplication, final DomainID domainID )
     {
         this.pwmApplication = Objects.requireNonNull( pwmApplication );
@@ -89,16 +98,15 @@ public class PwmDomain
 
     {
         final Instant startTime = Instant.now();
-        LOGGER.trace( sessionLabel, () -> "initializing domain " + domainID.stringValue() );
+        LOGGER.trace( sessionLabel, () -> "initializing domain " + domainID.stringValue() + " (instanceId=" + instanceId + ")" );
 
         pwmServiceManager.initAllServices();
 
-        if ( Boolean.parseBoolean( getConfig().readAppProperty( AppProperty.LOGGING_OUTPUT_CONFIGURATION ) ) )
-        {
-            PwmApplicationUtil.outputConfigurationToLog( pwmApplication, domainID );
-        }
+        PwmApplicationUtil.outputConfigurationToLog( pwmApplication, domainID );
 
-        LOGGER.trace( sessionLabel, () -> "completed initializing domain " + domainID.stringValue(), () -> TimeDuration.fromCurrent( startTime ) );
+        LOGGER.trace( sessionLabel, () -> "completed initializing domain " + domainID.stringValue()
+                        + " (instanceId=" + instanceId + ")",
+                TimeDuration.fromCurrent( startTime ) );
     }
 
     public DomainConfig getConfig( )
@@ -151,9 +159,9 @@ public class PwmDomain
         return pwmApplication.determineIfDetailErrorMsgShown();
     }
 
-    public LdapConnectionService getLdapConnectionService( )
+    public LdapDomainService getLdapConnectionService( )
     {
-        return ( LdapConnectionService ) pwmServiceManager.getService( PwmServiceEnum.LdapConnectionService );
+        return ( LdapDomainService ) pwmServiceManager.getService( PwmServiceEnum.LdapConnectionService );
     }
 
     public AuditService getAuditService()
@@ -180,11 +188,10 @@ public class PwmDomain
         }
     }
 
-    public ChaiProvider getProxyChaiProvider( final SessionLabel sessionLabel, final String identifier )
+    public ChaiProvider getProxyChaiProvider( final SessionLabel sessionLabel, final String profileId )
             throws PwmUnrecoverableException
     {
-        Objects.requireNonNull( identifier );
-        return getLdapConnectionService().getProxyChaiProvider( sessionLabel, identifier );
+        return getLdapConnectionService().getProxyChaiProvider( sessionLabel, profileId );
     }
 
     public List<PwmService> getPwmServices( )
@@ -244,14 +251,21 @@ public class PwmDomain
 
     public void shutdown()
     {
+        final Instant startTime = Instant.now();
         LOGGER.trace( sessionLabel, () -> "beginning shutdown domain " + domainID.stringValue() );
         pwmServiceManager.shutdownAllServices();
+        LOGGER.trace( sessionLabel, () -> "shutdown domain " + domainID.stringValue() + " completed", TimeDuration.fromCurrent( startTime ) );
     }
 
     public DomainID getDomainID()
     {
         return domainID;
     }
+
+    public AtomicInteger getActiveServletRequests( )
+    {
+        return activeServletRequests;
+    }
 }
 
 

+ 37 - 26
server/src/main/java/password/pwm/PwmDomainUtil.java

@@ -24,6 +24,7 @@ import password.pwm.bean.DomainID;
 import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
@@ -76,7 +77,7 @@ class PwmDomainUtil
             throws PwmUnrecoverableException
     {
         final Instant domainInitStartTime = Instant.now();
-        LOGGER.trace( () -> "beginning domain initializations" );
+        LOGGER.trace( pwmApplication.getSessionLabel(), () -> "beginning domain initializations" );
 
         final List<Callable<Optional<PwmUnrecoverableException>>> callables = domains.stream()
                 .map( DomainInitializingCallable::new )
@@ -95,7 +96,7 @@ class PwmDomainUtil
             throw domainStartupException.get();
         }
 
-        LOGGER.trace( () -> "completed domain initialization for domains", () -> TimeDuration.fromCurrent( domainInitStartTime ) );
+        LOGGER.trace( pwmApplication.getSessionLabel(), () -> "completed domain initialization for domains", TimeDuration.fromCurrent( domainInitStartTime ) );
     }
 
     private static class DomainInitializingCallable implements Callable<Optional<PwmUnrecoverableException>>
@@ -109,7 +110,6 @@ class PwmDomainUtil
 
         @Override
         public Optional<PwmUnrecoverableException> call()
-                throws Exception
         {
             try
             {
@@ -123,7 +123,7 @@ class PwmDomainUtil
         }
     }
 
-    static Map<DomainID, PwmDomain> reInitDomains(
+    static void reInitDomains(
             final PwmApplication pwmApplication,
             final AppConfig newConfig,
             final AppConfig oldConfig
@@ -131,33 +131,50 @@ class PwmDomainUtil
             throws PwmUnrecoverableException
     {
         final Map<DomainModifyCategory, Set<DomainID>> categorizedDomains = categorizeDomainModifications( newConfig, oldConfig );
+        categorizedDomains.forEach( (  modifyCategory, domainIDSet ) -> domainIDSet.forEach( domainID ->
+                LOGGER.trace( pwmApplication.getSessionLabel(), () -> "domain '" + domainID
+                        + "' configuration modification detected as: " + modifyCategory ) ) );
 
         final Set<PwmDomain> deletedDomains = pwmApplication.domains().entrySet().stream()
                 .filter( e -> categorizedDomains.get( DomainModifyCategory.obsolete ).contains( e.getKey() ) )
                 .map( Map.Entry::getValue ).collect( Collectors.toSet() );
 
+
         final Set<PwmDomain> newDomains = pwmApplication.domains().entrySet().stream()
                 .filter( e -> categorizedDomains.get( DomainModifyCategory.created ).contains( e.getKey() ) )
                 .map( Map.Entry::getValue ).collect( Collectors.toSet() );
 
+
         final Map<DomainID, PwmDomain> returnDomainMap = new TreeMap<>( pwmApplication.domains().entrySet().stream()
                 .filter( e -> categorizedDomains.get( DomainModifyCategory.unchanged ).contains( e.getKey() ) )
                 .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) ) );
 
         for ( final DomainID modifiedDomainID : categorizedDomains.get( DomainModifyCategory.modified ) )
         {
+            LOGGER.trace( pwmApplication.getSessionLabel(), () -> "domain '" + modifiedDomainID
+                    + "' configuration has changed and requires a restart" );
             deletedDomains.add( pwmApplication.domains().get( modifiedDomainID ) );
             final PwmDomain newDomain = new PwmDomain( pwmApplication, modifiedDomainID );
             newDomains.add( newDomain );
             returnDomainMap.put( modifiedDomainID, newDomain );
         }
 
+        pwmApplication.setDomains( returnDomainMap );
 
-        initDomains( pwmApplication, newDomains );
+        if ( newDomains.isEmpty() && deletedDomains.isEmpty() )
+        {
+            LOGGER.debug( pwmApplication.getSessionLabel(), () -> "no domain-level settings have been changed, restart of domain services is not required" );
+        }
 
-        processDeletedDomains( pwmApplication, deletedDomains );
+        if ( !newDomains.isEmpty() )
+        {
+            initDomains( pwmApplication, newDomains );
+        }
 
-        return Collections.unmodifiableMap( returnDomainMap );
+        if ( !deletedDomains.isEmpty() )
+        {
+            processDeletedDomains( pwmApplication, deletedDomains );
+        }
     }
 
     private static void processDeletedDomains(
@@ -165,22 +182,16 @@ class PwmDomainUtil
             final Set<PwmDomain> deletedDomains
     )
     {
-        // 1 minute later ( to avoid interrupting any in-progress requests, shutdown any obsoleted domains
-        if ( !deletedDomains.isEmpty() )
+        if ( deletedDomains.isEmpty() )
         {
-            pwmApplication.getPwmScheduler().immediateExecuteRunnableInNewThread( () ->
-                    {
-                        TimeDuration.MINUTE.pause();
-                        final Instant startTime = Instant.now();
-                        LOGGER.trace( pwmApplication.getSessionLabel(), () -> "shutting down obsoleted domain services" );
-                        deletedDomains.forEach( PwmDomain::shutdown );
-                        LOGGER.debug( pwmApplication.getSessionLabel(), () -> "shut down obsoleted domain services completed",
-                                () -> TimeDuration.fromCurrent( startTime ) );
-                    },
-                    pwmApplication.getSessionLabel(),
-                    "obsoleted domain cleanup" );
+            return;
         }
 
+        final Instant startTime = Instant.now();
+        LOGGER.trace( pwmApplication.getSessionLabel(), () -> "shutting down obsoleted domain services" );
+        deletedDomains.forEach( PwmDomain::shutdown );
+        LOGGER.debug( pwmApplication.getSessionLabel(), () -> "shut down obsoleted domain services completed",
+                TimeDuration.fromCurrent( startTime ) );
     }
 
     enum DomainModifyCategory
@@ -201,13 +212,13 @@ class PwmDomainUtil
         {
             final Set<DomainID> obsoleteDomains = new HashSet<>( oldConfig.getDomainConfigs().keySet() );
             obsoleteDomains.removeAll( newConfig.getDomainConfigs().keySet() );
-            types.put( DomainModifyCategory.obsolete, Collections.unmodifiableSet( obsoleteDomains ) );
+            types.put( DomainModifyCategory.obsolete, CollectionUtil.stripNulls( obsoleteDomains ) );
         }
 
         {
             final Set<DomainID> createdDomains = new HashSet<>( newConfig.getDomainConfigs().keySet() );
             createdDomains.removeAll( oldConfig.getDomainConfigs().keySet() );
-            types.put( DomainModifyCategory.created, Collections.unmodifiableSet( createdDomains ) );
+            types.put( DomainModifyCategory.created, CollectionUtil.stripNulls( createdDomains ) );
         }
 
         final Set<DomainID> unchangedDomains = new HashSet<>();
@@ -215,9 +226,9 @@ class PwmDomainUtil
         for ( final DomainID domainID : newConfig.getDomainConfigs().keySet() )
         {
             final DomainConfig newDomainConfig = newConfig.getDomainConfigs().get( domainID );
-            final String oldValueHash = oldConfig.getDomainConfigs().get( newDomainConfig.getDomainID() ).getValueHash();
+            final DomainConfig oldDomainConfig = oldConfig.getDomainConfigs().get( newDomainConfig.getDomainID() );
 
-            if ( Objects.equals( oldValueHash, newDomainConfig.getValueHash() ) )
+            if ( newDomainConfig != null && oldDomainConfig != null && Objects.equals( oldDomainConfig.getValueHash(), newDomainConfig.getValueHash() ) )
             {
                 unchangedDomains.add( domainID );
             }
@@ -226,8 +237,8 @@ class PwmDomainUtil
                 modifiedDomains.add( domainID );
             }
         }
-        types.put( DomainModifyCategory.unchanged, Collections.unmodifiableSet( unchangedDomains ) );
-        types.put( DomainModifyCategory.modified, Collections.unmodifiableSet( modifiedDomains ) );
+        types.put( DomainModifyCategory.unchanged, CollectionUtil.stripNulls( unchangedDomains ) );
+        types.put( DomainModifyCategory.modified, CollectionUtil.stripNulls( modifiedDomains ) );
         return Collections.unmodifiableMap( types );
     }
 }

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

@@ -20,9 +20,11 @@
 
 package password.pwm;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import lombok.Builder;
 import lombok.Singular;
 import lombok.Value;
+import password.pwm.bean.SessionLabel;
 import password.pwm.config.AppConfig;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -30,6 +32,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.ContextManager;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.util.logging.PwmLogger;
@@ -69,6 +72,8 @@ public class PwmEnvironment
     @Singular
     private Map<ApplicationParameter, String> parameters;
 
+    private final LazySupplier<DeploymentPlatform> deploymentPlatformLazySupplier = new LazySupplier<>( this::determineDeploymentPlatform );
+
     public enum ApplicationParameter
     {
         AutoExportHttpsKeyStoreFile,
@@ -90,8 +95,6 @@ public class PwmEnvironment
 
     public enum ApplicationFlag
     {
-        Appliance,
-        Docker,
         ManageHttps,
         NoFileLock,
         CommandLineInstance,;
@@ -102,6 +105,14 @@ public class PwmEnvironment
         }
     }
 
+    public enum DeploymentPlatform
+    {
+        War,
+        Onejar,
+        Docker,
+        Appliance,;
+    }
+
     public enum EnvironmentParameter
     {
         applicationPath,
@@ -168,7 +179,7 @@ public class PwmEnvironment
         }
         if ( applicationPathIsWebInfPath )
         {
-            LOGGER.trace( () -> "applicationPath appears to be servlet /WEB-INF directory" );
+            LOGGER.trace( SessionLabel.SYSTEM_LABEL, () -> "applicationPath appears to be servlet /WEB-INF directory" );
         }
     }
 
@@ -196,7 +207,7 @@ public class PwmEnvironment
             );
         }
 
-        LOGGER.trace( () -> "examining applicationPath of " + applicationPath.getAbsolutePath() + "" );
+        LOGGER.trace( SessionLabel.SYSTEM_LABEL, () -> "examining applicationPath of " + applicationPath.getAbsolutePath() + "" );
 
         if ( !applicationPath.exists() )
         {
@@ -223,7 +234,7 @@ public class PwmEnvironment
         }
 
         final File infoFile = new File( applicationPath.getAbsolutePath() + File.separator + PwmConstants.APPLICATION_PATH_INFO_FILE );
-        LOGGER.trace( () -> "checking " + infoFile.getAbsolutePath() + " status" );
+        LOGGER.trace( SessionLabel.SYSTEM_LABEL, () -> "checking " + infoFile.getAbsolutePath() + " status" );
         if ( infoFile.exists() )
         {
             final String errorMsg = "The file " + infoFile.getAbsolutePath() + " exists, and an applicationPath was not explicitly specified."
@@ -310,7 +321,7 @@ public class PwmEnvironment
                 }
                 else
                 {
-                    LOGGER.warn( () -> "unknown " + EnvironmentParameter.applicationFlags + " value: " + input );
+                    LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "unknown " + EnvironmentParameter.applicationFlags + " value: " + input );
                 }
             }
             return returnFlags;
@@ -330,7 +341,7 @@ public class PwmEnvironment
             }
             catch ( final Exception e )
             {
-                LOGGER.warn( () -> "error reading properties file '" + input + "' specified by environment setting "
+                LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "error reading properties file '" + input + "' specified by environment setting "
                         + EnvironmentParameter.applicationParamFile + ", error: " + e.getMessage() );
             }
 
@@ -347,14 +358,14 @@ public class PwmEnvironment
                     }
                     else
                     {
-                        LOGGER.warn( () -> "unknown " + EnvironmentParameter.applicationParamFile + " value: " + input );
+                        LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "unknown " + EnvironmentParameter.applicationParamFile + " value: " + input );
                     }
                 }
                 return Collections.unmodifiableMap( returnParams );
             }
             catch ( final Exception e )
             {
-                LOGGER.warn( () -> "unable to parse jason value of " + EnvironmentParameter.applicationParamFile + ", error: " + e.getMessage() );
+                LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "unable to parse jason value of " + EnvironmentParameter.applicationParamFile + ", error: " + e.getMessage() );
             }
 
             return Collections.emptyMap();
@@ -365,10 +376,33 @@ public class PwmEnvironment
     {
         if ( PwmConstants.TRIAL_MODE && mode == PwmApplicationMode.RUNNING )
         {
-            LOGGER.info( () -> "application is in trial mode" );
+            LOGGER.info( SessionLabel.SYSTEM_LABEL, () -> "application is in trial mode" );
             return PwmApplicationMode.CONFIGURATION;
         }
 
         return mode;
     }
+
+    public DeploymentPlatform getDeploymentPlatform()
+    {
+        return deploymentPlatformLazySupplier.get();
+    }
+
+    @SuppressFBWarnings( "DMI_HARDCODED_ABSOLUTE_FILENAME" )
+    private DeploymentPlatform determineDeploymentPlatform()
+    {
+        final File dockerEnvFile = new File( "/.dockerenv" );
+
+        if ( dockerEnvFile.exists() )
+        {
+            return DeploymentPlatform.Docker;
+        }
+
+        final String envValue = System.getProperty( "ONEJAR_ENV", "FALSE" );
+        if ( Boolean.getBoolean( envValue ) )
+        {
+            return DeploymentPlatform.Onejar;
+        }
+        return DeploymentPlatform.War;
+    }
 }

+ 1 - 1
server/src/main/java/password/pwm/VerificationMethodSystem.java

@@ -23,7 +23,7 @@ package password.pwm;
 import password.pwm.bean.SessionLabel;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.ldap.UserInfo;
+import password.pwm.user.UserInfo;
 
 import java.io.Serializable;
 import java.util.List;

+ 1 - 1
server/src/main/java/password/pwm/bean/LocalSessionStateBean.java

@@ -21,7 +21,7 @@
 package password.pwm.bean;
 
 import lombok.Data;
-import password.pwm.ldap.UserInfoBean;
+import password.pwm.user.UserInfoBean;
 import password.pwm.util.java.MovingAverage;
 import password.pwm.util.java.TimeDuration;
 

+ 1 - 2
server/src/main/java/password/pwm/bean/RemoteVerificationRequestBean.java

@@ -24,12 +24,11 @@ import lombok.Builder;
 import lombok.Value;
 import password.pwm.bean.pub.PublicUserInfoBean;
 
-import java.io.Serializable;
 import java.util.Map;
 
 @Value
 @Builder
-public class RemoteVerificationRequestBean implements Serializable
+public class RemoteVerificationRequestBean
 {
     private final String responseSessionID;
     private final PublicUserInfoBean userInfo;

+ 1 - 2
server/src/main/java/password/pwm/bean/RemoteVerificationResponseBean.java

@@ -23,11 +23,10 @@ package password.pwm.bean;
 import lombok.Value;
 import password.pwm.VerificationMethodSystem;
 
-import java.io.Serializable;
 import java.util.List;
 
 @Value
-public class RemoteVerificationResponseBean implements Serializable
+public class RemoteVerificationResponseBean
 {
     private String displayInstructions;
     private VerificationMethodSystem.VerificationState verificationState;

+ 2 - 0
server/src/main/java/password/pwm/bean/SessionLabel.java

@@ -40,6 +40,8 @@ public class SessionLabel implements Serializable
     public static final SessionLabel TEST_SESSION_LABEL = SessionLabel.builder().sessionID( SYSTEM_LABEL_SESSION_ID ).username( "test" ).build();
     public static final SessionLabel CLI_SESSION_LABEL = SessionLabel.builder().sessionID( SYSTEM_LABEL_SESSION_ID ).username( "cli" ).build();
     public static final SessionLabel CONTEXT_SESSION_LABEL = SessionLabel.builder().sessionID( SYSTEM_LABEL_SESSION_ID ).username( "context" ).build();
+    public static final SessionLabel ONEJAR_LABEL = SessionLabel.builder().sessionID( SYSTEM_LABEL_SESSION_ID ).username( "onejar" ).build();
+
 
     private final String sessionID;
     private final String requestID;

+ 1 - 1
server/src/main/java/password/pwm/bean/TokenDestinationItem.java

@@ -31,7 +31,7 @@ import password.pwm.config.option.MessageSendMethod;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.Display;
 import password.pwm.i18n.PwmDisplayBundle;
-import password.pwm.ldap.UserInfo;
+import password.pwm.user.UserInfo;
 import password.pwm.svc.token.TokenDestinationDisplayMasker;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.StringUtil;

+ 24 - 7
server/src/main/java/password/pwm/config/AppConfig.java

@@ -24,6 +24,7 @@ import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.PrivateKeyCertificate;
+import password.pwm.bean.SessionLabel;
 import password.pwm.config.option.CertificateMatchingMode;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.profile.EmailServerProfile;
@@ -40,7 +41,6 @@ import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.CollectionUtil;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
@@ -53,7 +53,6 @@ import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumMap;
-import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -103,7 +102,6 @@ public class AppConfig implements SettingReader
     {
         this.storedConfiguration = storedConfiguration;
         this.settingReader = new StoredSettingReader( storedConfiguration, null, DomainID.systemId() );
-
         this.appPropertyOverrides = makeAppPropertyOverrides( settingReader );
 
         this.applicationSecurityKey = makeAppSecurityKey( this );
@@ -188,8 +186,8 @@ public class AppConfig implements SettingReader
 
     public Map<AppProperty, String> readAllAppProperties()
     {
-          return Collections.unmodifiableMap( EnumSet.allOf( AppProperty.class ).stream()
-                  .collect( Collectors.toMap(
+          return Collections.unmodifiableMap( CollectionUtil.enumStream( AppProperty.class )
+                  .collect( CollectionUtil.collectorToLinkedMap(
                           Function.identity(),
                           this::readAppProperty
                   ) ) );
@@ -343,7 +341,7 @@ public class AppConfig implements SettingReader
         final Map<AppProperty, String> appPropertyMap = new EnumMap<>( AppProperty.class );
         for ( final Map.Entry<String, String> stringEntry : stringMap.entrySet() )
         {
-            JavaHelper.readEnumFromString( AppProperty.class, stringEntry.getKey() )
+            AppProperty.forKey( stringEntry.getKey() )
                     .ifPresent( appProperty ->
                     {
                        final String defaultValue = appProperty.getDefaultValue();
@@ -358,6 +356,25 @@ public class AppConfig implements SettingReader
         return Collections.unmodifiableMap( appPropertyMap );
     }
 
+    public boolean isSmsConfigured()
+    {
+        final String gatewayUrl = readSettingAsString( PwmSetting.SMS_GATEWAY_URL );
+        final String gatewayUser = readSettingAsString( PwmSetting.SMS_GATEWAY_USER );
+        final PasswordData gatewayPass = readSettingAsPassword( PwmSetting.SMS_GATEWAY_PASSWORD );
+        if ( gatewayUrl == null || gatewayUrl.length() < 1 )
+        {
+            return false;
+        }
+
+        if ( gatewayUser != null && gatewayUser.length() > 0 && ( gatewayPass == null ) )
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+
     private static PwmSecurityKey makeAppSecurityKey( final AppConfig appConfig )
     {
         try
@@ -368,7 +385,7 @@ public class AppConfig implements SettingReader
             {
                 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 );
+                LOGGER.warn( SessionLabel.SYSTEM_LABEL, errorInfo::toDebugStr );
                 return new PwmSecurityKey( PwmRandom.getInstance().alphaNumericString( 1024 ) );
             }
             else

+ 103 - 50
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -20,21 +20,30 @@
 
 package password.pwm.config;
 
-import lombok.Value;
+import org.jrivard.xmlchai.XmlElement;
 import password.pwm.PwmConstants;
+import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.StoredValue;
-import password.pwm.util.java.CollectionUtil;
+import password.pwm.config.value.ValueFactory;
+import password.pwm.i18n.Config;
+import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
+import password.pwm.util.macro.MacroRequest;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
+import java.util.EnumMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -90,6 +99,8 @@ public enum PwmSetting
     // telemetry
     PUBLISH_STATS_ENABLE(
             "pwm.publishStats.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.TELEMETRY ),
+    VERSION_CHECK_ENABLE(
+            "pwm.versionCheck.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.TELEMETRY ),
     PUBLISH_STATS_SITE_DESCRIPTION(
             "pwm.publishStats.siteDescription", PwmSettingSyntax.STRING, PwmSettingCategory.TELEMETRY ),
 
@@ -1305,7 +1316,6 @@ public enum PwmSetting
     HELPDESK_ENABLE_OTP_VERIFY(
             "helpdesk.otp.verify", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_BASE ),;
 
-
     private static final Map<String, PwmSetting> KEY_MAP = Arrays.stream( values() )
             .collect( Collectors.toUnmodifiableMap( PwmSetting::getKey, Function.identity() ) );
 
@@ -1320,7 +1330,19 @@ public enum PwmSetting
     private final String key;
     private final PwmSettingSyntax syntax;
     private final PwmSettingCategory category;
-    private final PwmSettingMetaDataReader pwmSettingMetaDataReader;
+
+    private static final Map<PwmSetting, List<TemplateSetReference<StoredValue>>> DEFAULT_VALUE_CACHE = initDefaultValueCache();
+
+    private final transient Supplier<String> defaultMenuLocation = new LazySupplier<>(
+            () -> readMenuLocationDebug( this, null, PwmConstants.DEFAULT_LOCALE ) );
+
+    private final transient Supplier<String> defaultLocaleLabel = new LazySupplier<>(
+            () -> readLabel( this, PwmConstants.DEFAULT_LOCALE ) );
+
+    private final transient Supplier<String> defaultLocaleDescription = new LazySupplier<>(
+            () -> readDescription( this, PwmConstants.DEFAULT_LOCALE ) );
+
+
 
     PwmSetting(
             final String key,
@@ -1331,7 +1353,6 @@ public enum PwmSetting
         this.key = key;
         this.syntax = syntax;
         this.category = category;
-        this.pwmSettingMetaDataReader = new PwmSettingMetaDataReader( this );
     }
 
     public static Comparator<PwmSetting> menuLocationComparator()
@@ -1359,9 +1380,14 @@ public enum PwmSetting
         return syntax;
     }
 
+    private PwmSettingMetaData getPwmSettingMetaData()
+    {
+        return PwmSettingMetaData.forSetting( this );
+    }
+
     private List<TemplateSetReference<StoredValue>> getDefaultValue()
     {
-        return pwmSettingMetaDataReader.getDefaultValue();
+        return DEFAULT_VALUE_CACHE.get( this );
     }
 
     public StoredValue getDefaultValue( final PwmSettingTemplateSet templateSet )
@@ -1383,52 +1409,62 @@ public enum PwmSetting
 
     public Map<PwmSettingProperty, String> getProperties( )
     {
-        return pwmSettingMetaDataReader.getProperties();
+        return getPwmSettingMetaData().getProperties();
     }
 
     public Set<PwmSettingFlag> getFlags( )
     {
-        return pwmSettingMetaDataReader.getFlags();
+        return getPwmSettingMetaData().getFlags();
     }
 
     public Map<String, String> getOptions()
     {
-        return pwmSettingMetaDataReader.getOptions();
+        return getPwmSettingMetaData().getOptions();
     }
 
     public String getLabel( final Locale locale )
     {
-        return pwmSettingMetaDataReader.getLabel( locale );
+        if ( PwmConstants.DEFAULT_LOCALE.equals( locale ) )
+        {
+            return defaultLocaleLabel.get();
+        }
+
+        return readLabel( this, locale );
     }
 
     public String getDescription( final Locale locale )
     {
-        return pwmSettingMetaDataReader.getDescription( locale );
+        if ( PwmConstants.DEFAULT_LOCALE.equals( locale ) )
+        {
+            return defaultLocaleDescription.get();
+        }
+
+        return readDescription( this, locale );
     }
 
     public String getExample( final PwmSettingTemplateSet template )
     {
-        return pwmSettingMetaDataReader.getExample( template );
+        return TemplateSetReference.referenceForTempleSet( getPwmSettingMetaData().getExamples(), template );
     }
 
     public boolean isRequired( )
     {
-        return pwmSettingMetaDataReader.isRequired();
+        return getPwmSettingMetaData().isRequired();
     }
 
     public boolean isHidden( )
     {
-        return pwmSettingMetaDataReader.isHidden();
+        return getPwmSettingMetaData().isHidden();
     }
 
     public int getLevel( )
     {
-        return pwmSettingMetaDataReader.getLevel();
+        return getPwmSettingMetaData().getLevel();
     }
 
     public Pattern getRegExPattern( )
     {
-        return pwmSettingMetaDataReader.getRegExPattern();
+        return getPwmSettingMetaData().getPattern();
     }
 
     public static Optional<PwmSetting> forKey( final String key )
@@ -1441,12 +1477,19 @@ public enum PwmSetting
             final Locale locale
     )
     {
-        return pwmSettingMetaDataReader.toMenuLocationDebug( profileID, locale );
+        if ( PwmConstants.DEFAULT_LOCALE.equals( locale ) && StringUtil.isEmpty( profileID ) )
+        {
+            return defaultMenuLocation.get();
+        }
+        else
+        {
+            return readMenuLocationDebug( this, profileID, locale );
+        }
     }
 
     public Collection<LDAPPermissionInfo> getLDAPPermissionInfo()
     {
-        return pwmSettingMetaDataReader.getLDAPPermissionInfo();
+        return getPwmSettingMetaData().getLdapPermissionInfo();
     }
 
     public static List<PwmSetting> sortedValues()
@@ -1454,46 +1497,56 @@ public enum PwmSetting
         return SORTED_VALUES;
     }
 
-    @Value
-    static class TemplateSetReference<T>
+    private static Map<PwmSetting, List<TemplateSetReference<StoredValue>>> initDefaultValueCache()
     {
-        private final T reference;
-        private final Set<PwmSettingTemplate> settingTemplates;
-
-        static <T> T referenceForTempleSet(
-                final List<TemplateSetReference<T>> templateSetReferences,
-                final PwmSettingTemplateSet pwmSettingTemplateSet
-        )
+        final Map<PwmSetting, List<TemplateSetReference<StoredValue>>> map = new EnumMap<>( PwmSetting.class );
+        for ( final XmlElement settingElement : PwmSettingXml.readAllSettingXmlElements() )
         {
-            final PwmSettingTemplateSet effectiveTemplateSet = pwmSettingTemplateSet == null
-                    ? PwmSettingTemplateSet.getDefault()
-                    : pwmSettingTemplateSet;
-
-            if ( templateSetReferences == null || templateSetReferences.isEmpty() )
+            final List<TemplateSetReference<StoredValue>> returnObj = new ArrayList<>();
+            final PwmSetting pwmSetting = PwmSetting.forKey( settingElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_KEY ).orElseThrow() ).orElseThrow();
+            final List<XmlElement> defaultElements = settingElement.getChildren( PwmSettingXml.XML_ELEMENT_DEFAULT );
+            if ( pwmSetting.getSyntax() == PwmSettingSyntax.PASSWORD )
             {
-                throw new IllegalStateException( "templateSetReferences can not be null" );
+                returnObj.add( new TemplateSetReference<>( new PasswordValue( null ), Collections.emptySet() ) );
             }
-
-            if ( templateSetReferences.size() == 1 )
-            {
-                return templateSetReferences.get( 0 ).getReference();
-            }
-
-            for ( int matchCountExamSize = templateSetReferences.size(); matchCountExamSize > 0; matchCountExamSize-- )
+            else
             {
-                for ( final TemplateSetReference<T> templateSetReference : templateSetReferences )
+                for ( final XmlElement defaultElement : defaultElements )
                 {
-                    final Set<PwmSettingTemplate> temporarySet = CollectionUtil.copyToEnumSet( templateSetReference.getSettingTemplates(), PwmSettingTemplate.class );
-                    temporarySet.retainAll( effectiveTemplateSet.getTemplates() );
-                    final int matchCount = temporarySet.size();
-                    if ( matchCount == matchCountExamSize )
-                    {
-                        return templateSetReference.getReference();
-                    }
+                    final Set<PwmSettingTemplate> definedTemplates = PwmSettingXml.parseTemplateAttribute( defaultElement );
+                    final StoredValue storedValue = ValueFactory.fromXmlValues( pwmSetting, defaultElement, null );
+                    returnObj.add( new TemplateSetReference<>( storedValue, definedTemplates ) );
                 }
             }
-
-            return templateSetReferences.get( 0 ).getReference();
+            if ( returnObj.isEmpty() )
+            {
+                throw new IllegalStateException( "no default value for setting " + pwmSetting.getKey() );
+            }
+            map.put( pwmSetting, returnObj );
         }
+        return map;
+    }
+
+    private static String readLabel( final PwmSetting pwmSetting, final Locale locale )
+    {
+        return readStringProperty( password.pwm.i18n.PwmSetting.SETTING_LABEL_PREFIX + pwmSetting.getKey(), locale );
+    }
+
+    private static String readDescription( final PwmSetting pwmSetting, final Locale locale )
+    {
+        return readStringProperty( password.pwm.i18n.PwmSetting.SETTING_DESCRIPTION_PREFIX + pwmSetting.getKey(), locale );
+    }
+
+    private static String readStringProperty( final String key, final Locale locale )
+    {
+        final String storedText = LocaleHelper.getLocalizedMessage( locale, key, null, password.pwm.i18n.PwmSetting.class );
+        final MacroRequest macroRequest = MacroRequest.forStatic();
+        return macroRequest.expandMacros( storedText );
+    }
+
+    private static String readMenuLocationDebug( final PwmSetting pwmSetting, final String profileID, final Locale locale )
+    {
+        final String separator = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
+        return pwmSetting.getCategory().toMenuLocationDebug( profileID, locale ) + separator + pwmSetting.getLabel( locale );
     }
 }

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

@@ -509,8 +509,7 @@ public enum PwmSettingCategory
 
         public static Set<PwmSettingCategory> readChildren( final PwmSettingCategory category )
         {
-            final Set<PwmSettingCategory> categories = EnumSet.allOf( PwmSettingCategory.class )
-                    .stream()
+            final Set<PwmSettingCategory> categories = CollectionUtil.enumStream( PwmSettingCategory.class )
                     .filter( ( loopCategory ) -> loopCategory.getParent() == category )
                     .collect( Collectors.toUnmodifiableSet() );
             return Collections.unmodifiableSet( CollectionUtil.copyToEnumSet( categories, PwmSettingCategory.class ) );

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

@@ -29,7 +29,7 @@ public enum PwmSettingFlag
     MacroSupport,
 
     /* Setting uses LDAP DN syntax */
-    ldapDNsyntax,
+    ldapDnSyntax,
 
     /* Setting must be a valid email address format */
     emailSyntax,

+ 244 - 0
server/src/main/java/password/pwm/config/PwmSettingMetaData.java

@@ -0,0 +1,244 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 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;
+
+import lombok.Builder;
+import lombok.Value;
+import org.jrivard.xmlchai.XmlElement;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.macro.MacroRequest;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+@Value
+@Builder
+/**
+ * Utility class for reading PwmSetting.xml values and making them available to PWM.  Since PwmSetting
+ * enums are fairly complex, there can be issues with static initialization circular dependencies during
+ * initialization of the classes in this package.
+ *
+ * This class tries to be self-sufficient during initialization, and then provide values for {@link PwmSetting}
+ * method invocations.
+ */
+class PwmSettingMetaData
+{
+    private static final Map<PwmSetting, PwmSettingMetaData> META_DATA_MAP = initMap();
+
+    private final List<TemplateSetReference<String>> examples;
+    private final Map<String, String> options;
+    private final Set<PwmSettingFlag> flags;
+    private final Map<PwmSettingProperty, String> properties;
+    private final Collection<LDAPPermissionInfo> ldapPermissionInfo;
+    private final boolean required;
+    private final boolean hidden;
+    private final int level;
+    private final Pattern pattern;
+
+    private static Map<PwmSetting, PwmSettingMetaData> initMap()
+    {
+        final EnumMap<PwmSetting, PwmSettingMetaData> map = new EnumMap<>( PwmSetting.class );
+
+        for ( final XmlElement settingElement : PwmSettingXml.readAllSettingXmlElements() )
+        {
+            final PwmSetting pwmSetting = PwmSetting.forKey( settingElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_KEY )
+                    .orElseThrow() ).orElseThrow();
+
+            final PwmSettingMetaData pwmSettingMetaData = PwmSettingMetaData.builder()
+                    .examples( InitReader.readExamples( settingElement ) )
+                    .properties( InitReader.readProperties( pwmSetting, settingElement ) )
+                    .options( InitReader.readOptions( pwmSetting, settingElement ) )
+                    .flags( InitReader.readFlags( settingElement ) )
+                    .ldapPermissionInfo( InitReader.readLdapPermissionInfo( settingElement ) )
+                    .required( InitReader.readRequired( settingElement ) )
+                    .hidden( InitReader.readHidden( pwmSetting, settingElement ) )
+                    .level( InitReader.readLevel( settingElement ) )
+                    .pattern( InitReader.readPattern( pwmSetting, settingElement ) )
+                    .build();
+
+            map.put( pwmSetting, pwmSettingMetaData );
+        }
+
+        return map;
+    }
+
+    static PwmSettingMetaData forSetting( final PwmSetting pwmSetting )
+    {
+        return META_DATA_MAP.get( pwmSetting );
+    }
+
+    private static class InitReader
+    {
+        private static Set<PwmSettingFlag> readFlags( final XmlElement settingElement )
+        {
+            final Set<PwmSettingFlag> returnObj = EnumSet.noneOf( PwmSettingFlag.class );
+            settingElement.getChild( PwmSettingXml.XML_ELEMENT_FLAGS ).ifPresent( flagElement ->
+                    flagElement.getChildren( PwmSettingXml.XML_ELEMENT_FLAG ).forEach( flagsElement ->
+                    {
+                        final String value = flagsElement.getText().orElse( "" ).trim();
+                        JavaHelper.readEnumFromString( PwmSettingFlag.class, value ).ifPresent( returnObj::add );
+                    } )
+            );
+            return Collections.unmodifiableSet( returnObj );
+        }
+
+        private static Map<String, String> readOptions( final PwmSetting pwmSetting, final XmlElement settingElement )
+        {
+            final Map<String, String> returnData = new LinkedHashMap<>();
+            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 )
+                    {
+                        final String value = optionElement.getAttribute( PwmSettingXml.XML_ELEMENT_VALUE )
+                                .orElseThrow( () -> new IllegalStateException( "option element is missing 'value' attribute for key " + pwmSetting.getKey() ) );
+
+                        optionElement.getText().ifPresent( textValue ->  returnData.put( value, textValue ) );
+                    }
+                }
+            }
+            return Collections.unmodifiableMap( returnData );
+        }
+
+        private static Collection<LDAPPermissionInfo> readLdapPermissionInfo( final XmlElement settingElement )
+        {
+            final List<LDAPPermissionInfo> returnObj = new ArrayList<>();
+
+            settingElement.getChild( PwmSettingXml.XML_ELEMENT_PERMISSION ).ifPresent( permissionElement ->
+            {
+                permissionElement.getChildren( PwmSettingXml.XML_ELEMENT_LDAP ).forEach( ldapElement ->
+                {
+                    final Optional<LDAPPermissionInfo.Actor> actor = JavaHelper.readEnumFromString(
+                            LDAPPermissionInfo.Actor.class,
+                            permissionElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACTOR ).orElse( "" ) );
+                    final Optional<LDAPPermissionInfo.Access> type = JavaHelper.readEnumFromString(
+                            LDAPPermissionInfo.Access.class,
+                            permissionElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACCESS ).orElse( "" ) );
+
+                    if ( actor.isPresent() && type.isPresent() )
+                    {
+                        returnObj.add( new LDAPPermissionInfo( type.get(), actor.get() ) );
+                    }
+                } );
+            } );
+
+            return Collections.unmodifiableList( returnObj );
+        }
+
+        private static List<TemplateSetReference<String>> readExamples( final XmlElement settingElement )
+        {
+            final List<TemplateSetReference<String>> returnObj = new ArrayList<>();
+            final MacroRequest macroRequest = MacroRequest.forStatic();
+            final List<XmlElement> exampleElements = settingElement.getChildren( PwmSettingXml.XML_ELEMENT_EXAMPLE );
+            for ( final XmlElement exampleElement : exampleElements )
+            {
+                final Set<PwmSettingTemplate> definedTemplates = PwmSettingXml.parseTemplateAttribute( exampleElement );
+                exampleElement.getText().ifPresent( textValue ->
+                {
+                    final String exampleString = macroRequest.expandMacros( textValue );
+                    returnObj.add( new TemplateSetReference<>( exampleString, Collections.unmodifiableSet( definedTemplates ) ) );
+                } );
+            }
+            if ( returnObj.isEmpty() )
+            {
+                returnObj.add( new TemplateSetReference<>( "", Collections.emptySet() ) );
+            }
+            return Collections.unmodifiableList( returnObj );
+        }
+
+        private static Map<PwmSettingProperty, String> readProperties( final PwmSetting pwmSetting, final XmlElement settingElement )
+        {
+            final Map<PwmSettingProperty, String> newProps = new EnumMap<>( PwmSettingProperty.class );
+            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 )
+                    {
+                        final String keyAttribute = propertyElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_KEY )
+                                .orElseThrow( () -> new IllegalStateException( "property element is missing 'key' attribute for value " + pwmSetting.getKey() ) );
+
+                        final PwmSettingProperty property = JavaHelper.readEnumFromString( PwmSettingProperty.class, keyAttribute )
+                                .orElseThrow( () -> new IllegalStateException( "property element has unknown 'key' attribute for value " + pwmSetting.getKey() ) );
+
+                        propertyElement.getText().ifPresent( value -> newProps.put( property, value ) );
+                    }
+                }
+            }
+            return Collections.unmodifiableMap( newProps );
+        }
+
+        private static boolean readRequired( final XmlElement settingElement )
+        {
+            final String requiredAttribute = settingElement.getAttribute( PwmSettingXml.XML_ELEMENT_REQUIRED ).orElse( "" );
+            return "true".equalsIgnoreCase( requiredAttribute );
+        }
+
+        private static boolean readHidden( final PwmSetting pwmSetting, final XmlElement settingElement )
+        {
+            final String requiredAttribute = settingElement.getAttribute( PwmSettingXml.XML_ELEMENT_HIDDEN ).orElse( "" );
+            return "true".equalsIgnoreCase( requiredAttribute ) || pwmSetting.getCategory().isHidden();
+        }
+
+        private static int readLevel( final XmlElement settingElement )
+        {
+            final String levelAttribute = settingElement.getAttribute( PwmSettingXml.XML_ELEMENT_LEVEL ).orElse( "" );
+            return JavaHelper.silentParseInt( levelAttribute, 0 );
+        }
+
+        private static Pattern readPattern( final PwmSetting pwmSetting, final XmlElement settingElement )
+        {
+            final Optional<XmlElement> regexNode = settingElement.getChild( PwmSettingXml.XML_ELEMENT_REGEX );
+            if ( regexNode.isPresent() )
+            {
+                final Optional<String> regexText = regexNode.get().getText();
+                if ( regexText.isPresent() )
+                {
+                    try
+                    {
+                        return Pattern.compile( regexText.get() );
+                    }
+                    catch ( final PatternSyntaxException e )
+                    {
+                        final String errorMsg = "error compiling regex constraints for setting " + pwmSetting + ", error: " + e.getMessage();
+                        throw new IllegalStateException( errorMsg, e );
+                    }
+                }
+            }
+            return Pattern.compile( ".*", Pattern.DOTALL );
+        }
+    }
+}

+ 0 - 430
server/src/main/java/password/pwm/config/PwmSettingMetaDataReader.java

@@ -1,430 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2021 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;
-
-import org.jrivard.xmlchai.XmlElement;
-import password.pwm.PwmConstants;
-import password.pwm.config.value.PasswordValue;
-import password.pwm.config.value.StoredValue;
-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.TimeDuration;
-import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroRequest;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.EnumSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Supplier;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-
-public class PwmSettingMetaDataReader
-{
-    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSettingMetaDataReader.class );
-
-    private final PwmSetting pwmSetting;
-
-    // cached values read from XML file
-    private final Supplier<List<PwmSetting.TemplateSetReference<StoredValue>>> defaultValues = new LazySupplier<>( () -> InternalReader.readDefaultValue( getPwmSetting() ) );
-    private final Supplier<List<PwmSetting.TemplateSetReference<String>>> examples = new LazySupplier<>( () -> InternalReader.readExamples( getPwmSetting() ) );
-    private final Supplier<Map<String, String>> options = new LazySupplier<>( () -> InternalReader.readOptions( getPwmSetting() ) );
-    private final Supplier<Set<PwmSettingFlag>> flags = new LazySupplier<>( () -> InternalReader.readFlags( getPwmSetting() ) );
-    private final Supplier<Map<PwmSettingProperty, String>> properties = new LazySupplier<>( () -> InternalReader.readProperties( getPwmSetting() ) );
-    private final Supplier<Collection<LDAPPermissionInfo>> ldapPermissionInfo = new LazySupplier<>( () -> InternalReader.readLdapPermissionInfo( getPwmSetting() ) );
-    private final Supplier<Boolean> required = new LazySupplier<>( () -> InternalReader.readRequired( getPwmSetting() ) );
-    private final Supplier<Boolean> hidden = new LazySupplier<>( () -> InternalReader.readHidden( getPwmSetting() ) );
-    private final Supplier<Integer> level = new LazySupplier<>( () -> InternalReader.readLevel( getPwmSetting() ) );
-    private final Supplier<Pattern> pattern = new LazySupplier<>( () -> InternalReader.readPattern( getPwmSetting() ) );
-    private final Supplier<String> defaultLocaleLabel = new LazySupplier<>( () -> InternalReader.readLabel( getPwmSetting(), PwmConstants.DEFAULT_LOCALE ) );
-    private final Supplier<String> defaultLocaleDescription = new LazySupplier<>( () -> InternalReader.readDescription( getPwmSetting(), PwmConstants.DEFAULT_LOCALE ) );
-    private final Supplier<String> defaultMenuLocation = new LazySupplier<>( () -> InternalReader.readMenuLocationDebugDefault( getPwmSetting() ) );
-
-    public PwmSettingMetaDataReader( final PwmSetting pwmSetting )
-    {
-        this.pwmSetting = pwmSetting;
-    }
-
-    public PwmSetting getPwmSetting()
-    {
-        return pwmSetting;
-    }
-
-    public Map<PwmSettingProperty, String> getProperties( )
-    {
-        return properties.get();
-    }
-
-    public Set<PwmSettingFlag> getFlags( )
-    {
-        return flags.get();
-    }
-
-    public Map<String, String> getOptions()
-    {
-        return options.get();
-    }
-
-
-    public String getLabel( final Locale locale )
-    {
-        if ( PwmConstants.DEFAULT_LOCALE.equals( locale ) )
-        {
-            return defaultLocaleLabel.get();
-        }
-
-        return InternalReader.readLabel( pwmSetting, locale );
-    }
-
-    public String getDescription( final Locale locale )
-    {
-        if ( PwmConstants.DEFAULT_LOCALE.equals( locale ) )
-        {
-            return defaultLocaleDescription.get();
-        }
-
-        return InternalReader.readDescription( pwmSetting, locale );
-    }
-
-    public String getExample( final PwmSettingTemplateSet template )
-    {
-         return PwmSetting.TemplateSetReference.referenceForTempleSet( examples.get(), template );
-    }
-
-    public boolean isRequired( )
-    {
-        return required.get();
-    }
-
-    public boolean isHidden( )
-    {
-        return hidden.get();
-    }
-
-    public int getLevel( )
-    {
-        return level.get();
-    }
-
-    public Pattern getRegExPattern( )
-    {
-        return pattern.get();
-    }
-
-    public Collection<LDAPPermissionInfo> getLDAPPermissionInfo()
-    {
-        return ldapPermissionInfo.get();
-    }
-
-    List<PwmSetting.TemplateSetReference<StoredValue>> getDefaultValue()
-    {
-        return defaultValues.get();
-    }
-
-    public String toMenuLocationDebug(
-            final String profileID,
-            final Locale locale
-    )
-    {
-        if ( PwmConstants.DEFAULT_LOCALE.equals( locale ) && StringUtil.isEmpty( profileID ) )
-        {
-            return defaultMenuLocation.get();
-        }
-        else
-        {
-            return InternalReader.readMenuLocationDebug( pwmSetting, profileID, locale );
-        }
-    }
-
-    /**
-     * Not required for normal operation, but executing this gets all the enum values populated form XML source.  If run prior to users accessing the settings
-     * module (particularly the config editor) it will increase the initial load performance significantly.  There are no side effects to calling this operation
-     * other than cache population.
-     */
-    public static void initCache()
-    {
-        final Instant startTime = Instant.now();
-        for ( final PwmSetting pwmSetting : EnumSet.allOf( PwmSetting.class ) )
-        {
-            pwmSetting.getProperties();
-            pwmSetting.getFlags();
-            pwmSetting.getOptions();
-            pwmSetting.getLabel( PwmConstants.DEFAULT_LOCALE );
-            pwmSetting.getDescription( PwmConstants.DEFAULT_LOCALE );
-            pwmSetting.getExample( PwmSettingTemplateSet.getDefault() );
-            pwmSetting.isRequired();
-            pwmSetting.isHidden();
-            pwmSetting.getLevel();
-            pwmSetting.getRegExPattern();
-            pwmSetting.getLDAPPermissionInfo();
-            pwmSetting.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
-        }
-        for ( final PwmSettingCategory pwmSettingCategory : EnumSet.allOf( PwmSettingCategory.class ) )
-        {
-            pwmSettingCategory.getLabel( PwmConstants.DEFAULT_LOCALE );
-            pwmSettingCategory.getDescription( PwmConstants.DEFAULT_LOCALE );
-            pwmSettingCategory.isHidden();
-            pwmSettingCategory.getLevel();
-            pwmSettingCategory.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
-            pwmSettingCategory.getScope();
-            pwmSettingCategory.getChildren();
-            pwmSettingCategory.getLevel();
-            pwmSettingCategory.getSettings();
-        }
-        LOGGER.trace( () -> "completed PwmSetting xml cache initialization", () -> TimeDuration.fromCurrent( startTime ) );
-    }
-
-    private static class InternalReader
-    {
-        private static Set<PwmSettingFlag> readFlags( final PwmSetting pwmSetting )
-        {
-            final Set<PwmSettingFlag> returnObj = EnumSet.noneOf( PwmSettingFlag.class );
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
-            final List<XmlElement> flagElements = settingElement.getChildren( "flag" );
-            for ( final XmlElement flagElement : flagElements )
-            {
-                final String value = flagElement.getText().orElse( "" ).trim();
-
-                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.unmodifiableSet( returnObj );
-        }
-
-        private static Map<String, String> readOptions( final PwmSetting pwmSetting )
-        {
-            final Map<String, String> returnData = 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 )
-                    {
-                        final String value = optionElement.getAttribute( PwmSettingXml.XML_ELEMENT_VALUE )
-                                .orElseThrow( () -> new IllegalStateException( "option element is missing 'value' attribute for key " + pwmSetting.getKey() ) );
-
-                        optionElement.getText().ifPresent( textValue ->  returnData.put( value, textValue ) );
-                    }
-                }
-            }
-            return Collections.unmodifiableMap( returnData );
-        }
-
-        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 Optional<LDAPPermissionInfo.Actor> actor = JavaHelper.readEnumFromString(
-                            LDAPPermissionInfo.Actor.class,
-                            permissionElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACTOR ).orElse( "" )
-                    );
-                    final Optional<LDAPPermissionInfo.Access> type = JavaHelper.readEnumFromString(
-                            LDAPPermissionInfo.Access.class,
-                            permissionElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACCESS ).orElse( "" )
-                    );
-                    if ( actor.isPresent() && type.isPresent() )
-                    {
-                        final LDAPPermissionInfo permissionInfo = new LDAPPermissionInfo( type.get(), actor.get() );
-                        returnObj.add( permissionInfo );
-                    }
-                }
-            }
-            return Collections.unmodifiableList( returnObj );
-        }
-
-        private static List<PwmSetting.TemplateSetReference<String>> readExamples( final PwmSetting pwmSetting )
-        {
-            final List<PwmSetting.TemplateSetReference<String>> returnObj = new ArrayList<>();
-            final MacroRequest macroRequest = MacroRequest.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 );
-                exampleElement.getText().ifPresent( textValue ->
-                {
-                    final String exampleString = macroRequest.expandMacros( textValue );
-                    returnObj.add( new PwmSetting.TemplateSetReference<>( exampleString, Collections.unmodifiableSet( definedTemplates ) ) );
-                } );
-            }
-            if ( returnObj.isEmpty() )
-            {
-                returnObj.add( new PwmSetting.TemplateSetReference<>( "", Collections.emptySet() ) );
-            }
-            return Collections.unmodifiableList( returnObj );
-        }
-
-        private static Map<PwmSettingProperty, String> readProperties( final PwmSetting pwmSetting )
-        {
-            final Map<PwmSettingProperty, String> newProps = new EnumMap<>( PwmSettingProperty.class );
-            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 )
-                    {
-                        final String keyAttribute = propertyElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_KEY )
-                                .orElseThrow( () -> new IllegalStateException( "property element is missing 'key' attribute for value " + pwmSetting.getKey() ) );
-
-                        final PwmSettingProperty property = JavaHelper.readEnumFromString( PwmSettingProperty.class, keyAttribute )
-                                .orElseThrow( () -> new IllegalStateException( "property element has unknown 'key' attribute for value " + pwmSetting.getKey() ) );
-
-                        propertyElement.getText().ifPresent( value -> newProps.put( property, value ) );
-                    }
-                }
-            }
-            return Collections.unmodifiableMap( newProps );
-        }
-
-        private static List<PwmSetting.TemplateSetReference<StoredValue>> readDefaultValue( final PwmSetting pwmSetting )
-        {
-            final List<PwmSetting.TemplateSetReference<StoredValue>> 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 PwmSetting.TemplateSetReference<>( 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 PwmSetting.TemplateSetReference<>( 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.getAttribute( PwmSettingXml.XML_ELEMENT_REQUIRED ).orElse( "" );
-            return "true".equalsIgnoreCase( requiredAttribute );
-        }
-
-        private static boolean readHidden( final PwmSetting pwmSetting )
-        {
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
-            final String requiredAttribute = settingElement.getAttribute( PwmSettingXml.XML_ELEMENT_HIDDEN ).orElse( "" );
-            return "true".equalsIgnoreCase( requiredAttribute ) || pwmSetting.getCategory().isHidden();
-        }
-
-        private static int readLevel( final PwmSetting pwmSetting )
-        {
-            final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
-            final String levelAttribute = settingElement.getAttribute( PwmSettingXml.XML_ELEMENT_LEVEL ).orElse( "" );
-            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() )
-            {
-                final Optional<String> regexText = regexNode.get().getText();
-                if ( regexText.isPresent() )
-                {
-                    try
-                    {
-                        return Pattern.compile( regexText.get() );
-                    }
-                    catch ( final PatternSyntaxException e )
-                    {
-                        final String errorMsg = "error compiling regex constraints for setting " + pwmSetting + ", error: " + e.getMessage();
-                        LOGGER.error( () -> errorMsg, e );
-                        throw new IllegalStateException( errorMsg, e );
-                    }
-                }
-            }
-            return Pattern.compile( ".*", Pattern.DOTALL );
-        }
-
-        private static String readMenuLocationDebugDefault( final PwmSetting pwmSetting )
-        {
-            final Locale locale = PwmConstants.DEFAULT_LOCALE;
-            final String separator = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
-            return pwmSetting.getCategory().toMenuLocationDebug( null, locale ) + separator + pwmSetting.getLabel( locale );
-        }
-
-        private static String readMenuLocationDebug( final PwmSetting pwmSetting, final String profileID, final Locale locale )
-        {
-            final String separator = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
-            return pwmSetting.getCategory().toMenuLocationDebug( profileID, locale ) + separator + pwmSetting.getLabel( locale );
-        }
-
-        private static String readLabel( final PwmSetting pwmSetting, final Locale locale )
-        {
-            return readStringProperty( password.pwm.i18n.PwmSetting.SETTING_LABEL_PREFIX + pwmSetting.getKey(), locale );
-        }
-
-        private static String readDescription( final PwmSetting pwmSetting, final Locale locale )
-        {
-            return readStringProperty( password.pwm.i18n.PwmSetting.SETTING_DESCRIPTION_PREFIX + pwmSetting.getKey(), locale );
-        }
-
-        private static String readStringProperty( final String key, final Locale locale )
-        {
-            final String storedText = LocaleHelper.getLocalizedMessage( locale, key, null, password.pwm.i18n.PwmSetting.class );
-            final MacroRequest macroRequest = MacroRequest.forStatic();
-            return macroRequest.expandMacros( storedText );
-        }
-    }
-}

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

@@ -20,8 +20,9 @@
 
 package password.pwm.config;
 
+import password.pwm.util.java.CollectionUtil;
+
 import java.util.Arrays;
-import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -41,7 +42,7 @@ public class PwmSettingStats
 
         returnObj.put( SettingStat.Total, PwmSetting.values().length );
 
-        returnObj.put( SettingStat.hasProfile, EnumSet.allOf( PwmSetting.class ).stream()
+        returnObj.put( SettingStat.hasProfile, CollectionUtil.enumStream( PwmSetting.class )
                 .filter( pwmSetting -> pwmSetting.getCategory().hasProfiles() )
                 .count() );
 

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

@@ -66,7 +66,7 @@ public enum PwmSettingTemplate
     public boolean isHidden( )
     {
         final XmlElement templateElement = readTemplateElement( this );
-        final Optional<String> requiredAttribute = templateElement.getAttribute( "hidden" );
+        final Optional<String> requiredAttribute = templateElement.getAttribute( PwmSettingXml.XML_ELEMENT_HIDDEN );
         return requiredAttribute.isPresent() && "true".equalsIgnoreCase( requiredAttribute.get() );
     }
 

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

@@ -24,9 +24,6 @@ import lombok.Value;
 import password.pwm.util.java.CollectionUtil;
 
 import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -44,10 +41,10 @@ public class PwmSettingTemplateSet implements Serializable
                 .map( PwmSettingTemplate::getType )
                 .collect( Collectors.toSet() );
 
-        workingSet.addAll( EnumSet.allOf( PwmSettingTemplate.Type.class ).stream()
+        workingSet.addAll( CollectionUtil.enumStream( PwmSettingTemplate.Type.class )
                 .filter( type -> !seenTypes.contains( type ) )
                 .map( PwmSettingTemplate.Type::getDefaultValue )
-                .collect( Collectors.toSet( ) ) );
+                .collect( Collectors.toUnmodifiableSet( ) ) );
 
         this.templates = Set.copyOf( workingSet );
     }
@@ -73,15 +70,8 @@ public class PwmSettingTemplateSet implements Serializable
      */
     public static List<PwmSettingTemplateSet> allValues()
     {
-        final List<PwmSettingTemplateSet> templateSets = new ArrayList<>();
-
-        for ( final PwmSettingTemplate template : EnumSet.allOf( PwmSettingTemplate.class ) )
-        {
-            final PwmSettingTemplateSet templateSet = new PwmSettingTemplateSet( Collections.singleton( template ) );
-            templateSets.add( templateSet );
-        }
-
-        templateSets.add( getDefault() );
-        return Collections.unmodifiableList( templateSets );
+        return CollectionUtil.enumStream( PwmSettingTemplate.class )
+                .map( pwmSettingTemplate -> new PwmSettingTemplateSet( Set.of( pwmSettingTemplate ) ) )
+                .collect( Collectors.toUnmodifiableList() );
     }
 }

+ 9 - 18
server/src/main/java/password/pwm/config/PwmSettingXml.java

@@ -34,6 +34,7 @@ import java.io.InputStream;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -42,7 +43,8 @@ public class PwmSettingXml
     public static final String SETTING_XML_FILENAME = ( PwmSetting.class.getPackage().getName()
             + "." + PwmSetting.class.getSimpleName() ).replace( '.', '/' ) + ".xml";
 
-    static final String XML_ELEMENT_LDAP_PERMISSION = "ldapPermission";
+    static final String XML_ELEMENT_PERMISSION = "permission";
+    static final String XML_ELEMENT_LDAP = "ldap";
     static final String XML_ELEMENT_EXAMPLE = "example";
     static final String XML_ELEMENT_DEFAULT = "default";
 
@@ -55,11 +57,15 @@ public class PwmSettingXml
     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_ELEMENT_FLAG = "flag";
+    static final String XML_ELEMENT_FLAGS = "flags";
+
     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";
     static final String XML_ELEMENT_SCOPE = "scope";
+    static final String XML_ELEMENT_SETTING = "setting";
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSettingXml.class );
 
@@ -82,26 +88,11 @@ public class PwmSettingXml
         }
     }
 
-    /*
-    private static void validateXmlSchema( )
+    static List<XmlElement> readAllSettingXmlElements()
     {
-        try
-        {
-            final InputStream xsdInputStream = PwmSetting.class.getClassLoader().getResourceAsStream( "password/pwm/config/PwmSetting.xsd" );
-            final InputStream xmlInputStream = PwmSetting.class.getClassLoader().getResourceAsStream( "password/pwm/config/PwmSetting.xml" );
-            final SchemaFactory factory = SchemaFactory.newInstance( XMLConstants.W3C_XML_SCHEMA_NS_URI );
-            final Schema schema = factory.newSchema( new StreamSource( xsdInputStream ) );
-            final Validator validator = schema.newValidator();
-            validator.validate( new StreamSource( xmlInputStream ) );
-        }
-        catch ( final Exception e )
-        {
-            throw new IllegalStateException( "error validating PwmSetting.xml schema using PwmSetting.xsd definition: " + e.getMessage() );
-        }
+        return XML_DOC_CACHE.get().getRootElement().getChildren( XML_ELEMENT_SETTING );
     }
 
-     */
-
     static XmlElement readSettingXml( final PwmSetting setting )
     {
         final String expression = "/settings/setting[@key=\"" + setting.getKey() + "\"]";

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

@@ -24,6 +24,7 @@ import password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.PrivateKeyCertificate;
+import password.pwm.bean.SessionLabel;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.profile.Profile;
 import password.pwm.config.profile.ProfileDefinition;
@@ -58,7 +59,6 @@ import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumMap;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -241,7 +241,7 @@ public class StoredSettingReader implements SettingReader
         )
         {
             final Map<ProfileDefinition, Map<String, Profile>> returnMap = new EnumMap<>( ProfileDefinition.class );
-            returnMap.putAll( EnumSet.allOf( ProfileDefinition.class ).stream()
+            returnMap.putAll( CollectionUtil.enumStream( ProfileDefinition.class )
                     .filter( profileDefinition -> domainID.inScope( profileDefinition.getCategory().getScope() ) )
                     .collect( CollectionUtil.collectorToLinkedMap(
                             profileDefinition -> profileDefinition,
@@ -321,7 +321,7 @@ public class StoredSettingReader implements SettingReader
 
         if ( setting.getFlags().contains( PwmSettingFlag.Deprecated ) )
         {
-            LOGGER.warn( () -> "attempt to read deprecated config setting: " + setting.toMenuLocationDebug( profileID, null ) );
+            LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "attempt to read deprecated config setting: " + setting.toMenuLocationDebug( profileID, null ) );
         }
 
         if ( StringUtil.isEmpty( profileID ) )

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

@@ -0,0 +1,70 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2021 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;
+
+import lombok.Value;
+import password.pwm.util.java.CollectionUtil;
+
+import java.util.List;
+import java.util.Set;
+
+@Value
+class TemplateSetReference<T>
+{
+    private final T reference;
+    private final Set<PwmSettingTemplate> settingTemplates;
+
+    static <T> T referenceForTempleSet(
+            final List<TemplateSetReference<T>> templateSetReferences,
+            final PwmSettingTemplateSet pwmSettingTemplateSet
+    )
+    {
+        final PwmSettingTemplateSet effectiveTemplateSet = pwmSettingTemplateSet == null
+                ? PwmSettingTemplateSet.getDefault()
+                : pwmSettingTemplateSet;
+
+        if ( templateSetReferences == null || templateSetReferences.isEmpty() )
+        {
+            throw new IllegalStateException( "templateSetReferences can not be null" );
+        }
+
+        if ( templateSetReferences.size() == 1 )
+        {
+            return templateSetReferences.get( 0 ).getReference();
+        }
+
+        for ( int matchCountExamSize = templateSetReferences.size(); matchCountExamSize > 0; matchCountExamSize-- )
+        {
+            for ( final TemplateSetReference<T> templateSetReference : templateSetReferences )
+            {
+                final Set<PwmSettingTemplate> temporarySet = CollectionUtil.copyToEnumSet( templateSetReference.getSettingTemplates(), PwmSettingTemplate.class );
+                temporarySet.retainAll( effectiveTemplateSet.getTemplates() );
+                final int matchCount = temporarySet.size();
+                if ( matchCount == matchCountExamSize )
+                {
+                    return templateSetReference.getReference();
+                }
+            }
+        }
+
+        return templateSetReferences.get( 0 ).getReference();
+    }
+}

+ 2 - 4
server/src/main/java/password/pwm/config/option/WebServiceUsage.java

@@ -20,13 +20,13 @@
 
 package password.pwm.config.option;
 
+import password.pwm.util.java.JavaHelper;
 import password.pwm.ws.server.RestAuthenticationType;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 public enum WebServiceUsage
 {
@@ -59,8 +59,6 @@ public enum WebServiceUsage
 
     public static Set<WebServiceUsage> forType( final RestAuthenticationType type )
     {
-        return EnumSet.allOf( WebServiceUsage.class ).stream()
-                .filter( webServiceUsage -> webServiceUsage.getTypes().contains( type ) )
-                .collect( Collectors.toUnmodifiableSet() );
+        return JavaHelper.readEnumsFromPredicate( WebServiceUsage.class, webServiceUsage -> webServiceUsage.getTypes().contains( type ) );
     }
 }

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

@@ -106,7 +106,7 @@ public class LdapProfile extends AbstractProfile implements Profile
     public String getDisplayName( final Locale locale )
     {
         final String displayName = readSettingAsLocalizedString( PwmSetting.LDAP_PROFILE_DISPLAY_NAME, locale );
-        return displayName == null || displayName.length() < 1 ? getIdentifier() : displayName;
+        return StringUtil.isTrimEmpty( displayName ) ? getIdentifier() : displayName;
     }
 
     public String getUsernameAttribute( )
@@ -182,7 +182,7 @@ public class LdapProfile extends AbstractProfile implements Profile
                 {
                     final String finalCanonical = canonicalValue;
                     LOGGER.trace( () -> "read and cached canonical ldap DN value for input '" + dnValue + "' as '" + finalCanonical + "'",
-                            () -> TimeDuration.fromCurrent( startTime ) );
+                            TimeDuration.fromCurrent( startTime ) );
                 }
             }
             catch ( final ChaiUnavailableException | ChaiOperationException e )

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

@@ -44,7 +44,6 @@ import password.pwm.util.password.PasswordRuleReaderHelper;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -52,7 +51,6 @@ 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.function.Supplier;
 import java.util.regex.Pattern;
@@ -73,12 +71,10 @@ public class PwmPasswordPolicy implements Profile, Serializable
     private final transient Supplier<List<HealthRecord>> healthChecker = new LazySupplier<>( () -> doHealthChecks( this ) );
     private final transient ChaiPasswordPolicy chaiPasswordPolicy;
 
-    private final DomainID domainID;
     private final Map<String, String> policyMap;
     private final PolicyMetaData policyMetaData;
 
     private PwmPasswordPolicy(
-            final DomainID domainID,
             final Map<String, String> policyMap,
             final ChaiPasswordPolicy chaiPasswordPolicy,
             final PolicyMetaData policyMetaData
@@ -101,10 +97,9 @@ public class PwmPasswordPolicy implements Profile, Serializable
             }
         }
 
-        this.domainID = domainID;
         this.chaiPasswordPolicy = chaiPasswordPolicy;
         this.policyMetaData = policyMetaData == null ? PolicyMetaData.builder().build() : policyMetaData;
-        this.policyMap = Map.copyOf( effectivePolicyMap );
+        this.policyMap = Collections.unmodifiableMap( new TreeMap<>( effectivePolicyMap ) );
     }
 
     public static PwmPasswordPolicy createPwmPasswordPolicy(
@@ -119,17 +114,17 @@ public class PwmPasswordPolicy implements Profile, Serializable
             final ChaiPasswordPolicy chaiPasswordPolicy
     )
     {
-        return new PwmPasswordPolicy( domainID, policyMap, chaiPasswordPolicy, null );
+        final PolicyMetaData policyMetaData = PolicyMetaData.builder().domainID( domainID ).build();
+        return new PwmPasswordPolicy( policyMap, chaiPasswordPolicy, policyMetaData );
     }
 
     public static PwmPasswordPolicy createPwmPasswordPolicy(
-            final DomainID domainID,
             final Map<String, String> policyMap,
             final ChaiPasswordPolicy chaiPasswordPolicy,
             final PolicyMetaData policyMetaData
     )
     {
-        return new PwmPasswordPolicy( domainID, policyMap, chaiPasswordPolicy, policyMetaData );
+        return new PwmPasswordPolicy( policyMap, chaiPasswordPolicy, policyMetaData );
     }
 
     public static PwmPasswordPolicy createPwmPasswordPolicy(
@@ -158,14 +153,15 @@ public class PwmPasswordPolicy implements Profile, Serializable
         }
 
         // set pwm-specific values
-        final PwmPasswordPolicy.PolicyMetaData policyMetaData = PwmPasswordPolicy.PolicyMetaData.builder()
+        final PwmPasswordPolicy.PolicyMetaData policyMetaData = PolicyMetaData.builder()
                 .profileID( profileID )
+                .domainID( domainConfig.getDomainID() )
                 .userPermissions( settingReader.readSettingAsUserPermission( PwmSetting.PASSWORD_POLICY_QUERY_MATCH ) )
                 .ruleText( readLocalizedSetting( PwmSetting.PASSWORD_POLICY_RULE_TEXT, domainConfig, settingReader ) )
                 .changePasswordText( readLocalizedSetting( PwmSetting.PASSWORD_POLICY_CHANGE_MESSAGE, domainConfig, settingReader ) )
                 .build();
 
-        return PwmPasswordPolicy.createPwmPasswordPolicy( domainConfig.getDomainID(), passwordPolicySettings, null, policyMetaData );
+        return PwmPasswordPolicy.createPwmPasswordPolicy( passwordPolicySettings, null, policyMetaData );
     }
 
 
@@ -207,7 +203,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
 
     public DomainID getDomainID()
     {
-        return domainID;
+        return policyMetaData == null ? null : policyMetaData.getDomainID();
     }
 
     private static PwmPasswordPolicy makeDefaultPolicy()
@@ -215,9 +211,10 @@ public class PwmPasswordPolicy implements Profile, Serializable
         PwmPasswordPolicy newDefaultPolicy = null;
         try
         {
-            final Map<String, String> defaultPolicyMap = EnumSet.allOf( PwmPasswordRule.class ).stream().collect( Collectors.toUnmodifiableMap(
-                    PwmPasswordRule::getKey,
-                    PwmPasswordRule::getDefaultValue ) );
+            final Map<String, String> defaultPolicyMap = CollectionUtil.enumStream( PwmPasswordRule.class )
+                    .collect( Collectors.toUnmodifiableMap(
+                            PwmPasswordRule::getKey,
+                            PwmPasswordRule::getDefaultValue ) );
 
             newDefaultPolicy = createPwmPasswordPolicy( DomainID.systemId(), defaultPolicyMap, null );
         }
@@ -288,26 +285,29 @@ public class PwmPasswordPolicy implements Profile, Serializable
             return this;
         }
 
-        final Set<PwmPasswordRule> pwmPasswordRules = EnumSet.allOf( PwmPasswordRule.class );
-        final Map<String, String> newPasswordPolicies = new HashMap<>( pwmPasswordRules.size() );
-
-        for ( final PwmPasswordRule rule : pwmPasswordRules )
-        {
-            final String ruleKey = rule.getKey();
-            final String value1 = this.policyMap.get( ruleKey );
-            final String value2 = otherPolicy.policyMap.get( ruleKey );
-
-            if ( StringUtil.notEmpty( value1 ) || StringUtil.notEmpty( value2 ) )
-            {
-                final PwmPasswordRuleFunctions.RuleMergeFunction ruleMergeFunction
-                        = PwmPasswordRuleFunctions.RULE_MERGE_FUNCTIONS.getOrDefault( rule, PwmPasswordRuleFunctions.DEFAULT_RULE_MERGE_SINGLETON );
-                ruleMergeFunction.apply( rule, value1, value2 ).ifPresent( value -> newPasswordPolicies.put( ruleKey, value ) );
-            }
-        }
+        final Map<String, String> newPasswordPolicies = CollectionUtil.enumStream( PwmPasswordRule.class )
+                .map( rule -> Map.entry( rule, mergeValue( otherPolicy, rule ) ) )
+                .filter( entry -> entry.getValue().isPresent() )
+                .collect( Collectors.toUnmodifiableMap( entry -> entry.getKey().getKey(), entry -> entry.getValue().get() ) );
 
         final ChaiPasswordPolicy backingPolicy = this.chaiPasswordPolicy != null ? chaiPasswordPolicy : otherPolicy.chaiPasswordPolicy;
         final PolicyMetaData metaData = getPolicyMetaData().merge( otherPolicy.getPolicyMetaData() );
-        return new PwmPasswordPolicy( domainID, newPasswordPolicies, backingPolicy, metaData );
+        return new PwmPasswordPolicy( newPasswordPolicies, backingPolicy, metaData );
+    }
+
+    private Optional<String> mergeValue( final PwmPasswordPolicy otherPolicy, final PwmPasswordRule rule )
+    {
+        final String ruleKey = rule.getKey();
+        final String thisValue = this.policyMap.get( ruleKey );
+        final String otherValue = otherPolicy.policyMap.get( ruleKey );
+
+        if ( thisValue != null || otherValue != null )
+        {
+            final PwmPasswordRuleFunctions.RuleMergeFunction ruleMergeFunction
+                    = PwmPasswordRuleFunctions.RULE_MERGE_FUNCTIONS.getOrDefault( rule, PwmPasswordRuleFunctions.DEFAULT_RULE_MERGE_SINGLETON );
+            return ruleMergeFunction.apply( rule, thisValue, otherValue );
+        }
+        return Optional.empty();
     }
 
     private PolicyMetaData getPolicyMetaData()
@@ -315,9 +315,6 @@ public class PwmPasswordPolicy implements Profile, Serializable
         return policyMetaData;
     }
 
-
-
-
     public Map<String, String> getPolicyMap( )
     {
         return policyMap;
@@ -401,6 +398,8 @@ public class PwmPasswordPolicy implements Profile, Serializable
     @Builder
     public static class PolicyMetaData implements Serializable
     {
+        private final DomainID domainID;
+
         private final String profileID;
 
         @Builder.Default
@@ -418,6 +417,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
                     .changePasswordText( CollectionUtil.isEmpty( changePasswordText ) ? otherPolicy.changePasswordText : changePasswordText )
                     .userPermissions( CollectionUtil.isEmpty( userPermissions ) ? otherPolicy.userPermissions : userPermissions )
                     .profileID( StringUtil.isEmpty( profileID ) ? otherPolicy.profileID : profileID )
+                    .domainID( domainID == null ? otherPolicy.domainID : domainID )
                     .build();
         }
     }

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

@@ -26,6 +26,7 @@ import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Message;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
 
 import java.util.List;
 import java.util.Locale;
@@ -44,259 +45,228 @@ public enum PwmPasswordRule
             null,
             ChaiPasswordRule.PolicyEnabled.getRuleType(),
             ChaiPasswordRule.PolicyEnabled.getDefaultValue(),
-            true ),
+            Flag.positiveBooleanMerge ),
 
     MinimumLength(
             ChaiPasswordRule.MinimumLength,
             PwmSetting.PASSWORD_POLICY_MINIMUM_LENGTH,
             ChaiPasswordRule.MinimumLength.getRuleType(),
-            ChaiPasswordRule.MinimumLength.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumLength.getDefaultValue() ),
 
     MaximumLength(
             ChaiPasswordRule.MaximumLength,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_LENGTH,
             ChaiPasswordRule.MaximumLength.getRuleType(),
-            ChaiPasswordRule.MaximumLength.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumLength.getDefaultValue() ),
 
     MinimumUpperCase(
             ChaiPasswordRule.MinimumUpperCase,
             PwmSetting.PASSWORD_POLICY_MINIMUM_UPPERCASE,
             ChaiPasswordRule.MinimumUpperCase.getRuleType(),
-            ChaiPasswordRule.MinimumUpperCase.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumUpperCase.getDefaultValue() ),
 
     MaximumUpperCase(
             ChaiPasswordRule.MaximumUpperCase,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_UPPERCASE,
             ChaiPasswordRule.MaximumUpperCase.getRuleType(),
-            ChaiPasswordRule.MaximumUpperCase.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumUpperCase.getDefaultValue() ),
 
     MinimumLowerCase(
             ChaiPasswordRule.MinimumLowerCase,
             PwmSetting.PASSWORD_POLICY_MINIMUM_LOWERCASE,
             ChaiPasswordRule.MinimumLowerCase.getRuleType(),
-            ChaiPasswordRule.MinimumLowerCase.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumLowerCase.getDefaultValue() ),
 
     MaximumLowerCase(
             ChaiPasswordRule.MaximumLowerCase,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_LOWERCASE,
             ChaiPasswordRule.MaximumLowerCase.getRuleType(),
-            ChaiPasswordRule.MaximumLowerCase.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumLowerCase.getDefaultValue() ),
 
     AllowNumeric(
             ChaiPasswordRule.AllowNumeric,
             PwmSetting.PASSWORD_POLICY_ALLOW_NUMERIC,
             ChaiPasswordRule.AllowNumeric.getRuleType(),
-            ChaiPasswordRule.AllowNumeric.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowNumeric.getDefaultValue() ),
 
     MinimumNumeric(
             ChaiPasswordRule.MinimumNumeric,
             PwmSetting.PASSWORD_POLICY_MINIMUM_NUMERIC,
             ChaiPasswordRule.MinimumNumeric.getRuleType(),
-            ChaiPasswordRule.MinimumNumeric.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumNumeric.getDefaultValue() ),
 
     MaximumNumeric(
             ChaiPasswordRule.MaximumNumeric,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_NUMERIC,
             ChaiPasswordRule.MaximumNumeric.getRuleType(),
-            ChaiPasswordRule.MaximumNumeric.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumNumeric.getDefaultValue() ),
 
     MinimumUnique(
             ChaiPasswordRule.MinimumUnique,
             PwmSetting.PASSWORD_POLICY_MINIMUM_UNIQUE,
             ChaiPasswordRule.MinimumUnique.getRuleType(),
-            ChaiPasswordRule.MinimumUnique.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumUnique.getDefaultValue() ),
 
     MaximumUnique(
             ChaiPasswordRule.MaximumUnique,
             null,
             ChaiPasswordRule.MaximumUnique.getRuleType(),
-            ChaiPasswordRule.MaximumUnique.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumUnique.getDefaultValue() ),
 
     AllowFirstCharNumeric(
             ChaiPasswordRule.AllowFirstCharNumeric,
             PwmSetting.PASSWORD_POLICY_ALLOW_FIRST_CHAR_NUMERIC,
             ChaiPasswordRule.AllowFirstCharNumeric.getRuleType(),
-            ChaiPasswordRule.AllowFirstCharNumeric.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowFirstCharNumeric.getDefaultValue() ),
 
     AllowLastCharNumeric(
             ChaiPasswordRule.AllowLastCharNumeric,
             PwmSetting.PASSWORD_POLICY_ALLOW_LAST_CHAR_NUMERIC,
             ChaiPasswordRule.AllowLastCharNumeric.getRuleType(),
-            ChaiPasswordRule.AllowLastCharNumeric.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowLastCharNumeric.getDefaultValue() ),
 
     AllowSpecial(
             ChaiPasswordRule.AllowSpecial,
             PwmSetting.PASSWORD_POLICY_ALLOW_SPECIAL,
             ChaiPasswordRule.AllowSpecial.getRuleType(),
-            ChaiPasswordRule.AllowSpecial.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowSpecial.getDefaultValue() ),
 
     MinimumSpecial(
             ChaiPasswordRule.MinimumSpecial,
             PwmSetting.PASSWORD_POLICY_MINIMUM_SPECIAL,
             ChaiPasswordRule.MinimumSpecial.getRuleType(),
-            ChaiPasswordRule.MinimumSpecial.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumSpecial.getDefaultValue() ),
 
     MaximumSpecial(
             ChaiPasswordRule.MaximumSpecial,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_SPECIAL,
             ChaiPasswordRule.MaximumSpecial.getRuleType(),
-            ChaiPasswordRule.MaximumSpecial.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumSpecial.getDefaultValue() ),
 
     AllowFirstCharSpecial(
             ChaiPasswordRule.AllowFirstCharSpecial,
             PwmSetting.PASSWORD_POLICY_ALLOW_FIRST_CHAR_SPECIAL,
             ChaiPasswordRule.AllowFirstCharSpecial.getRuleType(),
-            ChaiPasswordRule.AllowFirstCharSpecial.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowFirstCharSpecial.getDefaultValue() ),
 
     AllowLastCharSpecial(
             ChaiPasswordRule.AllowLastCharSpecial,
             PwmSetting.PASSWORD_POLICY_ALLOW_LAST_CHAR_SPECIAL,
             ChaiPasswordRule.AllowLastCharSpecial.getRuleType(),
-            ChaiPasswordRule.AllowLastCharSpecial.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowLastCharSpecial.getDefaultValue() ),
 
     MaximumRepeat(
             ChaiPasswordRule.MaximumRepeat,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_REPEAT,
             ChaiPasswordRule.MaximumRepeat.getRuleType(),
-            ChaiPasswordRule.MaximumRepeat.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumRepeat.getDefaultValue() ),
 
     MaximumSequentialRepeat(
             ChaiPasswordRule.MaximumSequentialRepeat,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_SEQUENTIAL_REPEAT,
             ChaiPasswordRule.MaximumSequentialRepeat.getRuleType(),
-            ChaiPasswordRule.MaximumSequentialRepeat.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumSequentialRepeat.getDefaultValue() ),
 
     ChangeMessage(
             ChaiPasswordRule.ChangeMessage,
             PwmSetting.PASSWORD_POLICY_CHANGE_MESSAGE,
             ChaiPasswordRule.ChangeMessage.getRuleType(),
-            ChaiPasswordRule.ChangeMessage.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.ChangeMessage.getDefaultValue() ),
 
     ExpirationInterval(
             ChaiPasswordRule.ExpirationInterval,
             null,
             ChaiPasswordRule.ExpirationInterval.getRuleType(),
-            ChaiPasswordRule.ExpirationInterval.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.ExpirationInterval.getDefaultValue() ),
 
     MinimumLifetime(
             ChaiPasswordRule.MinimumLifetime,
             PwmSetting.PASSWORD_POLICY_MINIMUM_LIFETIME,
             ChaiPasswordRule.MinimumLifetime.getRuleType(),
-            ChaiPasswordRule.MinimumLifetime.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumLifetime.getDefaultValue() ),
 
     CaseSensitive(
             ChaiPasswordRule.CaseSensitive,
             null,
             ChaiPasswordRule.CaseSensitive.getRuleType(),
             ChaiPasswordRule.CaseSensitive.getDefaultValue(),
-            true ),
+            Flag.positiveBooleanMerge ),
 
     EnforceAtLogin(
             ChaiPasswordRule.EnforceAtLogin,
             null,
             ChaiPasswordRule.EnforceAtLogin.getRuleType(),
-            ChaiPasswordRule.EnforceAtLogin.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.EnforceAtLogin.getDefaultValue() ),
 
     ChallengeResponseEnabled(
             ChaiPasswordRule.ChallengeResponseEnabled,
             null,
             ChaiPasswordRule.ChallengeResponseEnabled.getRuleType(),
-            ChaiPasswordRule.ChallengeResponseEnabled.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.ChallengeResponseEnabled.getDefaultValue() ),
 
     UniqueRequired(
             ChaiPasswordRule.UniqueRequired,
             null,
             ChaiPasswordRule.UniqueRequired.getRuleType(),
             ChaiPasswordRule.UniqueRequired.getDefaultValue(),
-            true ),
+            Flag.positiveBooleanMerge ),
 
     DisallowedValues(
             ChaiPasswordRule.DisallowedValues,
             PwmSetting.PASSWORD_POLICY_DISALLOWED_VALUES,
             ChaiPasswordRule.DisallowedValues.getRuleType(),
-            ChaiPasswordRule.DisallowedValues.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.DisallowedValues.getDefaultValue() ),
 
     DisallowedAttributes(
             ChaiPasswordRule.DisallowedAttributes,
             PwmSetting.PASSWORD_POLICY_DISALLOWED_ATTRIBUTES,
             ChaiPasswordRule.DisallowedAttributes.getRuleType(),
-            ChaiPasswordRule.DisallowedAttributes.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.DisallowedAttributes.getDefaultValue() ),
 
     DisallowCurrent(
             null,
             PwmSetting.PASSWORD_POLICY_DISALLOW_CURRENT,
             ChaiPasswordRule.RuleType.BOOLEAN,
             "false",
-            true ),
+            Flag.positiveBooleanMerge ),
 
     AllowUserChange(
             ChaiPasswordRule.AllowUserChange,
             null,
             ChaiPasswordRule.AllowUserChange.getRuleType(),
             ChaiPasswordRule.AllowUserChange.getDefaultValue(),
-            true ),
+            Flag.positiveBooleanMerge ),
 
     AllowAdminChange(
             ChaiPasswordRule.AllowAdminChange,
             null,
             ChaiPasswordRule.AllowAdminChange.getRuleType(),
             ChaiPasswordRule.AllowAdminChange.getDefaultValue(),
-            true ),
+            Flag.positiveBooleanMerge ),
 
     ADComplexityMaxViolations(
             ChaiPasswordRule.ADComplexityMaxViolation,
             PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_MAX_VIOLATIONS,
             ChaiPasswordRule.ADComplexityMaxViolation.getRuleType(),
-            ChaiPasswordRule.ADComplexityMaxViolation.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.ADComplexityMaxViolation.getDefaultValue() ),
 
     AllowNonAlpha(
             null,
             PwmSetting.PASSWORD_POLICY_ALLOW_NON_ALPHA,
             ChaiPasswordRule.AllowNonAlpha.getRuleType(),
-            ChaiPasswordRule.AllowNonAlpha.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowNonAlpha.getDefaultValue() ),
 
     MinimumNonAlpha(
             null,
             PwmSetting.PASSWORD_POLICY_MINIMUM_NON_ALPHA,
             ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
+            "0" ),
 
     MaximumNonAlpha(
             null,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_NON_ALPHA,
             ChaiPasswordRule.RuleType.MAX,
-            "0",
-            false ),
+            "0" ),
 
     // pwm specific rules
     // value will be imported indirectly from chai rule
@@ -304,86 +274,73 @@ public enum PwmPasswordRule
             null,
             PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL,
             ChaiPasswordRule.RuleType.OTHER,
-            "NONE",
-            false ),
+            "NONE" ),
 
     MaximumOldChars(
             null,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_OLD_PASSWORD_CHARS,
             ChaiPasswordRule.RuleType.NUMERIC,
-            "",
-            false ),
+            "" ),
 
     RegExMatch(
             null,
             PwmSetting.PASSWORD_POLICY_REGULAR_EXPRESSION_MATCH,
             ChaiPasswordRule.RuleType.OTHER,
-            "",
-            false ),
+            "" ),
 
     RegExNoMatch(
             null,
             PwmSetting.PASSWORD_POLICY_REGULAR_EXPRESSION_NOMATCH,
             ChaiPasswordRule.RuleType.OTHER,
-            "",
-            false
-    ),
+            "" ),
 
     MinimumAlpha(
             null,
             PwmSetting.PASSWORD_POLICY_MINIMUM_ALPHA,
             ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
+            "0" ),
 
     MaximumAlpha(
             null,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_ALPHA,
             ChaiPasswordRule.RuleType.MAX,
-            "0",
-            false
-    ),
+            "0" ),
 
     EnableWordlist(
             null,
             PwmSetting.PASSWORD_POLICY_ENABLE_WORDLIST,
             ChaiPasswordRule.RuleType.BOOLEAN,
-            "true",
-            true ),
+            "false",
+            Flag.positiveBooleanMerge ),
 
     MinimumStrength(
             null,
             PwmSetting.PASSWORD_POLICY_MINIMUM_STRENGTH,
             ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
+            "0" ),
 
     MaximumConsecutive(
             null,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_CONSECUTIVE,
             ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
+            "0" ),
 
     CharGroupsMinMatch(
             null,
             PwmSetting.PASSWORD_POLICY_CHAR_GROUPS_MIN_MATCH,
             ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
+            "0" ),
 
     CharGroupsValues(
             null,
             PwmSetting.PASSWORD_POLICY_CHAR_GROUPS,
             ChaiPasswordRule.RuleType.OTHER,
-            "",
-            false ),
+            "" ),
 
     AllowMacroInRegExSetting(
             AppProperty.ALLOW_MACRO_IN_REGEX_SETTING,
             ChaiPasswordRule.RuleType.BOOLEAN,
-            "true",
-            false ),;
+            "true" ),;
 
     private final ChaiPasswordRule chaiPasswordRule;
     private final PwmSetting pwmSetting;
@@ -392,12 +349,17 @@ public enum PwmPasswordRule
     private final String defaultValue;
     private final boolean positiveBooleanMerge;
 
+    private enum Flag
+    {
+        positiveBooleanMerge,
+    }
+
     PwmPasswordRule(
             final ChaiPasswordRule chaiPasswordRule,
             final PwmSetting pwmSetting,
             final ChaiPasswordRule.RuleType ruleType,
             final String defaultValue,
-            final boolean positiveBooleanMerge
+            final Flag... flags
     )
     {
         this.pwmSetting = pwmSetting;
@@ -405,14 +367,14 @@ public enum PwmPasswordRule
         this.appProperty = null;
         this.ruleType = ruleType;
         this.defaultValue = defaultValue;
-        this.positiveBooleanMerge = positiveBooleanMerge;
+        this.positiveBooleanMerge = JavaHelper.enumArrayContainsValue( flags, Flag.positiveBooleanMerge );
     }
 
     PwmPasswordRule(
             final AppProperty appProperty,
             final ChaiPasswordRule.RuleType ruleType,
             final String defaultValue,
-            final boolean positiveBooleanMerge
+            final Flag... flags
     )
     {
         this.pwmSetting = null;
@@ -420,7 +382,7 @@ public enum PwmPasswordRule
         this.appProperty = appProperty;
         this.ruleType = ruleType;
         this.defaultValue = defaultValue;
-        this.positiveBooleanMerge = positiveBooleanMerge;
+        this.positiveBooleanMerge = JavaHelper.enumArrayContainsValue( flags, Flag.positiveBooleanMerge );
     }
 
     public String getKey( )

+ 11 - 10
server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java

@@ -22,6 +22,7 @@ package password.pwm.config.stored;
 
 import password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingTemplateSet;
@@ -112,7 +113,7 @@ public class ConfigurationCleaner
             }
             catch ( final PwmUnrecoverableException e )
             {
-                LOGGER.error( () -> "error converting deprecated AD password policy setting: " + key + ", error: " + e.getMessage() );
+                LOGGER.error( SessionLabel.SYSTEM_LABEL, () -> "error converting deprecated AD password policy setting: " + key + ", error: " + e.getMessage() );
             }
         }
     }
@@ -138,7 +139,7 @@ public class ConfigurationCleaner
                                 : new StringValue( RecoveryMinLifetimeOption.ALLOW.name() );
                         final Optional<ValueMetaData> existingData = oldConfig.readSettingMetadata( key );
                         final UserIdentity newActor = existingData.map( ValueMetaData::getUserIdentity ).orElse( null );
-                        LOGGER.info( () -> "converting deprecated non-default setting "
+                        LOGGER.info( SessionLabel.SYSTEM_LABEL, () -> "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="
@@ -164,7 +165,7 @@ public class ConfigurationCleaner
                 final StoredConfigKey existingPubWebservicesKey = StoredConfigKey.forSetting( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null, domainID );
                 if ( oldConfig.readStoredValue( existingPubWebservicesKey ).isPresent() )
                 {
-                    LOGGER.info( () -> "converting deprecated non-default setting "
+                    LOGGER.info( SessionLabel.SYSTEM_LABEL, () -> "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 StoredConfigKey existingPubEnableKey = StoredConfigKey.forSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null, domainID );
@@ -221,7 +222,7 @@ public class ConfigurationCleaner
                     }
                     catch ( final PwmUnrecoverableException e )
                     {
-                        LOGGER.warn( () -> "error moving setting " + pwmSetting.getKey() + " without profile attribute to profile \"" + destProfile
+                        LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "error moving setting " + pwmSetting.getKey() + " without profile attribute to profile \"" + destProfile
                                 + "\", error: " + e.getMessage() );
                     }
                 }
@@ -229,12 +230,12 @@ public class ConfigurationCleaner
 
             try
             {
-                LOGGER.info( () -> "removing setting " + key + " without profile" );
+                LOGGER.info( SessionLabel.SYSTEM_LABEL, () -> "removing setting " + key + " without profile" );
                 modifier.deleteKey( key );
             }
             catch ( final PwmUnrecoverableException e )
             {
-                LOGGER.warn( () -> "error deleting setting " + pwmSetting.getKey() + " after adding profile settings: " + e.getMessage() );
+                LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "error deleting setting " + pwmSetting.getKey() + " after adding profile settings: " + e.getMessage() );
             }
         }
     }
@@ -265,12 +266,12 @@ public class ConfigurationCleaner
         {
             try
             {
-                LOGGER.info( () -> "removing setting " + key.toString() + " with non-existing profileID" );
+                LOGGER.info( SessionLabel.SYSTEM_LABEL, () -> "removing setting " + key.toString() + " with non-existing profileID" );
                 modifier.deleteKey( key );
             }
             catch ( final PwmUnrecoverableException e )
             {
-                LOGGER.warn( () -> "error deleting setting " + key.toString() + " with non-existing profileID: " + e.getMessage() );
+                LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "error deleting setting " + key.toString() + " with non-existing profileID: " + e.getMessage() );
             }
         }
     }
@@ -301,12 +302,12 @@ public class ConfigurationCleaner
             try
             {
                 final StoredValue value = inputConfig.readStoredValue( key ).orElseThrow();
-                LOGGER.info( () -> "removing setting " + key.toString() + " with default value: " + value.toDebugString( PwmConstants.DEFAULT_LOCALE ) );
+                LOGGER.info( SessionLabel.SYSTEM_LABEL, () -> "removing setting " + key.toString() + " with default value: " + value.toDebugString( PwmConstants.DEFAULT_LOCALE ) );
                 modifier.deleteKey( key );
             }
             catch ( final PwmUnrecoverableException e )
             {
-                LOGGER.warn( () -> "error deleting setting " + key.toString() + " with default value: " + e.getMessage() );
+                LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "error deleting setting " + key.toString() + " with default value: " + e.getMessage() );
             }
         }
     }

+ 18 - 17
server/src/main/java/password/pwm/config/stored/ConfigurationFileManager.java

@@ -67,6 +67,8 @@ public class ConfigurationFileManager
 
     private final File configFile;
     private final String configFileChecksum;
+    private final SessionLabel sessionLabel;
+
     private AppConfig domainConfig;
     private StoredConfiguration storedConfiguration;
     private ErrorInformation configFileError;
@@ -75,9 +77,11 @@ public class ConfigurationFileManager
 
     private volatile boolean saveInProgress;
 
-    public ConfigurationFileManager( final File configFile ) throws PwmUnrecoverableException
+    public ConfigurationFileManager( final File configFile, final SessionLabel sessionLabel )
+            throws PwmUnrecoverableException
     {
         this.configFile = configFile;
+        this.sessionLabel = sessionLabel;
 
         this.configFileChecksum = readFileChecksum( configFile );
         try
@@ -88,7 +92,7 @@ public class ConfigurationFileManager
         catch ( final PwmUnrecoverableException e )
         {
             this.configFileError = e.getErrorInformation();
-            LOGGER.warn( () -> "error reading configuration file: " + e.getMessage() );
+            LOGGER.warn( sessionLabel, () -> "error reading configuration file: " + e.getMessage() );
         }
 
         if ( storedConfiguration == null )
@@ -96,7 +100,7 @@ public class ConfigurationFileManager
             this.storedConfiguration = StoredConfigurationFactory.newConfig();
         }
 
-        LOGGER.debug( () -> "configuration mode: " + configMode );
+        LOGGER.debug( sessionLabel, () -> "configuration mode: " + configMode );
     }
 
     public PwmApplicationMode getConfigMode( )
@@ -123,11 +127,11 @@ public class ConfigurationFileManager
 
     private StoredConfiguration readStoredConfig( ) throws PwmUnrecoverableException
     {
-        LOGGER.debug( () -> "loading configuration file: " + configFile );
+        LOGGER.debug( sessionLabel, () -> "loading configuration file: " + configFile );
 
         if ( !configFile.exists() )
         {
-            LOGGER.warn( () -> "configuration file '" + configFile.getAbsolutePath() + "' does not exist" );
+            LOGGER.warn( sessionLabel, () -> "configuration file '" + configFile.getAbsolutePath() + "' does not exist" );
             return null;
         }
 
@@ -180,7 +184,7 @@ public class ConfigurationFileManager
         }
 
         final String fileSize = StringUtil.formatDiskSize( configFile.length() );
-        LOGGER.debug( () -> "configuration reading/parsing of " + fileSize + " complete", () -> TimeDuration.fromCurrent( startTime ) );
+        LOGGER.debug( sessionLabel, () -> "configuration reading/parsing of " + fileSize + " complete", TimeDuration.fromCurrent( startTime ) );
 
 
         final Optional<String> configIsEditable = storedConfiguration.readConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE );
@@ -198,8 +202,7 @@ public class ConfigurationFileManager
 
     public void saveConfiguration(
             final StoredConfiguration storedConfiguration,
-            final PwmApplication pwmApplication,
-            final SessionLabel sessionLabel
+            final PwmApplication pwmApplication
     )
             throws IOException, PwmUnrecoverableException, PwmOperationalException
     {
@@ -248,12 +251,12 @@ public class ConfigurationFileManager
 
         if ( pwmApplication != null && pwmApplication.getAuditService() != null )
         {
-            auditModifiedSettings( pwmApplication, storedConfiguration, sessionLabel );
+            auditModifiedSettings( pwmApplication, storedConfiguration );
         }
 
         try
         {
-            outputConfigurationFile( storedConfiguration, pwmApplication, sessionLabel, backupRotations, backupDirectory );
+            outputConfigurationFile( storedConfiguration, pwmApplication, backupRotations, backupDirectory );
         }
         finally
         {
@@ -261,8 +264,7 @@ public class ConfigurationFileManager
         }
     }
 
-    private static void auditModifiedSettings( final PwmApplication pwmApplication, final StoredConfiguration newConfig, final SessionLabel sessionLabel )
-            throws PwmUnrecoverableException
+    private void auditModifiedSettings( final PwmApplication pwmApplication, final StoredConfiguration newConfig )
     {
         final Instant startTime = Instant.now();
 
@@ -286,7 +288,7 @@ public class ConfigurationFileManager
                     .orElse( "removed" );
 
             final String finalMsg = modifyMessage;
-            LOGGER.trace( () -> "sending audit notice: " + finalMsg );
+            LOGGER.trace( sessionLabel, () -> "sending audit notice: " + finalMsg );
 
             AuditServiceClient.submit( pwmApplication, sessionLabel, AuditRecordFactory.make( sessionLabel, pwmApplication ).createUserAuditRecord(
                     AuditEvent.MODIFY_CONFIGURATION,
@@ -298,13 +300,12 @@ public class ConfigurationFileManager
         }
 
         final int finalChangeCount = changeCount;
-        LOGGER.debug( () -> "sent " + finalChangeCount + " audit notifications about changed settings", () -> TimeDuration.fromCurrent( startTime ) );
+        LOGGER.debug( sessionLabel, () -> "sent " + finalChangeCount + " audit notifications about changed settings", TimeDuration.fromCurrent( startTime ) );
     }
 
     private void outputConfigurationFile(
             final StoredConfiguration storedConfiguration,
             final PwmApplication pwmApplication,
-            final SessionLabel sessionLabel,
             final int backupRotations,
             final File backupDirectory
     )
@@ -322,14 +323,14 @@ public class ConfigurationFileManager
             StoredConfigurationFactory.output( storedConfiguration, fileOutputStream );
         }
 
-        LOGGER.info( () -> "saved configuration", () -> TimeDuration.fromCurrent( saveFileStartTime ) );
+        LOGGER.info( sessionLabel, () -> "saved configuration", TimeDuration.fromCurrent( saveFileStartTime ) );
         if ( pwmApplication != null )
         {
             final String actualChecksum = StoredConfigurationUtil.valueHash( storedConfiguration );
             pwmApplication.writeAppAttribute( AppAttribute.CONFIG_HASH, actualChecksum );
         }
 
-        LOGGER.trace( () -> "renaming file " + tempWriteFile.getAbsolutePath() + " to " + configFile.getAbsolutePath() );
+        LOGGER.trace( sessionLabel, () -> "renaming file " + tempWriteFile.getAbsolutePath() + " to " + configFile.getAbsolutePath() );
         try
         {
             Files.move( tempWriteFile.toPath(), configFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE );

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

@@ -918,7 +918,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                 final XmlElement settingElement = StoredConfigXmlSerializer.XmlOutputHandler.makeSettingXmlElement(
                         null,
                         key,
-                        new StringArrayValue( newValues ),
+                        StringArrayValue.create( 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.attachElement( settingElement ) );

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

@@ -27,7 +27,7 @@ import password.pwm.config.value.FileValue;
 import password.pwm.config.value.StoredValue;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.ImmutableByteArray;
+import password.pwm.data.ImmutableByteArray;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.json.JsonProvider;
@@ -104,7 +104,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
                 }
                 else
                 {
-                    storedValue = syntax.getFactory().fromJson( serializedValue.getValueData() );
+                    storedValue = syntax.getFactory().fromJson( key.toPwmSetting(), serializedValue.getValueData() );
                 }
                 storedValueMap.put( key, storedValue );
             }

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

@@ -25,7 +25,6 @@ import lombok.Value;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.logging.PwmLogger;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -33,8 +32,6 @@ import java.io.OutputStream;
 
 public class StoredConfigurationFactory
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationFactory.class );
-
     private static final StoredConfigSerializer SERIALIZER = new StoredConfigXmlSerializer();
 
     public static StoredConfiguration newConfig() throws PwmUnrecoverableException

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

@@ -27,6 +27,7 @@ import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.StringValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
+import password.pwm.error.PwmInternalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.PwmLocaleBundle;
@@ -219,7 +220,8 @@ public class StoredConfigurationModifier
         this.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, passwordHash );
     }
 
-    private void update( final FunctionWithException<StoredConfigData> function ) throws PwmUnrecoverableException
+    private void update( final FunctionWithException<StoredConfigData> function )
+            throws PwmUnrecoverableException
     {
         try
         {
@@ -231,7 +233,7 @@ public class StoredConfigurationModifier
                 }
                 catch ( final PwmUnrecoverableException e )
                 {
-                    throw new RuntimeException( e );
+                    throw new PwmInternalException( e );
                 }
             } );
             modifications.incrementAndGet();

+ 25 - 41
server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java

@@ -41,20 +41,23 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.MiscUtil;
 import password.pwm.util.java.PwmExceptionLoggingConsumer;
 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.HmacAlgorithm;
+import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.PwmRandom;
-import password.pwm.util.secure.SecureEngine;
 
-import java.time.Instant;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.DigestOutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -185,15 +188,10 @@ public abstract class StoredConfigurationUtil
             return Stream.empty();
         };
 
-        final Instant startTime = Instant.now();
-        final List<String> errorStrings = CollectionUtil.iteratorToStream( storedConfiguration.keys() )
+        return CollectionUtil.iteratorToStream( storedConfiguration.keys() )
                 .filter( key -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
                 .flatMap( validateSettingFunction )
-                .collect( Collectors.toList() );
-
-
-        LOGGER.trace( () -> "StoredConfiguration validator completed", () -> TimeDuration.fromCurrent( startTime ) );
-        return Collections.unmodifiableList( errorStrings );
+                .collect( Collectors.toUnmodifiableList() );
     }
 
     public static boolean verifyPassword( final StoredConfiguration storedConfiguration, final String password )
@@ -252,8 +250,6 @@ public abstract class StoredConfigurationUtil
                 new PasswordValue( new PasswordData( PwmRandom.getInstance().alphaNumericString( 1024 ) ) ),
                 null
         );
-
-        LOGGER.debug( () -> "initialized new random security key" );
     }
 
     public static Map<String, String> makeDebugMap(
@@ -321,8 +317,6 @@ public abstract class StoredConfigurationUtil
             final StoredConfiguration modifiedConfiguration
     )
     {
-        final Instant startTime = Instant.now();
-
         final Predicate<StoredConfigKey> hashTester = key ->
         {
             final Optional<String> hash1 = originalConfiguration.readStoredValue( key ).map( StoredValue::valueHash );
@@ -330,16 +324,12 @@ public abstract class StoredConfigurationUtil
             return !hash1.equals( hash2 );
         };
 
-        final Set<StoredConfigKey> deltaReferences = Stream.concat(
+        return Stream.concat(
                 CollectionUtil.iteratorToStream( originalConfiguration.keys() ),
                 CollectionUtil.iteratorToStream( modifiedConfiguration.keys() ) )
                 .distinct()
                 .filter( hashTester )
                 .collect( Collectors.toUnmodifiableSet() );
-
-        LOGGER.trace( () -> "generated " + deltaReferences.size() + " changeLog items via compare", () -> TimeDuration.fromCurrent( startTime ) );
-
-        return deltaReferences;
     }
 
     public static StoredValue getValueOrDefault(
@@ -437,7 +427,7 @@ public abstract class StoredConfigurationUtil
             final List<String> newProfileIDList = new ArrayList<>( existingProfiles );
             newProfileIDList.add( destinationID );
             final StoredConfigKey key = StoredConfigKey.forSetting( profileSetting, null, domainID );
-            final StoredValue value = new StringArrayValue( newProfileIDList );
+            final StoredValue value = StringArrayValue.create( newProfileIDList );
             modifier.writeSetting( key, value, userIdentity );
         }
 
@@ -452,7 +442,6 @@ public abstract class StoredConfigurationUtil
     )
             throws PwmUnrecoverableException
     {
-        final Instant startTime = Instant.now();
         final DomainID sourceID = DomainID.create( source );
         final DomainID destinationID = DomainID.create( destination );
 
@@ -491,38 +480,33 @@ public abstract class StoredConfigurationUtil
             final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.DOMAIN_LIST, null, DomainID.systemId() );
             final List<String> domainList = new ArrayList<>( ValueTypeConverter.valueToStringArray( StoredConfigurationUtil.getValueOrDefault( oldStoredConfiguration, key ) ) );
             domainList.add( destination );
-            final StoredValue value = new StringArrayValue( domainList );
+            final StoredValue value = StringArrayValue.create( domainList );
             modifier.writeSetting( key, value, userIdentity );
         }
 
-        LOGGER.trace( () -> "copied " + modifier.modificationCount() + " domain settings from '" + source + "' to '" + destination + "' domain",
-                () -> TimeDuration.fromCurrent( startTime ) );
-
         return modifier.newStoredConfiguration();
     }
 
     public static String valueHash( final StoredConfiguration storedConfiguration )
     {
-        final Instant startTime = Instant.now();
-        final StringBuilder sb = new StringBuilder();
-
-        CollectionUtil.iteratorToStream( storedConfiguration.keys() )
-                .map( storedConfiguration::readStoredValue )
-                .flatMap( Optional::stream )
-                .forEach( v -> sb.append( v.valueHash() ) );
-
-        final String output;
-        try
+        try ( DigestOutputStream digestOutputStream = new DigestOutputStream( OutputStream.nullOutputStream(), PwmHashAlgorithm.SHA512.newMessageDigest() ) )
         {
-            output = SecureEngine.hmac( HmacAlgorithm.HMAC_SHA_512, storedConfiguration.getKey(), sb.toString() );
+            final Iterator<StoredConfigKey> keyIterator = storedConfiguration.keys();
+            while ( keyIterator.hasNext() )
+            {
+                final Optional<StoredValue> value = storedConfiguration.readStoredValue( keyIterator.next() );
+                if ( value.isPresent() )
+                {
+                    digestOutputStream.write( value.get().valueHash().getBytes( StandardCharsets.UTF_8 ) );
+                }
+            }
+
+            return JavaHelper.binaryArrayToHex( digestOutputStream.getMessageDigest().digest() );
         }
-        catch ( final PwmUnrecoverableException e )
+        catch ( final IOException e )
         {
             throw new IllegalStateException( e );
         }
-
-        LOGGER.trace( () -> "calculated StoredConfiguration hash: " + output, () -> TimeDuration.fromCurrent( startTime ) );
-        return output;
     }
 
     public static boolean isDefaultValue( final StoredConfiguration storedConfiguration, final StoredConfigKey key )

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

@@ -27,7 +27,7 @@ import password.pwm.PwmConstants;
 import password.pwm.config.stored.StoredConfigXmlConstants;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.ImmutableByteArray;
+import password.pwm.data.ImmutableByteArray;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;

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

@@ -67,7 +67,7 @@ public class ActionValue extends AbstractValue implements StoredValue
     private static class ActionStoredValueFactory implements StoredValueFactory
     {
         @Override
-        public ActionValue fromJson( final String input )
+        public ActionValue fromJson( final PwmSetting pwmSetting, final String input )
         {
             return input == null
                     ? new ActionValue( Collections.emptyList() )

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

@@ -58,7 +58,7 @@ public class BooleanValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public BooleanValue fromJson( final String value )
+            public BooleanValue fromJson( final PwmSetting pwmSetting, final String value )
             {
                 return BooleanValue.of( JsonFactory.get().deserialize( value, Boolean.class ) );
             }

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

@@ -78,7 +78,7 @@ public class ChallengeValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public ChallengeValue fromJson( final String input )
+            public ChallengeValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -25,6 +25,7 @@ import org.jrivard.xmlchai.XmlElement;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.CustomLinkConfiguration;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.util.secure.PwmSecurityKey;
 
@@ -41,7 +42,7 @@ public class CustomLinkValue extends AbstractValue implements StoredValue
 
     public CustomLinkValue( final List<CustomLinkConfiguration> values )
     {
-        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( CollectionUtil.stripNulls( values ) );
     }
 
     public static StoredValueFactory factory( )
@@ -49,7 +50,7 @@ public class CustomLinkValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public CustomLinkValue fromJson( final String input )
+            public CustomLinkValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {
@@ -57,13 +58,8 @@ public class CustomLinkValue extends AbstractValue implements StoredValue
                 }
                 else
                 {
-                    List<CustomLinkConfiguration> srcList = JsonFactory.get().deserializeList( input, CustomLinkConfiguration.class );
-                    srcList = srcList == null ? Collections.emptyList() : srcList;
-                    while ( srcList.contains( null ) )
-                    {
-                        srcList.remove( null );
-                    }
-                    return new CustomLinkValue( Collections.unmodifiableList( srcList ) );
+                    final List<CustomLinkConfiguration> srcList = JsonFactory.get().deserializeList( input, CustomLinkConfiguration.class );
+                    return new CustomLinkValue( srcList );
                 }
             }
 

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

@@ -26,6 +26,7 @@ import password.pwm.bean.EmailItemBean;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.util.secure.PwmSecurityKey;
@@ -45,7 +46,9 @@ public class EmailValue extends AbstractValue implements StoredValue
 
     EmailValue( final Map<String, EmailItemBean> values )
     {
-        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
+        this.values = values == null
+                ? Collections.emptyMap()
+                : Collections.unmodifiableMap( CollectionUtil.stripNulls( values ) );
     }
 
     public static StoredValueFactory factory( )
@@ -53,7 +56,7 @@ public class EmailValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public EmailValue fromJson( final String input )
+            public EmailValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {
@@ -61,11 +64,8 @@ public class EmailValue extends AbstractValue implements StoredValue
                 }
                 else
                 {
-                    Map<String, EmailItemBean> srcMap = JsonFactory.get().deserializeMap( input, String.class, EmailItemBean.class );
-
-                    srcMap = srcMap == null ? Collections.emptyMap() : srcMap;
-                    srcMap.remove( null );
-                    return new EmailValue( Collections.unmodifiableMap( srcMap ) );
+                    final Map<String, EmailItemBean> srcMap = JsonFactory.get().deserializeMap( input, String.class, EmailItemBean.class );
+                    return new EmailValue( srcMap );
                 }
             }
 

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

@@ -30,7 +30,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.StoredConfigXmlConstants;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.ImmutableByteArray;
+import password.pwm.data.ImmutableByteArray;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.json.JsonFactory;
@@ -171,7 +171,7 @@ public class FileValue extends AbstractValue implements StoredValue
             }
 
             @Override
-            public StoredValue fromJson( final String input )
+            public StoredValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 throw new IllegalStateException( "not implemented" );
             }

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

@@ -54,7 +54,7 @@ public class FormValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public FormValue fromJson( final String input )
+            public FormValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -72,7 +72,7 @@ public class LocalizedStringArrayValue extends AbstractValue implements StoredVa
         return new StoredValueFactory()
         {
             @Override
-            public LocalizedStringArrayValue fromJson( final String input )
+            public LocalizedStringArrayValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -68,7 +68,7 @@ public class LocalizedStringValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public LocalizedStringValue fromJson( final String input )
+            public LocalizedStringValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -29,6 +29,7 @@ import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.NamedSecretData;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
+import password.pwm.error.PwmInternalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
@@ -75,7 +76,7 @@ public class NamedSecretValue implements StoredValue
         return new StoredValue.StoredValueFactory()
         {
             @Override
-            public NamedSecretValue fromJson( final String value )
+            public NamedSecretValue fromJson( final PwmSetting pwmSetting, final String value )
             {
                 try
                 {
@@ -200,7 +201,7 @@ public class NamedSecretValue implements StoredValue
         }
         catch ( final Exception e )
         {
-            throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
+            throw new PwmInternalException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
         }
         return Collections.unmodifiableList( valuesElement );
     }

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

@@ -50,7 +50,7 @@ public class NumericArrayValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public NumericArrayValue fromJson( final String value )
+            public NumericArrayValue fromJson( final PwmSetting pwmSetting, final String value )
             {
                 final long[] longArray = JsonFactory.get().deserialize( value, long[].class );
                 final List<Long> list = Arrays.stream( longArray ).boxed().collect( Collectors.toList() );

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

@@ -47,7 +47,7 @@ public class NumericValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public NumericValue fromJson( final String value )
+            public NumericValue fromJson( final PwmSetting pwmSetting, final String value )
             {
                 return new NumericValue( JsonFactory.get().deserialize( value, Long.class ) );
             }

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

@@ -24,6 +24,7 @@ import org.jrivard.xmlchai.XmlChai;
 import org.jrivard.xmlchai.XmlElement;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.XmlOutputProcessData;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.util.secure.PwmSecurityKey;
 
@@ -49,7 +50,7 @@ public class OptionListValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public OptionListValue fromJson( final String input )
+            public OptionListValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {
@@ -59,10 +60,7 @@ public class OptionListValue extends AbstractValue implements StoredValue
                 {
                     List<String> srcList = JsonFactory.get().deserializeStringList( input );
                     srcList = srcList == null ? Collections.emptyList() : srcList;
-                    while ( srcList.contains( null ) )
-                    {
-                        srcList.remove( null );
-                    }
+                    srcList = CollectionUtil.stripNulls( srcList );
                     return new OptionListValue( Set.copyOf( srcList ) );
                 }
             }

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

@@ -29,6 +29,7 @@ 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.PwmInternalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
@@ -65,7 +66,7 @@ public class PasswordValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public PasswordValue fromJson( final String value )
+            public PasswordValue fromJson( final PwmSetting pwmSetting, final String value )
             {
                 final String strValue = JsonFactory.get().deserialize( value, String.class );
                 if ( strValue != null && !strValue.isEmpty() )
@@ -174,7 +175,7 @@ public class PasswordValue implements StoredValue
         }
         catch ( final Exception e )
         {
-            throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
+            throw new PwmInternalException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
         }
         return Collections.singletonList( valueElement );
     }

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

@@ -26,6 +26,7 @@ import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.StoredConfigXmlConstants;
 import password.pwm.config.stored.XmlOutputProcessData;
+import password.pwm.error.PwmInternalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.json.JsonFactory;
@@ -135,7 +136,7 @@ public class PrivateKeyValue extends AbstractValue
             }
 
             @Override
-            public X509CertificateValue fromJson( final String input )
+            public X509CertificateValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 return new X509CertificateValue( Collections.emptyList() );
             }
@@ -201,7 +202,7 @@ public class PrivateKeyValue extends AbstractValue
             }
             catch ( final Exception e )
             {
-                throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
+                throw new PwmInternalException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
             }
         }
         return Collections.singletonList( valueElement );

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

@@ -59,7 +59,7 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public RemoteWebServiceValue fromJson( final String input )
+            public RemoteWebServiceValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -46,7 +46,7 @@ public interface StoredValue extends Serializable
 
     interface StoredValueFactory
     {
-        StoredValue fromJson( String input );
+        StoredValue fromJson( PwmSetting pwmSetting, String input );
 
         StoredValue fromXmlElement( PwmSetting pwmSetting, XmlElement settingElement, PwmSecurityKey key )
                 throws PwmException;

部分文件因文件數量過多而無法顯示