瀏覽代碼

Merge branch 'master' into enh-domainadmin

# Conflicts:
#	server/src/main/java/password/pwm/http/PwmSession.java
#	server/src/main/java/password/pwm/http/servlet/ControlledPwmServlet.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/http/servlet/configeditor/ConfigEditorServlet.java
#	server/src/main/java/password/pwm/http/servlet/configeditor/function/UserMatchViewerFunction.java
#	server/src/main/java/password/pwm/http/tag/value/PwmValue.java
#	server/src/main/java/password/pwm/ldap/auth/SessionAuthenticator.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/localdb/LocalDB.java
#	server/src/test/java/password/pwm/http/PwmURLTest.java
#	server/src/test/java/password/pwm/http/servlet/ControlledPwmServletTest.java
#	webapp/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp
#	webapp/src/main/webapp/WEB-INF/jsp/configmanager-login.jsp
Jason Rivard 2 年之前
父節點
當前提交
53485c17dd
共有 100 個文件被更改,包括 2508 次插入1247 次删除
  1. 19 0
      CHANGES.md
  2. 100 44
      README.md
  3. 3 2
      build/checkstyle-import.xml
  4. 13 13
      client/angular/package-lock.json
  5. 1 1
      client/angular/package.json
  6. 1 1
      client/angular/src/i18n/translations_en.json
  7. 0 7
      client/angular/src/modules/configeditor/configeditor.module.ts
  8. 1 1
      client/angular/src/modules/helpdesk/verifications-dialog.controller.ts
  9. 3 3
      client/angular/src/services/helpdesk.service.ts
  10. 1 1
      client/angular/src/services/peoplesearch-config.service.ts
  11. 6 4
      client/pom.xml
  12. 7 10
      data-service/pom.xml
  13. 2 2
      data-service/src/main/java/password/pwm/receiver/ContextManager.java
  14. 6 4
      data-service/src/main/java/password/pwm/receiver/Logger.java
  15. 4 7
      data-service/src/main/java/password/pwm/receiver/PublishVersionServlet.java
  16. 35 0
      data-service/src/main/java/password/pwm/receiver/PwmReceiverApp.java
  17. 115 0
      data-service/src/main/java/password/pwm/receiver/RequestLoggerFilter.java
  18. 3 3
      data-service/src/main/java/password/pwm/receiver/Settings.java
  19. 2 2
      data-service/src/main/java/password/pwm/receiver/Storage.java
  20. 13 0
      data-service/src/main/java/password/pwm/receiver/SummaryBean.java
  21. 6 7
      data-service/src/main/java/password/pwm/receiver/TelemetryRestReceiver.java
  22. 3 8
      data-service/src/main/java/password/pwm/receiver/TelemetryViewerServlet.java
  23. 36 3
      data-service/src/main/webapp/WEB-INF/jsp/telemetry-viewer.jsp
  24. 4 4
      docker/pom.xml
  25. 1 0
      docker/src/main/image-files/app/java.vmoptions
  26. 3 2
      lib-data/pom.xml
  27. 31 0
      lib-data/src/main/java/password/pwm/data/FileUploadItem.java
  28. 10 10
      lib-data/src/test/java/password/pwm/bean/VersionNumberTest.java
  29. 2 1
      lib-util/pom.xml
  30. 19 11
      lib-util/src/main/java/password/pwm/util/EventRateMeter.java
  31. 3 11
      lib-util/src/main/java/password/pwm/util/MovingAverage.java
  32. 1 1
      lib-util/src/main/java/password/pwm/util/Percent.java
  33. 0 1
      lib-util/src/main/java/password/pwm/util/ProgressInfoCalculator.java
  34. 13 21
      lib-util/src/main/java/password/pwm/util/TransactionSizeCalculator.java
  35. 46 13
      lib-util/src/main/java/password/pwm/util/java/AverageTracker.java
  36. 89 67
      lib-util/src/main/java/password/pwm/util/java/CollectionUtil.java
  37. 142 0
      lib-util/src/main/java/password/pwm/util/java/CollectorUtil.java
  38. 5 6
      lib-util/src/main/java/password/pwm/util/java/ConditionalTaskExecutor.java
  39. 124 0
      lib-util/src/main/java/password/pwm/util/java/EnumUtil.java
  40. 60 0
      lib-util/src/main/java/password/pwm/util/java/FunctionalReentrantLock.java
  41. 12 83
      lib-util/src/main/java/password/pwm/util/java/JavaHelper.java
  42. 0 55
      lib-util/src/main/java/password/pwm/util/java/LazySoftReference.java
  43. 23 48
      lib-util/src/main/java/password/pwm/util/java/LazySupplier.java
  44. 176 0
      lib-util/src/main/java/password/pwm/util/java/LazySupplierImpl.java
  45. 12 21
      lib-util/src/main/java/password/pwm/util/java/MutableReference.java
  46. 12 0
      lib-util/src/main/java/password/pwm/util/java/PwmNumberFormat.java
  47. 8 10
      lib-util/src/main/java/password/pwm/util/java/StatisticAverageBundle.java
  48. 11 11
      lib-util/src/main/java/password/pwm/util/java/StatisticCounterBundle.java
  49. 83 0
      lib-util/src/main/java/password/pwm/util/java/StatisticRateBundle.java
  50. 31 24
      lib-util/src/main/java/password/pwm/util/java/StringUtil.java
  51. 5 5
      lib-util/src/test/java/password/pwm/util/java/AtomicLoopIntIncrementerTest.java
  52. 5 5
      lib-util/src/test/java/password/pwm/util/java/AverageTrackerTest.java
  53. 0 58
      lib-util/src/test/java/password/pwm/util/java/CollectionUtilTest.java
  54. 127 0
      lib-util/src/test/java/password/pwm/util/java/CollectorUtilTest.java
  55. 35 35
      lib-util/src/test/java/password/pwm/util/java/CopyingInputStreamTest.java
  56. 4 4
      lib-util/src/test/java/password/pwm/util/java/JavaHelperTest.java
  57. 42 0
      lib-util/src/test/java/password/pwm/util/java/LazySupplierTest.java
  58. 56 0
      lib-util/src/test/java/password/pwm/util/java/PwmNumberFormatTest.java
  59. 41 21
      lib-util/src/test/java/password/pwm/util/java/StringUtilTest.java
  60. 4 4
      onejar/pom.xml
  61. 20 15
      pom.xml
  62. 1 10
      rest-test-service/pom.xml
  63. 27 20
      server/pom.xml
  64. 3 1
      server/src/main/java/password/pwm/AppAttribute.java
  65. 15 6
      server/src/main/java/password/pwm/AppProperty.java
  66. 7 29
      server/src/main/java/password/pwm/PwmAboutProperty.java
  67. 15 19
      server/src/main/java/password/pwm/PwmApplication.java
  68. 27 45
      server/src/main/java/password/pwm/PwmApplicationUtil.java
  69. 2 2
      server/src/main/java/password/pwm/PwmConstants.java
  70. 8 9
      server/src/main/java/password/pwm/PwmDomain.java
  71. 67 27
      server/src/main/java/password/pwm/PwmDomainUtil.java
  72. 11 9
      server/src/main/java/password/pwm/PwmEnvironment.java
  73. 44 19
      server/src/main/java/password/pwm/bean/DomainID.java
  74. 1 1
      server/src/main/java/password/pwm/bean/LocalSessionStateBean.java
  75. 139 0
      server/src/main/java/password/pwm/bean/ProfileID.java
  76. 163 18
      server/src/main/java/password/pwm/bean/SessionLabel.java
  77. 18 108
      server/src/main/java/password/pwm/bean/UserIdentity.java
  78. 40 69
      server/src/main/java/password/pwm/config/AppConfig.java
  79. 64 42
      server/src/main/java/password/pwm/config/DomainConfig.java
  80. 18 11
      server/src/main/java/password/pwm/config/PwmSetting.java
  81. 20 20
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  82. 5 4
      server/src/main/java/password/pwm/config/PwmSettingMetaData.java
  83. 2 2
      server/src/main/java/password/pwm/config/PwmSettingStats.java
  84. 2 1
      server/src/main/java/password/pwm/config/PwmSettingTemplate.java
  85. 3 2
      server/src/main/java/password/pwm/config/PwmSettingTemplateSet.java
  86. 11 2
      server/src/main/java/password/pwm/config/PwmSettingXml.java
  87. 15 17
      server/src/main/java/password/pwm/config/StoredSettingReader.java
  88. 2 2
      server/src/main/java/password/pwm/config/option/WebServiceUsage.java
  89. 11 9
      server/src/main/java/password/pwm/config/profile/AbstractProfile.java
  90. 3 2
      server/src/main/java/password/pwm/config/profile/AccountInformationProfile.java
  91. 3 2
      server/src/main/java/password/pwm/config/profile/ActivateUserProfile.java
  92. 7 6
      server/src/main/java/password/pwm/config/profile/ChallengeProfile.java
  93. 3 2
      server/src/main/java/password/pwm/config/profile/ChangePasswordProfile.java
  94. 3 2
      server/src/main/java/password/pwm/config/profile/DeleteAccountProfile.java
  95. 3 10
      server/src/main/java/password/pwm/config/profile/EmailServerProfile.java
  96. 3 2
      server/src/main/java/password/pwm/config/profile/ForgottenPasswordProfile.java
  97. 3 2
      server/src/main/java/password/pwm/config/profile/HelpdeskProfile.java
  98. 81 31
      server/src/main/java/password/pwm/config/profile/LdapProfile.java
  99. 15 12
      server/src/main/java/password/pwm/config/profile/NewUserProfile.java
  100. 3 2
      server/src/main/java/password/pwm/config/profile/PeopleSearchProfile.java

+ 19 - 0
CHANGES.md

@@ -6,6 +6,25 @@
 ### Changed
 ### Changed
 - Removed setting 'Security ⇨ Web Security ⇨ Permitted IP Network Addresses', this functionality is better provided by the web server itself.
 - Removed setting 'Security ⇨ Web Security ⇨ Permitted IP Network Addresses', this functionality is better provided by the web server itself.
 
 
+## [2.0.4] - Released Oct 1, 2022
+- version check service request frequency fix
+- update java and javascript dependencies
+- update tomcat to 9.0.67 for onejar/docker images
+- update java to 11.0.16.1 in docker image
+
+## [2.0.3] - Released July 30, 2022
+- version check service de-serialization error fix
+- fix issue with config guide buttons not working on storage selection page
+
+## [2.0.2] - Released July 7, 2022
+- add version check service
+- update java and npm, dependencies including tomcat 9.0.65 for onejar/docker images.  
+- fix issue #542 - web actions do not save/load properly if a basic auth password is not included
+- fix issue #660 - Shortcut module does not display shortcuts based on …
+- fix issue with js dom/ready initialization on helpdesk/peoplesearch page loading
+- replace log4j with reload4j (issue #628)
+
+
 ## [2.0.1] - Released March 11, 2022
 ## [2.0.1] - Released March 11, 2022
 ### Changed
 ### Changed
 - Issue #573 - PWM 5081 at the end of user activation ( no profile assigned )
 - Issue #573 - PWM 5081 at the end of user activation ( no profile assigned )

+ 100 - 44
README.md

@@ -1,6 +1,6 @@
 # PWM
 # PWM
 
 
-PWM is an open source password self-service application for LDAP directories. PWM is an ideal candidate for organizations that wish to “roll their own” password self service solution, but do not wish to start from scratch. [Overview/Screenshots](https://docs.google.com/presentation/d/1LxDXV_iiToJXAzzT9mc1xXO0atVObmRpCame6qXOyxM/pub?slide=id.p8)
+PWM is an open source password self-service application for LDAP directories.
 
 
 Official project page is at [https://github.com/pwm-project/pwm/](https://github.com/pwm-project/pwm/).
 Official project page is at [https://github.com/pwm-project/pwm/](https://github.com/pwm-project/pwm/).
 
 
@@ -8,52 +8,108 @@ Official project page is at [https://github.com/pwm-project/pwm/](https://github
 * [PWM-General Google Group](https://groups.google.com/group/pwm-general) - please ask for assistance here first.
 * [PWM-General Google Group](https://groups.google.com/group/pwm-general) - please ask for assistance here first.
 * [PWM Documentation Wiki](https://github.com/pwm-project/pwm/wiki) - Home for PWM documentation
 * [PWM Documentation Wiki](https://github.com/pwm-project/pwm/wiki) - Home for PWM documentation
 * [PWM Reference](https://www.pwm-project.org/pwm/public/reference/) - Reference documentation built into PWM.
 * [PWM Reference](https://www.pwm-project.org/pwm/public/reference/) - Reference documentation built into PWM.
+* [Downloads](https://github.com/pwm-project/pwm/releases)
 
 
 # Features
 # Features
 * Web based configuration manager with over 500 configurable settings
 * Web based configuration manager with over 500 configurable settings
+  * All configuration contained in a single importable/exportable file
   * Configurable display values for every user-facing text string
   * Configurable display values for every user-facing text string
-  * Localized for Chinese (中文), Czech (ceština), Dutch (Nederlands), English, Finnish (suomi), French (français), German (Deutsch), Hebrew (עברית), Italian (italiano), Japanese (日本語), Korean (한국어), Polish (polski), Portuguese (português), Slovak (Slovenčina), Spanish (español), Thai (ไทย) and Turkish (Türkçe)
-* Change Password functionality
-  * Polished, intuitive end-user interface with as-you-type password rule enforcement
-  * Large set of configurable password polices to match any organizational requirements
-  * Read policies from LDAP directories (where supported by LDAP server)
-* Forgotten Password
-  * Store Responses in local server, standard RDBMS database, LDAP server or Novell NMAS repositories
-  * Use Forgotten Password, Email/SMS Token/PIN, TOTP, Remote REST service, User LDAP attribute values, or any combination
-  * Stand-alone, easy to deploy, java web application
-* Helpdesk password reset and intruder lockout clearing
-* New User Registration / Account Creation
-* Guest User Registration / Updating
-* PeopleSearch (white pages)
-  * Configurable detail pages
-  * OrgChart view
-* Account Activation  / First time password assignment
-* All configuration contained in a single importable/exportable file
-* Support for multple domains/tenants  
-* Administration modules including intruder-lockout manager, and online log viewer, daily stats viewer and user information debugging
+* Included localizations (not all are complete or current):
+  * English - English
+  * Catalan - català
+  * Chinese (China) - 中文 (中国)
+  * Chinese (Taiwan) - 中文 (台灣)
+  * Czech - čeština
+  * Danish - dansk
+  * Dutch - Nederlands
+  * English (Canada) - English (Canada)
+  * Finnish - suomi
+  * French - français
+  * French (Canada) - français (Canada)
+  * German - Deutsch
+  * Greek - Ελληνικά
+  * Hebrew - עברית
+  * Hungarian - magyar
+  * Italian - italiano
+  * Japanese - 日本語
+  * Korean - 한국어
+  * Norwegian - norsk
+  * Norwegian Bokmål - norsk bokmål
+  * Norwegian Nynorsk - nynorsk
+  * Polish - polski
+  * Portuguese - português
+  * Portuguese (Brazil) - português (Brasil)
+  * Russian - русский
+  * Slovak - slovenčina
+  * Spanish - español
+  * Swedish - svenska
+  * Thai - ไทย
+  * Turkish - Türkçe
+* LDAP Directory Support:
+  * Multiple LDAP vendor support:
+    * Generic LDAP (best-effort, LDAP password behavior and error handling is not standardized in LDAP)
+    * Directory 389
+      * Reading of configured user password policies
+    * NetIQ eDirectory
+      * Read Password Policies & Challenge Sets
+      * NMAS Operations and Error handling
+      * Support for NMAS user challenge/responses
+    * Microsoft Active Directory
+      * Reading of Fine-Grained Password Policy (FGPP) Password Setting Objects (PSO) (does not read domain policies)
+    * OpenLDAP
+  * Native LDAP retry/failover support of multiple redundant LDAP servers
+* Large set of locally configurable password polices
+  * Standard syntax rules
+  * Regex rules
+  * Password dictionary enforcement
+  * Remote REST server checking
+  * AD-style syntax groups
+  * Shared password history to prevent passwords from being reused organizationally
+* Modules
+  * Change Password
+    * as-you-type password rule enforcement
+    * password strength feedback display
+  * Account Activation / First time password assignment
+  * Forgotten Password
+    * Store Responses in local server, standard RDBMS database, LDAP server or eDirectory NMAS repositories
+    * User verification options:
+      * Email/SMS Token/PIN
+      * TOTP
+      * Remote REST service
+      * OAuth service
+      * User LDAP attribute values
+  * New User Registration / Account Creation
+  * Guest User Registration / Updating
+  * PeopleSearch (white pages)
+    * Configurable detail pages
+    * OrgChart view
+  * Helpdesk password reset and intruder lockout clearing
+  * Administration modules including intruder-lockout manager
+    * online log viewer 
+    * daily stats viewer and user information debugging
+    * statistics
+    * audit records
+* Multiple Deployment Options
+  * Java WAR file (bring your own application server, tested with Apache Tomcat)
+  * Java single JAR file (bring your own Java VM)
+  * Docker container
 * Theme-able interface with several example CSS themes
 * Theme-able interface with several example CSS themes
-* Support for large dictionary wordlists to enforce strong passwords
-* Shared password history to prevent passwords from being reused organizationally
+  * Mobile devices specific CSS themes
+  * Configuration support for additional web assets (css, js, images, etc)
+  * Force display of organizational 
 * Captcha support using Google reCaptcha
 * Captcha support using Google reCaptcha
-* Integration with CAS
-* Support for minimal, restricted and mobile browsers with no cookies, javascript or css
-* Specialized skins for iPhone/Mobile devices
-* Designed for integration with existing portals and web security gateways
-* OAuth Service Provider to allow single-signon from OAuth servers and using OAuth as a forgotten password verification method
-* REST Server APIs for most functionality  
-* Callout to REST servers for custom integrations of several functions    
-* LDAP Features
-  * Support for password replication checking and minimum time delays during password sets
-  * Automatic LDAP server fail-over to multiple ldap servers and retry during LDAP server failures
-* LDAP Directory Support
-  * Generic LDAP
-  * Directory 389
-  * NetIQ eDirectory
-    * Password Policies & Challenge Sets
-    * NMAS Operations and Error handling
-    * Support for NMAS user challenge/responses
-  * Microsoft Active Directory
-  * OpenLDAP
+* Multiple SSO options
+  * Basic Authentication 
+  * HTTP header username injection
+  * Central Authentication Service (CAS)
+  * OAuth client
+* REST Server APIs for most functionality
+  * Password set
+  * Forgotten password
+  * Password policy reading
+  * User attribute updates
+  * Password policy verification
+* Outbound REST API for custom integrations during user activities such as change password, new user registration, etc.    
 
 
 ## Deploy
 ## Deploy
 PWM is distributed in the following artifacts:
 PWM is distributed in the following artifacts:
@@ -104,8 +160,8 @@ By default the executable will remain attached to the console and listen for HTT
 
 
 ### Docker
 ### Docker
 The PWM docker image includes Java and Tomcat.  It listens using https on port 8443, and has a volume exposed
 The PWM docker image includes Java and Tomcat.  It listens using https on port 8443, and has a volume exposed
-as `/config`.  You will need to map the `/config` volume to either a localhost or some type of persistent docker
-volume for PWM to work properly.
+as `/config`.  You will need to map the `/config` volume to some type of persistent docker
+volume for PWM to retain configuration.
 
 
 Requirements:
 Requirements:
 * Server running docker
 * Server running docker
@@ -120,7 +176,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
 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:
 local file system _/home/user/pwm-config_ folder:
 ```
 ```
-docker create --name mypwm -p '8443:8443' pwm/pwm-webapp -v '/config:/home/user/pwm-config'
+docker create --name mypwm -p '8443:8443' --mount 'type=bind,source=/home/user/pwm-config,destination=/config' pwm/pwm-webapp
 ```
 ```
 
 
 1. Start the _mypwm_ container:
 1. Start the _mypwm_ container:

+ 3 - 2
build/checkstyle-import.xml

@@ -52,8 +52,9 @@
     <!-- graylog2 -->
     <!-- graylog2 -->
     <allow pkg="org.graylog2"/>
     <allow pkg="org.graylog2"/>
 
 
-    <!-- log4j -->
-    <allow pkg="org.apache.log4j"/>
+    <!-- logback -->
+    <allow pkg="org.slf4j"/>
+    <allow pkg="ch.qos.logback"/>
 
 
     <!-- testing -->
     <!-- testing -->
     <allow pkg="org.junit"/>
     <allow pkg="org.junit"/>

+ 13 - 13
client/angular/package-lock.json

@@ -49,7 +49,7 @@
                 "karma-spec-reporter": "0.0.32",
                 "karma-spec-reporter": "0.0.32",
                 "karma-webpack": "5.0.0",
                 "karma-webpack": "5.0.0",
                 "lodash": ">=4.17.21",
                 "lodash": ">=4.17.21",
-                "moment": "^2.29.3",
+                "moment": "^2.29.4",
                 "ngtemplate-loader": "2.0.1",
                 "ngtemplate-loader": "2.0.1",
                 "node-sass": "^7.0.0",
                 "node-sass": "^7.0.0",
                 "postcss-loader": "2.1.1",
                 "postcss-loader": "2.1.1",
@@ -7061,9 +7061,9 @@
             }
             }
         },
         },
         "node_modules/moment": {
         "node_modules/moment": {
-            "version": "2.29.3",
-            "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
-            "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
+            "version": "2.29.4",
+            "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+            "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
             "dev": true,
             "dev": true,
             "engines": {
             "engines": {
                 "node": "*"
                 "node": "*"
@@ -10956,9 +10956,9 @@
             "dev": true
             "dev": true
         },
         },
         "node_modules/terser": {
         "node_modules/terser": {
-            "version": "4.8.0",
-            "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
-            "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
+            "version": "4.8.1",
+            "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
+            "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
             "dev": true,
             "dev": true,
             "dependencies": {
             "dependencies": {
                 "commander": "^2.20.0",
                 "commander": "^2.20.0",
@@ -19298,9 +19298,9 @@
             }
             }
         },
         },
         "moment": {
         "moment": {
-            "version": "2.29.3",
-            "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
-            "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
+            "version": "2.29.4",
+            "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+            "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
             "dev": true
             "dev": true
         },
         },
         "move-concurrently": {
         "move-concurrently": {
@@ -22466,9 +22466,9 @@
             }
             }
         },
         },
         "terser": {
         "terser": {
-            "version": "4.8.0",
-            "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
-            "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
+            "version": "4.8.1",
+            "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
+            "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
             "dev": true,
             "dev": true,
             "requires": {
             "requires": {
                 "commander": "^2.20.0",
                 "commander": "^2.20.0",

+ 1 - 1
client/angular/package.json

@@ -57,7 +57,7 @@
         "karma-spec-reporter": "0.0.32",
         "karma-spec-reporter": "0.0.32",
         "karma-webpack": "5.0.0",
         "karma-webpack": "5.0.0",
         "lodash": ">=4.17.21",
         "lodash": ">=4.17.21",
-        "moment": "^2.29.3",
+        "moment": "^2.29.4",
         "ngtemplate-loader": "2.0.1",
         "ngtemplate-loader": "2.0.1",
         "node-sass": "^7.0.0",
         "node-sass": "^7.0.0",
         "postcss-loader": "2.1.1",
         "postcss-loader": "2.1.1",

+ 1 - 1
client/angular/src/i18n/translations_en.json

@@ -34,7 +34,7 @@
   "Display_InvalidVerification": "Viewing details only available after a user has been successfully verified",
   "Display_InvalidVerification": "Viewing details only available after a user has been successfully verified",
   "Display_MatchCondition": "Match Condition",
   "Display_MatchCondition": "Match Condition",
   "Display_NoResponses": "User does not have responses",
   "Display_NoResponses": "User does not have responses",
-  "Display_PasswordGeneration": "The following passwords have been randomly generated for you.  These passwords are based on real words to make them easier to remember, but have been modified to make them difficult to guess.",
+  "Display_PasswordGeneration": "The following passwords have been randomly generated for you.",
   "Display_PasswordPrompt": "Please type your new password",
   "Display_PasswordPrompt": "Please type your new password",
   "Display_PleaseWait": "Loading...",
   "Display_PleaseWait": "Loading...",
   "Display_Random": "Random",
   "Display_Random": "Random",

+ 0 - 7
client/angular/src/modules/configeditor/configeditor.module.ts

@@ -29,10 +29,3 @@ import ConfigEditorController from './configeditor.controller';
 module('configeditor.module', ['textAngular'])
 module('configeditor.module', ['textAngular'])
     .controller('ConfigEditorController', ConfigEditorController);
     .controller('ConfigEditorController', ConfigEditorController);
 
 
-// lowercase and uppercase have been removed from angular, but textAngular still hasn't caught up with the change. So
-// The following polyfills it for now:
-
-// @ts-ignore
-if (!angular.lowercase) angular.lowercase = (str) => str ? str.toLowerCase() : str;
-// @ts-ignore
-if (!angular.uppercase) angular.uppercase = (str) => str ? str.toUpperCase() : str;

+ 1 - 1
client/angular/src/modules/helpdesk/verifications-dialog.controller.ts

@@ -183,7 +183,7 @@ export default class VerificationsDialogController {
             })
             })
             .catch((reason) => {
             .catch((reason) => {
                 this.verificationStatus = STATUS_FAILED;
                 this.verificationStatus = STATUS_FAILED;
-            })
+            });
     }
     }
 
 
     onTokenDestinationChanged() {
     onTokenDestinationChanged() {

+ 3 - 3
client/angular/src/services/helpdesk.service.ts

@@ -78,16 +78,16 @@ export interface IVerificationOptions {
     verificationMethods: {
     verificationMethods: {
         optional: string[];
         optional: string[];
         required: string[];
         required: string[];
-    },
+    };
     verificationForm: [{
     verificationForm: [{
         name: string;
         name: string;
         label: string;
         label: string;
-    }],
+    }];
     tokenDestinations: [{
     tokenDestinations: [{
         id: string;
         id: string;
         display: string;
         display: string;
         type: string;
         type: string;
-    }]
+    }];
 }
 }
 
 
 export interface IVerificationStatus {
 export interface IVerificationStatus {

+ 1 - 1
client/angular/src/services/peoplesearch-config.service.ts

@@ -94,7 +94,7 @@ export default class PeopleSearchConfigService
                 maxExportDepth: results[3],
                 maxExportDepth: results[3],
                 emailTeamEnabled: results[4],
                 emailTeamEnabled: results[4],
                 maxEmailDepth: results[5]
                 maxEmailDepth: results[5]
-            }
+            };
         });
         });
     }
     }
 
 

+ 6 - 4
client/pom.xml

@@ -9,12 +9,13 @@
 
 
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
 
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-client</artifactId>
     <artifactId>pwm-client</artifactId>
     <packaging>jar</packaging>
     <packaging>jar</packaging>
 
 
     <properties>
     <properties>
-        <node.version>v16.15.1</node.version>
-        <npm.version>8.12.1</npm.version>
+        <node.version>v16.17.1</node.version>
+        <npm.version>8.19.2</npm.version>
     </properties>
     </properties>
 
 
     <name>PWM Password Self Service: Angular Client JAR</name>
     <name>PWM Password Self Service: Angular Client JAR</name>
@@ -32,7 +33,7 @@
         <plugins>
         <plugins>
             <plugin>
             <plugin>
                 <artifactId>maven-resources-plugin</artifactId>
                 <artifactId>maven-resources-plugin</artifactId>
-                <version>3.2.0</version>
+                <version>3.3.0</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <id>copy-client-files</id>
                         <id>copy-client-files</id>
@@ -41,6 +42,7 @@
                             <goal>copy-resources</goal>
                             <goal>copy-resources</goal>
                         </goals>
                         </goals>
                         <configuration>
                         <configuration>
+                            <propertiesEncoding>ISO-8859-1</propertiesEncoding>
                             <outputDirectory>${project.build.outputDirectory}/META-INF/resources/webjars/${project.artifactId}/${project.version}</outputDirectory>
                             <outputDirectory>${project.build.outputDirectory}/META-INF/resources/webjars/${project.artifactId}/${project.version}</outputDirectory>
                             <resources>
                             <resources>
                                 <resource>
                                 <resource>
@@ -126,7 +128,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.3.0</version>
                 <configuration>
                 <configuration>
                     <archive>
                     <archive>
                         <manifestEntries>
                         <manifestEntries>

+ 7 - 10
data-service/pom.xml

@@ -9,8 +9,8 @@
 
 
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
 
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-data-service</artifactId>
     <artifactId>pwm-data-service</artifactId>
-
     <packaging>war</packaging>
     <packaging>war</packaging>
 
 
     <name>PWM Password Self Service: Data Service WAR</name>
     <name>PWM Password Self Service: Data Service WAR</name>
@@ -73,7 +73,7 @@
             </plugin>
             </plugin>
             <plugin>
             <plugin>
                 <artifactId>maven-resources-plugin</artifactId>
                 <artifactId>maven-resources-plugin</artifactId>
-                <version>3.2.0</version>
+                <version>3.3.0</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <id>copy-resources</id>
                         <id>copy-resources</id>
@@ -82,6 +82,7 @@
                             <goal>copy-resources</goal>
                             <goal>copy-resources</goal>
                         </goals>
                         </goals>
                         <configuration>
                         <configuration>
+                            <propertiesEncoding>ISO-8859-1</propertiesEncoding>
                             <outputDirectory>${project.build.outputDirectory}/src</outputDirectory>
                             <outputDirectory>${project.build.outputDirectory}/src</outputDirectory>
                             <resources>
                             <resources>
                                 <resource><directory>src/main/java</directory></resource>
                                 <resource><directory>src/main/java</directory></resource>
@@ -99,7 +100,7 @@
             <plugin>
             <plugin>
                 <groupId>com.google.cloud.tools</groupId>
                 <groupId>com.google.cloud.tools</groupId>
                 <artifactId>jib-maven-plugin</artifactId>
                 <artifactId>jib-maven-plugin</artifactId>
-                <version>3.2.1</version>
+                <version>3.3.0</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <id>make-docker-image</id>
                         <id>make-docker-image</id>
@@ -127,6 +128,7 @@
                                 <jvmFlags>
                                 <jvmFlags>
                                     <jvmFlag>-server</jvmFlag>
                                     <jvmFlag>-server</jvmFlag>
                                     <jvmFlag>-Xmx256m</jvmFlag>
                                     <jvmFlag>-Xmx256m</jvmFlag>
+                                    <jvmFlag>-XX:+UseStringDeduplication</jvmFlag>
                                 </jvmFlags>
                                 </jvmFlags>
                                 <environment>
                                 <environment>
                                     <DATA_SERVICE_PROPS>/config/data-service.properties</DATA_SERVICE_PROPS>
                                     <DATA_SERVICE_PROPS>/config/data-service.properties</DATA_SERVICE_PROPS>
@@ -184,11 +186,6 @@
         </dependency>
         </dependency>
         <!-- / container dependencies -->
         <!-- / container dependencies -->
 
 
-        <dependency>
-            <groupId>commons-net</groupId>
-            <artifactId>commons-net</artifactId>
-            <version>3.8.0</version>
-        </dependency>
         <dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-csv</artifactId>
             <artifactId>commons-csv</artifactId>
@@ -202,12 +199,12 @@
         <dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
             <artifactId>slf4j-api</artifactId>
-            <version>2.0.0-alpha7</version>
+            <version>2.0.2</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>ch.qos.logback</groupId>
             <groupId>ch.qos.logback</groupId>
             <artifactId>logback-classic</artifactId>
             <artifactId>logback-classic</artifactId>
-            <version>1.3.0-alpha16</version>
+            <version>1.4.1</version>
         </dependency>
         </dependency>
     </dependencies>
     </dependencies>
 </project>
 </project>

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

@@ -37,7 +37,7 @@ public class ContextManager implements ServletContextListener
     {
     {
         app = new PwmReceiverApp();
         app = new PwmReceiverApp();
         sce.getServletContext().setAttribute( CONTEXT_ATTR, this );
         sce.getServletContext().setAttribute( CONTEXT_ATTR, this );
-        LOGGER.info( "open for bidness" );
+        LOGGER.info( () -> "open for bidness" );
     }
     }
 
 
     @Override
     @Override
@@ -45,7 +45,7 @@ public class ContextManager implements ServletContextListener
     {
     {
         app.close();
         app.close();
         app = null;
         app = null;
-        LOGGER.info( "cya!" );
+        LOGGER.info( () -> "cya!" );
     }
     }
 
 
     public PwmReceiverApp getApp( )
     public PwmReceiverApp getApp( )

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

@@ -20,6 +20,8 @@
 
 
 package password.pwm.receiver;
 package password.pwm.receiver;
 
 
+import java.util.function.Supplier;
+
 public class Logger
 public class Logger
 {
 {
     private final org.slf4j.Logger logger;
     private final org.slf4j.Logger logger;
@@ -34,13 +36,13 @@ public class Logger
         return new Logger( classname );
         return new Logger( classname );
     }
     }
 
 
-    public void info( final String input )
+    public void info( final Supplier<String> input )
     {
     {
-        logger.info( input );
+        logger.makeLoggingEventBuilder( org.slf4j.event.Level.INFO ).log( input );
     }
     }
 
 
-    public void debug( final String input )
+    public void debug( final Supplier<String> input )
     {
     {
-        logger.debug( input );
+        logger.makeLoggingEventBuilder( org.slf4j.event.Level.DEBUG ).log( input );
     }
     }
 }
 }

+ 4 - 7
data-service/src/main/java/password/pwm/receiver/PublishVersionServlet.java

@@ -21,7 +21,6 @@
 package password.pwm.receiver;
 package password.pwm.receiver;
 
 
 import password.pwm.bean.pub.PublishVersionBean;
 import password.pwm.bean.pub.PublishVersionBean;
-import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.RestResultBean;
 
 
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.annotation.WebServlet;
@@ -38,19 +37,17 @@ import java.util.Collections;
 )
 )
 public class PublishVersionServlet extends HttpServlet
 public class PublishVersionServlet extends HttpServlet
 {
 {
-    private static final Logger LOGGER = Logger.createLogger( PublishVersionServlet.class );
-    private static final AtomicLoopIntIncrementer REQ_COUNTER = new AtomicLoopIntIncrementer();
-
-
     @Override
     @Override
     protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
     protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
             throws IOException
             throws IOException
     {
     {
-        final int requestId = REQ_COUNTER.next();
-        LOGGER.debug( "http request #" + requestId + " for version" );
 
 
         final ContextManager contextManager = ContextManager.getContextManager( req.getServletContext() );
         final ContextManager contextManager = ContextManager.getContextManager( req.getServletContext() );
         final PwmReceiverApp app = contextManager.getApp();
         final PwmReceiverApp app = contextManager.getApp();
+
+        app.getStatisticCounterBundle().increment( PwmReceiverApp.CounterStatsKey.VersionCheckRequests );
+        app.getStatisticEpsBundle().markEvent( PwmReceiverApp.EpsStatKey.VersionCheckRequests );
+
         final PublishVersionBean publishVersionBean = new PublishVersionBean(
         final PublishVersionBean publishVersionBean = new PublishVersionBean(
                 Collections.singletonMap( PublishVersionBean.VersionKey.current, app.getSettings().getCurrentVersionInfo() ) );
                 Collections.singletonMap( PublishVersionBean.VersionKey.current, app.getSettings().getCurrentVersionInfo() ) );
 
 

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

@@ -21,9 +21,12 @@
 package password.pwm.receiver;
 package password.pwm.receiver;
 
 
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StatisticCounterBundle;
+import password.pwm.util.java.StatisticRateBundle;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 
 
 import java.io.IOException;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
@@ -38,6 +41,24 @@ public class PwmReceiverApp
 
 
     private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
     private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
     private final Status status = new Status();
     private final Status status = new Status();
+    private final StatisticCounterBundle statisticCounterBundle = new StatisticCounterBundle( CounterStatsKey.class );
+    private final StatisticRateBundle statisticRateBundle = new StatisticRateBundle( EpsStatKey.class );
+    private final Instant startupTime = Instant.now();
+
+    public enum EpsStatKey
+    {
+        VersionCheckRequests,
+        TelemetryPublishRequests,
+        TelemetryViewRequests,
+    }
+
+    public enum CounterStatsKey
+    {
+        VersionCheckRequests,
+        TelemetryPublishRequests,
+        TelemetryViewRequests,
+    }
+
 
 
     public PwmReceiverApp( )
     public PwmReceiverApp( )
     {
     {
@@ -106,4 +127,18 @@ public class PwmReceiverApp
         return status;
         return status;
     }
     }
 
 
+    public StatisticCounterBundle getStatisticCounterBundle()
+    {
+        return statisticCounterBundle;
+    }
+
+    public StatisticRateBundle getStatisticEpsBundle()
+    {
+        return statisticRateBundle;
+    }
+
+    public Instant getStartupTime()
+    {
+        return startupTime;
+    }
 }
 }

+ 115 - 0
data-service/src/main/java/password/pwm/receiver/RequestLoggerFilter.java

@@ -0,0 +1,115 @@
+/*
+ * 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.util.java.AtomicLoopLongIncrementer;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.annotation.WebFilter;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.time.Instant;
+import java.util.List;
+
+@WebFilter( filterName = "RequestLoggerFilter", urlPatterns = "*" )
+public class RequestLoggerFilter implements Filter
+{
+    private static final Logger LOGGER = Logger.createLogger( TelemetryViewerServlet.class );
+    private static final AtomicLoopLongIncrementer REQ_COUNTER = new AtomicLoopLongIncrementer();
+
+    @Override
+    public void doFilter( final ServletRequest request, final ServletResponse response, final FilterChain chain )
+            throws ServletException, IOException
+    {
+        final Instant startTime = Instant.now();
+
+        chain.doFilter( request, response );
+
+        final HttpServletRequest req = ( HttpServletRequest ) request;
+        final long requestId = REQ_COUNTER.next();
+        final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
+
+        LOGGER.debug( () -> "http request #" + requestId + " for "
+                + req.getRequestURI() + " from "
+                + getSrcDisplayString( req )
+                + " (" + timeDuration.asCompactString() + ")" );
+    }
+
+    @Override
+    public void init( final FilterConfig filterConfig )
+            throws ServletException
+    {
+        Filter.super.init( filterConfig );
+    }
+
+    @Override
+    public void destroy()
+    {
+        Filter.super.destroy();
+    }
+
+    private static String getSrcDisplayString( final HttpServletRequest request )
+    {
+        final String address = srcAddress( request );
+        final String hostname = srcHostname( request );
+
+        if ( StringUtil.isEmpty( hostname ) || hostname.equals( address ) )
+        {
+            return address;
+        }
+
+        return address + "/" + hostname;
+    }
+
+    public static String srcAddress( final HttpServletRequest request )
+    {
+        final String xForwardedForValue = request.getHeader( "X-Forwarded-For" );
+        if ( StringUtil.isEmpty( xForwardedForValue ) )
+        {
+            return request.getRemoteAddr();
+        }
+
+        final List<String> values = StringUtil.splitAndTrim( xForwardedForValue, "," );
+        return values.get( 0 );
+    }
+
+    public static String srcHostname( final HttpServletRequest request )
+    {
+        final String addr = srcAddress( request );
+        try
+        {
+            return InetAddress.getByName( addr ).getHostName();
+        }
+        catch ( final Exception e )
+        {
+            /* ignore */
+        }
+        return addr;
+    }
+}

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

@@ -21,7 +21,7 @@
 package password.pwm.receiver;
 package password.pwm.receiver;
 
 
 import password.pwm.bean.VersionNumber;
 import password.pwm.bean.VersionNumber;
-import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 
 
@@ -87,7 +87,7 @@ public class Settings
         try ( Reader reader = new InputStreamReader( Files.newInputStream( path ), StandardCharsets.UTF_8 ) )
         try ( Reader reader = new InputStreamReader( Files.newInputStream( path ), StandardCharsets.UTF_8 ) )
         {
         {
             properties.load( reader );
             properties.load( reader );
-            final Map<Setting, String> returnMap = CollectionUtil.enumStream( Setting.class )
+            final Map<Setting, String> returnMap = EnumUtil.enumStream( Setting.class )
                     .collect( Collectors.toUnmodifiableMap(
                     .collect( Collectors.toUnmodifiableMap(
                             setting -> setting,
                             setting -> setting,
                             setting -> properties.getProperty( setting.name(), setting.getDefaultValue() )
                             setting -> properties.getProperty( setting.name(), setting.getDefaultValue() )
@@ -128,7 +128,7 @@ public class Settings
         }
         }
         catch ( final Exception e )
         catch ( final Exception e )
         {
         {
-            LOGGER.info( "error parsing version string from setting properties: " + e.getMessage() );
+            LOGGER.info( () -> "error parsing version string from setting properties: " + e.getMessage() );
             return VersionNumber.ZERO;
             return VersionNumber.ZERO;
         }
         }
     }
     }

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

@@ -67,12 +67,12 @@ public class Storage
         final EnvironmentConfig environmentConfig = new EnvironmentConfig();
         final EnvironmentConfig environmentConfig = new EnvironmentConfig();
         environment = Environments.newInstance( storagePath.getAbsolutePath(), environmentConfig );
         environment = Environments.newInstance( storagePath.getAbsolutePath(), environmentConfig );
 
 
-        LOGGER.info( "environment open" );
+        LOGGER.info( () -> "environment open" );
 
 
         environment.executeInTransaction( txn -> store
         environment.executeInTransaction( txn -> store
                 = environment.openStore( STORE_NAME, StoreConfig.WITHOUT_DUPLICATES, txn ) );
                 = environment.openStore( STORE_NAME, StoreConfig.WITHOUT_DUPLICATES, txn ) );
 
 
-        LOGGER.info( "store open with " + count() + " records" );
+        LOGGER.info( () -> "store open with " + count() + " records" );
     }
     }
 
 
     public void store( final TelemetryPublishBean bean )
     public void store( final TelemetryPublishBean bean )

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

@@ -26,6 +26,7 @@ import password.pwm.PwmAboutProperty;
 import password.pwm.bean.TelemetryPublishBean;
 import password.pwm.bean.TelemetryPublishBean;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 
 
 import java.time.Duration;
 import java.time.Duration;
@@ -45,6 +46,7 @@ public class SummaryBean
     private Map<String, Integer> settingCount;
     private Map<String, Integer> settingCount;
     private Map<String, Integer> statCount;
     private Map<String, Integer> statCount;
     private Map<String, Integer> osCount;
     private Map<String, Integer> osCount;
+    private Map<String, Integer> deploymentCount;
     private Map<String, Integer> dbCount;
     private Map<String, Integer> dbCount;
     private Map<String, Integer> javaCount;
     private Map<String, Integer> javaCount;
     private Map<String, Integer> appVersionCount;
     private Map<String, Integer> appVersionCount;
@@ -60,6 +62,7 @@ public class SummaryBean
         final Map<String, Integer> appServerCount = new TreeMap<>();
         final Map<String, Integer> appServerCount = new TreeMap<>();
         final Map<String, Integer> settingCount = new TreeMap<>();
         final Map<String, Integer> settingCount = new TreeMap<>();
         final Map<String, Integer> statCount = new TreeMap<>();
         final Map<String, Integer> statCount = new TreeMap<>();
+        final Map<String, Integer> deploymentCount = new TreeMap<>();
         final Map<String, Integer> osCount = new TreeMap<>();
         final Map<String, Integer> osCount = new TreeMap<>();
         final Map<String, Integer> dbCount = new TreeMap<>();
         final Map<String, Integer> dbCount = new TreeMap<>();
         final Map<String, Integer> javaCount = new TreeMap<>();
         final Map<String, Integer> javaCount = new TreeMap<>();
@@ -86,6 +89,7 @@ public class SummaryBean
                         .installAge( TimeDuration.fromCurrent( bean.getInstallTime() ).asDuration() )
                         .installAge( TimeDuration.fromCurrent( bean.getInstallTime() ).asDuration() )
                         .updateAge( TimeDuration.fromCurrent( bean.getTimestamp() ).asDuration() )
                         .updateAge( TimeDuration.fromCurrent( bean.getTimestamp() ).asDuration() )
                         .ldapVendor( ldapVendor )
                         .ldapVendor( ldapVendor )
+
                         .osName( bean.getAbout().get( PwmAboutProperty.java_osName.name() ) )
                         .osName( bean.getAbout().get( PwmAboutProperty.java_osName.name() ) )
                         .osVersion( bean.getAbout().get( PwmAboutProperty.java_osVersion.name() ) )
                         .osVersion( bean.getAbout().get( PwmAboutProperty.java_osVersion.name() ) )
                         .servletName( bean.getAbout().get( PwmAboutProperty.java_appServerInfo.name() ) )
                         .servletName( bean.getAbout().get( PwmAboutProperty.java_appServerInfo.name() ) )
@@ -106,6 +110,8 @@ public class SummaryBean
 
 
                 incrementCounterMap( javaCount, siteSummary.getJavaVm() );
                 incrementCounterMap( javaCount, siteSummary.getJavaVm() );
 
 
+                incrementCounterMap( deploymentCount, bean.getAbout().get( PwmAboutProperty.app_deployment_type.name() ) );
+
                 incrementCounterMap( appVersionCount, siteSummary.getVersion() );
                 incrementCounterMap( appVersionCount, siteSummary.getVersion() );
 
 
                 for ( final String settingKey : bean.getConfiguredSettings() )
                 for ( final String settingKey : bean.getConfiguredSettings() )
@@ -138,6 +144,7 @@ public class SummaryBean
                 .appServerCount( appServerCount )
                 .appServerCount( appServerCount )
                 .osCount( osCount )
                 .osCount( osCount )
                 .dbCount( dbCount )
                 .dbCount( dbCount )
+                .deploymentCount( deploymentCount )
                 .javaCount( javaCount )
                 .javaCount( javaCount )
                 .appVersionCount( appVersionCount )
                 .appVersionCount( appVersionCount )
                 .build();
                 .build();
@@ -151,6 +158,11 @@ public class SummaryBean
 
 
     private static void incrementCounterMap( final Map<String, Integer> map, final String key, final int count )
     private static void incrementCounterMap( final Map<String, Integer> map, final String key, final int count )
     {
     {
+        if ( map == null || StringUtil.isEmpty( key ) )
+        {
+            return;
+        }
+
         if ( map.containsKey( key ) )
         if ( map.containsKey( key ) )
         {
         {
             map.put( key, map.get( key ) + count );
             map.put( key, map.get( key ) + count );
@@ -198,6 +210,7 @@ public class SummaryBean
         private String osName;
         private String osName;
         private String osVersion;
         private String osVersion;
         private String servletName;
         private String servletName;
+        private String deploymentType;
         private String dbVendor;
         private String dbVendor;
         private String javaVm;
         private String javaVm;
         private String platform;
         private String platform;

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

@@ -26,7 +26,6 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.ServletUtility;
 import password.pwm.http.ServletUtility;
 import password.pwm.i18n.Message;
 import password.pwm.i18n.Message;
-import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.RestResultBean;
 
 
@@ -46,8 +45,6 @@ import java.io.IOException;
 public class TelemetryRestReceiver extends HttpServlet
 public class TelemetryRestReceiver extends HttpServlet
 {
 {
     private static final Logger LOGGER = Logger.createLogger( TelemetryViewerServlet.class );
     private static final Logger LOGGER = Logger.createLogger( TelemetryViewerServlet.class );
-    private static final AtomicLoopIntIncrementer REQ_COUNTER = new AtomicLoopIntIncrementer();
-
 
 
     @Override
     @Override
     protected void doPost( final HttpServletRequest req, final HttpServletResponse resp )
     protected void doPost( final HttpServletRequest req, final HttpServletResponse resp )
@@ -55,17 +52,19 @@ public class TelemetryRestReceiver extends HttpServlet
     {
     {
         try
         try
         {
         {
-            final int requestId = REQ_COUNTER.next();
-            LOGGER.debug( "http rest request #" + requestId + " for telemetry update" );
+            final ContextManager contextManager = ContextManager.getContextManager( req.getServletContext() );
+            final PwmReceiverApp app = contextManager.getApp();
+            app.getStatisticCounterBundle().increment( PwmReceiverApp.CounterStatsKey.TelemetryPublishRequests );
+            app.getStatisticEpsBundle().markEvent( PwmReceiverApp.EpsStatKey.TelemetryPublishRequests );
 
 
             final String input = ServletUtility.readRequestBodyAsString( req, 1024 * 1024 );
             final String input = ServletUtility.readRequestBodyAsString( req, 1024 * 1024 );
             final TelemetryPublishBean telemetryPublishBean = JsonFactory.get().deserialize( input, TelemetryPublishBean.class );
             final TelemetryPublishBean telemetryPublishBean = JsonFactory.get().deserialize( input, TelemetryPublishBean.class );
-            final Storage storage = ContextManager.getContextManager( this.getServletContext() ).getApp().getStorage();
+            final Storage storage = app.getStorage();
             storage.store( telemetryPublishBean );
             storage.store( telemetryPublishBean );
 
 
             final RestResultBean restResultBean = RestResultBean.forSuccessMessage( null, null, null, Message.Success_Unknown );
             final RestResultBean restResultBean = RestResultBean.forSuccessMessage( null, null, null, Message.Success_Unknown );
             ReceiverUtil.outputJsonResponse( req, resp, restResultBean );
             ReceiverUtil.outputJsonResponse( req, resp, restResultBean );
-            LOGGER.debug( "http rest request #" + requestId + " received from " + telemetryPublishBean.getSiteDescription() );
+            LOGGER.debug( () -> "http telemetry rest data received from " + telemetryPublishBean.getSiteDescription() );
         }
         }
         catch ( final PwmUnrecoverableException e )
         catch ( final PwmUnrecoverableException e )
         {
         {

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

@@ -20,7 +20,6 @@
 
 
 package password.pwm.receiver;
 package password.pwm.receiver;
 
 
-import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 
 
 import javax.servlet.ServletException;
 import javax.servlet.ServletException;
@@ -40,31 +39,27 @@ import java.time.temporal.ChronoUnit;
 )
 )
 public class TelemetryViewerServlet extends HttpServlet
 public class TelemetryViewerServlet extends HttpServlet
 {
 {
-    private static final Logger LOGGER = Logger.createLogger( TelemetryViewerServlet.class );
     private static final String PARAM_DAYS = "days";
     private static final String PARAM_DAYS = "days";
-    private static final AtomicLoopIntIncrementer REQ_COUNTER = new AtomicLoopIntIncrementer();
 
 
     public static final String SUMMARY_ATTR = "SummaryBean";
     public static final String SUMMARY_ATTR = "SummaryBean";
 
 
-
     @Override
     @Override
     protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
     protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
             throws ServletException, IOException
             throws ServletException, IOException
     {
     {
-        final int requestId = REQ_COUNTER.next();
-        LOGGER.debug( "http request #" + requestId + " for viewer" );
         final String daysString = req.getParameter( PARAM_DAYS );
         final String daysString = req.getParameter( PARAM_DAYS );
         final int days = StringUtil.isEmpty( daysString ) ? 30 : Integer.parseInt( daysString );
         final int days = StringUtil.isEmpty( daysString ) ? 30 : Integer.parseInt( daysString );
         final ContextManager contextManager = ContextManager.getContextManager( req.getServletContext() );
         final ContextManager contextManager = ContextManager.getContextManager( req.getServletContext() );
+
         final PwmReceiverApp app = contextManager.getApp();
         final PwmReceiverApp app = contextManager.getApp();
+        app.getStatisticCounterBundle().increment( PwmReceiverApp.CounterStatsKey.TelemetryViewRequests );
+        app.getStatisticEpsBundle().markEvent( PwmReceiverApp.EpsStatKey.TelemetryViewRequests );
 
 
         {
         {
             final String errorState = app.getStatus().getErrorState();
             final String errorState = app.getStatus().getErrorState();
             if ( StringUtil.notEmpty( errorState ) )
             if ( StringUtil.notEmpty( errorState ) )
             {
             {
                 resp.sendError( 500, errorState );
                 resp.sendError( 500, errorState );
-                final String htmlBody = "<html>Error: " + errorState + "</html>";
-                resp.getWriter().print( htmlBody );
                 return;
                 return;
             }
             }
         }
         }

+ 36 - 3
data-service/src/main/webapp/WEB-INF/jsp/telemetry-viewer.jsp

@@ -27,11 +27,15 @@
 <%@ page import="password.pwm.receiver.PwmReceiverApp" %>
 <%@ page import="password.pwm.receiver.PwmReceiverApp" %>
 <%@ page import="password.pwm.receiver.ContextManager" %>
 <%@ page import="password.pwm.receiver.ContextManager" %>
 <%@ page import="password.pwm.util.java.StringUtil" %>
 <%@ page import="password.pwm.util.java.StringUtil" %>
+<%@ page import="java.util.Map" %>
+<%@ page import="java.time.Duration" %>
+<%@ page import="password.pwm.util.java.PwmNumberFormat" %>
 
 
 <!DOCTYPE html>
 <!DOCTYPE html>
 <%@ page contentType="text/html" %>
 <%@ page contentType="text/html" %>
 <% SummaryBean summaryBean = (SummaryBean)request.getAttribute(TelemetryViewerServlet.SUMMARY_ATTR); %>
 <% SummaryBean summaryBean = (SummaryBean)request.getAttribute(TelemetryViewerServlet.SUMMARY_ATTR); %>
 <% PwmReceiverApp app = ContextManager.getContextManager(request.getServletContext()).getApp(); %>
 <% PwmReceiverApp app = ContextManager.getContextManager(request.getServletContext()).getApp(); %>
+<% PwmNumberFormat format = PwmNumberFormat.forLocale( request.getLocale() ); %>
 <html>
 <html>
 <head>
 <head>
     <title>Telemetry Data</title>
     <title>Telemetry Data</title>
@@ -40,8 +44,11 @@
 </head>
 </head>
 <body>
 <body>
 <div>
 <div>
+    <h2>Server Info</h2>
     Current Time: <%=StringUtil.toIsoDate( Instant.now() )%>
     Current Time: <%=StringUtil.toIsoDate( Instant.now() )%>
     <br/>
     <br/>
+    Up Time: <%=StringUtil.toIsoDuration(Duration.between(app.getStartupTime(), Instant.now()))%>
+    <br/>
     <% if (app.getSettings().isFtpEnabled()) {%>
     <% if (app.getSettings().isFtpEnabled()) {%>
     <% Instant lastIngest = app.getStatus().getLastFtpIngest(); %>
     <% Instant lastIngest = app.getStatus().getLastFtpIngest(); %>
     Last FTP Ingestion: <%= lastIngest == null ? "n/a" : lastIngest.toString()%>
     Last FTP Ingestion: <%= lastIngest == null ? "n/a" : lastIngest.toString()%>
@@ -55,15 +62,28 @@
     <br/>
     <br/>
     Servers Shown: <%= summaryBean.getServerCount() %>
     Servers Shown: <%= summaryBean.getServerCount() %>
     <br/>
     <br/>
+    <h3>Counters</h3>
+    <% final Map<String, String> counterStatMap = app.getStatisticCounterBundle().debugStats( request.getLocale() ); %>
+    <% for ( final Map.Entry<String, String> entry : counterStatMap.entrySet() ) { %>
+    <%= entry.getKey() %>: <%= entry.getValue()%><br/>
+    <% } %>
+    <br/>
+    <h3>Events/Second</h3>
+    <% final Map<String, String> epsStatMap = app.getStatisticEpsBundle().debugStats( request.getLocale() ); %>
+    <% for ( final Map.Entry<String, String> entry : epsStatMap.entrySet() ) { %>
+    <%= entry.getKey() %>: <%= entry.getValue()%><br/>
+    <% } %>
     <br/>
     <br/>
+    <h1>PWM Telemetry Data</h1>
 
 
+    <%--
     <form method="get">
     <form method="get">
         <label>Servers that have sent data in last number of days
         <label>Servers that have sent data in last number of days
             <input type="number" name="days" id="days" value="30" max="3650" min="1">
             <input type="number" name="days" id="days" value="30" max="3650" min="1">
         </label>
         </label>
         <button type="submit">Update</button>
         <button type="submit">Update</button>
     </form>
     </form>
-
+    --%>
     <h2>Versions</h2>
     <h2>Versions</h2>
     <table class="sortable">
     <table class="sortable">
         <tr>
         <tr>
@@ -116,6 +136,19 @@
         </tr>
         </tr>
         <% } %>
         <% } %>
     </table>
     </table>
+    <h2>Deployment Type</h2>
+    <table class="sortable">
+        <tr>
+            <td><b>Deployment Type</b></td>
+            <td><b>Count</b></td>
+        </tr>
+        <% for (final String osName : summaryBean.getDeploymentCount().keySet()) { %>
+        <tr>
+            <td><%=osName%></td>
+            <td><%=summaryBean.getDeploymentCount().get(osName)%></td>
+        </tr>
+        <% } %>
+    </table>
     <h2>DB Vendors</h2>
     <h2>DB Vendors</h2>
     <table class="sortable">
     <table class="sortable">
         <tr>
         <tr>
@@ -151,7 +184,7 @@
         <% for (final String setting: summaryBean.getSettingCount().keySet()) { %>
         <% for (final String setting: summaryBean.getSettingCount().keySet()) { %>
         <tr>
         <tr>
             <td><%=setting%></td>
             <td><%=setting%></td>
-            <td><%=summaryBean.getSettingCount().get(setting)%></td>
+            <td><%=format.format(summaryBean.getSettingCount().get(setting).longValue())%></td>
         </tr>
         </tr>
         <% } %>
         <% } %>
     </table>
     </table>
@@ -164,7 +197,7 @@
         <% for (final String statistic: summaryBean.getStatCount().keySet()) { %>
         <% for (final String statistic: summaryBean.getStatCount().keySet()) { %>
         <tr>
         <tr>
             <td><%=statistic%></td>
             <td><%=statistic%></td>
-            <td><%=summaryBean.getStatCount().get(statistic)%></td>
+            <td><%=format.format(summaryBean.getStatCount().get(statistic).longValue())%></td>
         </tr>
         </tr>
         <% } %>
         <% } %>
     </table>
     </table>

+ 4 - 4
docker/pom.xml

@@ -9,9 +9,9 @@
 
 
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
 
 
-    <packaging>jar</packaging>
-
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-docker</artifactId>
     <artifactId>pwm-docker</artifactId>
+    <packaging>jar</packaging>
 
 
     <name>PWM Password Self Service: Docker Image</name>
     <name>PWM Password Self Service: Docker Image</name>
 
 
@@ -34,7 +34,7 @@
             <plugin>
             <plugin>
                 <groupId>com.google.cloud.tools</groupId>
                 <groupId>com.google.cloud.tools</groupId>
                 <artifactId>jib-maven-plugin</artifactId>
                 <artifactId>jib-maven-plugin</artifactId>
-                <version>3.2.1</version>
+                <version>3.3.0</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <id>make-docker-image</id>
                         <id>make-docker-image</id>
@@ -45,7 +45,7 @@
                         <configuration>
                         <configuration>
                             <skip>${skipDocker}</skip>
                             <skip>${skipDocker}</skip>
                             <from>
                             <from>
-                                <image>openjdk:17-alpine</image>
+                                <image>eclipse-temurin:18-jre</image>
                             </from>
                             </from>
                             <to>
                             <to>
                                 <image>${dockerImageTag}</image>
                                 <image>${dockerImageTag}</image>

+ 1 - 0
docker/src/main/image-files/app/java.vmoptions

@@ -1,4 +1,5 @@
 -server
 -server
 -Xmx1g
 -Xmx1g
 -Xms1g
 -Xms1g
+-XX:+UseStringDeduplication
 -Xlog:gc:file=/config/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=10M
 -Xlog:gc:file=/config/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=10M

+ 3 - 2
lib-data/pom.xml

@@ -9,6 +9,7 @@
 
 
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
 
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-lib-data</artifactId>
     <artifactId>pwm-lib-data</artifactId>
     <packaging>jar</packaging>
     <packaging>jar</packaging>
 
 
@@ -40,7 +41,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.3.0</version>
                 <configuration>
                 <configuration>
                     <archive>
                     <archive>
                         <manifestEntries>
                         <manifestEntries>
@@ -70,7 +71,7 @@
         <dependency>
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>
             <artifactId>gson</artifactId>
-            <version>2.9.0</version>
+            <version>2.9.1</version>
         </dependency>
         </dependency>
     </dependencies>
     </dependencies>
 </project>
 </project>

+ 31 - 0
lib-data/src/main/java/password/pwm/data/FileUploadItem.java

@@ -0,0 +1,31 @@
+/*
+ * 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.data;
+
+import lombok.Value;
+
+@Value
+public class FileUploadItem
+{
+    private final String name;
+    private final String type;
+    private final ImmutableByteArray content;
+}

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

@@ -20,8 +20,8 @@
 
 
 package password.pwm.bean;
 package password.pwm.bean;
 
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Collections;
@@ -34,12 +34,12 @@ public class VersionNumberTest
     {
     {
         {
         {
             final VersionNumber versionNumber = VersionNumber.parse( "v1.2.3" );
             final VersionNumber versionNumber = VersionNumber.parse( "v1.2.3" );
-            Assert.assertEquals( VersionNumber.of( 1, 2, 3 ), versionNumber );
+            Assertions.assertEquals( VersionNumber.of( 1, 2, 3 ), versionNumber );
         }
         }
 
 
         {
         {
             final VersionNumber versionNumber = VersionNumber.parse( "v1.2" );
             final VersionNumber versionNumber = VersionNumber.parse( "v1.2" );
-            Assert.assertEquals( VersionNumber.of( 1, 2, 0 ), versionNumber );
+            Assertions.assertEquals( VersionNumber.of( 1, 2, 0 ), versionNumber );
         }
         }
     }
     }
 
 
@@ -57,11 +57,11 @@ public class VersionNumberTest
 
 
         Collections.sort( list );
         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 ) );
+        Assertions.assertEquals( VersionNumber.of( 1, 3, 3 ), list.get( 0 ) );
+        Assertions.assertEquals( VersionNumber.of( 1, 3, 5 ), list.get( 1 ) );
+        Assertions.assertEquals( VersionNumber.of( 1, 4, 5 ), list.get( 2 ) );
+        Assertions.assertEquals( VersionNumber.of( 3, 2, 3 ), list.get( 3 ) );
+        Assertions.assertEquals( VersionNumber.of( 7, 0, 11 ), list.get( 4 ) );
+        Assertions.assertEquals( VersionNumber.of( 42, 2, 1 ), list.get( 5 ) );
     }
     }
 }
 }

+ 2 - 1
lib-util/pom.xml

@@ -9,6 +9,7 @@
 
 
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
 
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-lib-util</artifactId>
     <artifactId>pwm-lib-util</artifactId>
     <packaging>jar</packaging>
     <packaging>jar</packaging>
 
 
@@ -40,7 +41,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.3.0</version>
                 <configuration>
                 <configuration>
                     <archive>
                     <archive>
                         <manifestEntries>
                         <manifestEntries>

+ 19 - 11
server/src/main/java/password/pwm/util/EventRateMeter.java → lib-util/src/main/java/password/pwm/util/EventRateMeter.java

@@ -20,29 +20,27 @@
 
 
 package password.pwm.util;
 package password.pwm.util;
 
 
-import password.pwm.util.java.MovingAverage;
-import password.pwm.util.java.TimeDuration;
+import password.pwm.util.java.PwmNumberFormat;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.math.BigDecimal;
+import java.time.Duration;
+import java.util.Locale;
+import java.util.Objects;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.concurrent.locks.ReentrantLock;
 
 
 public class EventRateMeter implements Serializable
 public class EventRateMeter implements Serializable
 {
 {
-    private final TimeDuration maxDuration;
+    private final long maxDuration;
     private final Lock lock = new ReentrantLock();
     private final Lock lock = new ReentrantLock();
 
 
     private volatile MovingAverage movingAverage;
     private volatile MovingAverage movingAverage;
     private volatile double remainder;
     private volatile double remainder;
 
 
-    public EventRateMeter( final TimeDuration maxDuration )
+    public EventRateMeter( final Duration maxDuration )
     {
     {
-        if ( maxDuration == null )
-        {
-            throw new NullPointerException( "maxDuration cannot be null" );
-        }
-        this.maxDuration = maxDuration;
+        this.maxDuration = Objects.requireNonNull( maxDuration ) .toMillis();
         reset();
         reset();
     }
     }
 
 
@@ -51,7 +49,7 @@ public class EventRateMeter implements Serializable
         lock.lock();
         lock.lock();
         try
         try
         {
         {
-            movingAverage = new MovingAverage( maxDuration.asMillis() );
+            movingAverage = new MovingAverage( Duration.ofMillis( maxDuration ) );
             remainder = 0;
             remainder = 0;
         }
         }
         finally
         finally
@@ -60,6 +58,11 @@ public class EventRateMeter implements Serializable
         }
         }
     }
     }
 
 
+    public void markEvent()
+    {
+        markEvents( 1 );
+    }
+
     public void markEvents( final int eventCount )
     public void markEvents( final int eventCount )
     {
     {
         lock.lock();
         lock.lock();
@@ -83,7 +86,12 @@ public class EventRateMeter implements Serializable
         }
         }
     }
     }
 
 
-    public BigDecimal readEventRate( )
+    public String prettyEps( final Locale locale )
+    {
+        return PwmNumberFormat.prettyBigDecimal( rawEps(), 3, locale );
+    }
+
+    public BigDecimal rawEps( )
     {
     {
         lock.lock();
         lock.lock();
         try
         try

+ 3 - 11
lib-util/src/main/java/password/pwm/util/java/MovingAverage.java → lib-util/src/main/java/password/pwm/util/MovingAverage.java

@@ -18,7 +18,7 @@
  * limitations under the License.
  * limitations under the License.
  */
  */
 
 
-package password.pwm.util.java;
+package password.pwm.util;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.text.NumberFormat;
 import java.text.NumberFormat;
@@ -59,20 +59,12 @@ public class MovingAverage implements Serializable
     private volatile long lastMillis;
     private volatile long lastMillis;
     private volatile double average;
     private volatile double average;
 
 
-
     /**
     /**
      * Construct a {@link MovingAverage}, providing the time window
      * Construct a {@link MovingAverage}, providing the time window
-     * we want the average over. For example, providing a value of
-     * 3,600,000 provides a moving average over the last hour.
+     * we want the average over.
      *
      *
-     * @param windowMillis the length of the sliding window in
-     *                     milliseconds
+     * @param timeDuration the length of the sliding window.
      */
      */
-    public MovingAverage( final long windowMillis )
-    {
-        this.windowMillis = windowMillis;
-    }
-
     public MovingAverage( final Duration timeDuration )
     public MovingAverage( final Duration timeDuration )
     {
     {
         this.windowMillis = timeDuration.toMillis();
         this.windowMillis = timeDuration.toMillis();

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

@@ -18,7 +18,7 @@
  * limitations under the License.
  * limitations under the License.
  */
  */
 
 
-package password.pwm.util.java;
+package password.pwm.util;
 
 
 import java.math.BigDecimal;
 import java.math.BigDecimal;
 import java.math.MathContext;
 import java.math.MathContext;

+ 0 - 1
lib-util/src/main/java/password/pwm/util/ProgressInfoCalculator.java

@@ -20,7 +20,6 @@
 
 
 package password.pwm.util;
 package password.pwm.util;
 
 
-import password.pwm.util.java.Percent;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 
 
 import java.io.Serializable;
 import java.io.Serializable;

+ 13 - 21
server/src/main/java/password/pwm/util/TransactionSizeCalculator.java → lib-util/src/main/java/password/pwm/util/TransactionSizeCalculator.java

@@ -24,7 +24,7 @@ import lombok.Builder;
 import lombok.Value;
 import lombok.Value;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 
 
-import java.util.Objects;
+import java.time.Duration;
 
 
 public class TransactionSizeCalculator
 public class TransactionSizeCalculator
 {
 {
@@ -42,32 +42,29 @@ public class TransactionSizeCalculator
     public void reset( )
     public void reset( )
     {
     {
         transactionSize = settings.getMinTransactions();
         transactionSize = settings.getMinTransactions();
-        lastDuration = settings.getDurationGoal().asMillis();
+        lastDuration = settings.getDurationGoal();
     }
     }
 
 
-    public void recordLastTransactionDuration( final long duration )
+    public void recordLastTransactionDuration( final Duration duration )
     {
     {
-        recordLastTransactionDuration( TimeDuration.of( duration, TimeDuration.Unit.MILLISECONDS ) );
+        recordLastTransactionDuration( duration.toMillis() );
     }
     }
 
 
-    @SuppressWarnings( "ResultOfMethodCallIgnored" )
     public void pause( )
     public void pause( )
     {
     {
-        final long pauseTimeMs = Math.min( lastDuration, settings.getDurationGoal().asMillis() * 2 );
+        final long pauseTimeMs = Math.min( lastDuration, settings.getDurationGoal() * 2 );
         TimeDuration.of( pauseTimeMs, TimeDuration.Unit.MILLISECONDS ).pause();
         TimeDuration.of( pauseTimeMs, TimeDuration.Unit.MILLISECONDS ).pause();
     }
     }
 
 
-    public void recordLastTransactionDuration( final TimeDuration duration )
+    public void recordLastTransactionDuration(  final long duration  )
     {
     {
-        Objects.requireNonNull( duration );
-
-        lastDuration = duration.asMillis();
-        final long durationGoalMs = settings.getDurationGoal().asMillis();
-        final long difference = Math.abs( duration.asMillis() - durationGoalMs );
+        lastDuration = duration;
+        final long durationGoalMs = settings.getDurationGoal();
+        final long difference = Math.abs( duration - durationGoalMs );
         final int closeThreshold = ( int ) ( durationGoalMs * .15f );
         final int closeThreshold = ( int ) ( durationGoalMs * .15f );
 
 
         int newTransactionSize;
         int newTransactionSize;
-        if ( duration.isShorterThan( settings.getDurationGoal() ) )
+        if ( duration < ( settings.getDurationGoal() ) )
         {
         {
             if ( difference > closeThreshold )
             if ( difference > closeThreshold )
             {
             {
@@ -78,7 +75,7 @@ public class TransactionSizeCalculator
                 newTransactionSize = transactionSize + 1;
                 newTransactionSize = transactionSize + 1;
             }
             }
         }
         }
-        else if ( duration.isLongerThan( settings.getDurationGoal() ) )
+        else if ( duration > ( settings.getDurationGoal() ) )
         {
         {
             if ( difference > ( 10 * durationGoalMs ) )
             if ( difference > ( 10 * durationGoalMs ) )
             {
             {
@@ -117,7 +114,7 @@ public class TransactionSizeCalculator
     public static class Settings
     public static class Settings
     {
     {
         @Builder.Default
         @Builder.Default
-        private TimeDuration durationGoal = TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS );
+        private long durationGoal = 100;
 
 
         @Builder.Default
         @Builder.Default
         private int maxTransactions = 5003;
         private int maxTransactions = 5003;
@@ -142,12 +139,7 @@ public class TransactionSizeCalculator
                 throw new IllegalArgumentException( "minTransactions must be less than maxTransactions" );
                 throw new IllegalArgumentException( "minTransactions must be less than maxTransactions" );
             }
             }
 
 
-            if ( durationGoal == null )
-            {
-                throw new IllegalArgumentException( "durationGoal must not be null" );
-            }
-
-            if ( durationGoal.asMillis() < 1 )
+            if ( durationGoal < 1 )
             {
             {
                 throw new IllegalArgumentException( "durationGoal must be greater than 0ms" );
                 throw new IllegalArgumentException( "durationGoal must be greater than 0ms" );
             }
             }

+ 46 - 13
lib-util/src/main/java/password/pwm/util/java/AverageTracker.java

@@ -23,21 +23,24 @@ package password.pwm.util.java;
 import java.math.BigDecimal;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.math.BigInteger;
 import java.math.MathContext;
 import java.math.MathContext;
-import java.util.ArrayDeque;
-import java.util.Queue;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 
 public class AverageTracker
 public class AverageTracker
 {
 {
     private final int maxSamples;
     private final int maxSamples;
-    private final Queue<BigInteger> samples = new ArrayDeque<>();
+    private final AtomicLongArray samples;
+    private final AtomicInteger index = new AtomicInteger();
+    private final AtomicInteger top = new AtomicInteger();
 
 
     private final transient ReadWriteLock lock = new ReentrantReadWriteLock();
     private final transient ReadWriteLock lock = new ReentrantReadWriteLock();
 
 
     public AverageTracker( final int maxSamples )
     public AverageTracker( final int maxSamples )
     {
     {
-        this.maxSamples = maxSamples;
+        this.maxSamples = maxSamples - 1;
+        this.samples = new AtomicLongArray( maxSamples );
     }
     }
 
 
     public void addSample( final long input )
     public void addSample( final long input )
@@ -45,11 +48,9 @@ public class AverageTracker
         lock.writeLock().lock();
         lock.writeLock().lock();
         try
         try
         {
         {
-            samples.add( BigInteger.valueOf( input ) );
-            while ( samples.size() > maxSamples )
-            {
-                samples.remove();
-            }
+            samples.set( index.get(), input );
+            index.updateAndGet( current -> current >= maxSamples ? 0 : current + 1 );
+            top.updateAndGet( current -> current >= maxSamples ? maxSamples : current + 1 );
         }
         }
         finally
         finally
         {
         {
@@ -62,23 +63,55 @@ public class AverageTracker
         lock.readLock().lock();
         lock.readLock().lock();
         try
         try
         {
         {
-            if ( samples.isEmpty() )
+            if ( top.get() == 0 )
             {
             {
                 return BigDecimal.ZERO;
                 return BigDecimal.ZERO;
             }
             }
 
 
-            final BigInteger total = samples.stream().reduce( BigInteger::add ).get();
-            final BigDecimal sampleSize = new BigDecimal( samples.size() );
-            return new BigDecimal( total ).divide( sampleSize, MathContext.DECIMAL128 );
+            return primitiveSum();
+        }
+        catch ( final ArithmeticException e )
+        {
+            return bigSum();
         }
         }
         finally
         finally
         {
         {
             lock.readLock().unlock();
             lock.readLock().unlock();
         }
         }
+
     }
     }
 
 
     public long avgAsLong( )
     public long avgAsLong( )
     {
     {
         return avg().longValue();
         return avg().longValue();
     }
     }
+
+    private BigDecimal primitiveSum()
+            throws ArithmeticException
+    {
+        long total = 0;
+        for ( int i = 0; i <= top.get(); i++ )
+        {
+            // math add exact throws exception on overflow
+            total = Math.addExact( total, samples.get( i ) );
+        }
+        return calcAvg( BigDecimal.valueOf( total ) );
+    }
+
+    private BigDecimal bigSum()
+    {
+        BigInteger total = BigInteger.ZERO;
+        for ( int i = 0; i <= top.get(); i++ )
+        {
+            total = total.add( BigInteger.valueOf( samples.get( i ) ) );
+        }
+
+        return calcAvg( new BigDecimal( total ) );
+    }
+
+    private BigDecimal calcAvg( final BigDecimal total )
+    {
+        final BigDecimal sampleSize = new BigDecimal( top.get() + 1 );
+        return total.divide( sampleSize, MathContext.DECIMAL128 );
+    }
 }
 }

+ 89 - 67
lib-util/src/main/java/password/pwm/util/java/CollectionUtil.java

@@ -27,6 +27,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.EnumMap;
 import java.util.EnumMap;
 import java.util.EnumSet;
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
@@ -37,23 +38,28 @@ import java.util.Set;
 import java.util.Spliterator;
 import java.util.Spliterator;
 import java.util.Spliterators;
 import java.util.Spliterators;
 import java.util.function.Function;
 import java.util.function.Function;
-import java.util.stream.Collector;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 import java.util.stream.StreamSupport;
 
 
-public class CollectionUtil
+public final class CollectionUtil
 {
 {
+    private CollectionUtil()
+    {
+    }
+
     public static <T> Stream<T> iteratorToStream( final Iterator<T> iterator )
     public static <T> Stream<T> iteratorToStream( final Iterator<T> iterator )
     {
     {
-        return StreamSupport.stream( Spliterators.spliteratorUnknownSize( iterator, Spliterator.ORDERED ), false );
+        return Optional.ofNullable( iterator )
+                .map( it -> StreamSupport.stream( Spliterators.spliteratorUnknownSize( it, Spliterator.ORDERED ), false ) )
+                .orElse( Stream.empty() );
     }
     }
 
 
     public static <V> List<V> stripNulls( final List<V> input )
     public static <V> List<V> stripNulls( final List<V> input )
     {
     {
         if ( input == null )
         if ( input == null )
         {
         {
-            return Collections.emptyList();
+            return List.of();
         }
         }
 
 
         return input.stream()
         return input.stream()
@@ -65,7 +71,7 @@ public class CollectionUtil
     {
     {
         if ( input == null )
         if ( input == null )
         {
         {
-            return Collections.emptySet();
+            return Set.of();
         }
         }
 
 
         return input.stream()
         return input.stream()
@@ -77,42 +83,45 @@ public class CollectionUtil
     {
     {
         if ( input == null )
         if ( input == null )
         {
         {
-            return Collections.emptyMap();
+            return Map.of();
         }
         }
 
 
-        return input.entrySet().stream()
-                .filter( e -> e.getKey() != null && e.getValue() != null )
-                .collect( collectorToLinkedMap( Map.Entry::getKey, Map.Entry::getValue ) );
+        final Stream<Map.Entry<K, V>> stream = input.entrySet().stream()
+                .filter( CollectionUtil::testMapEntryForNotNull );
+
+        final boolean ordered = input instanceof LinkedHashMap;
+        return ordered
+                ? stream.collect( CollectorUtil.toUnmodifiableLinkedMap( Map.Entry::getKey, Map.Entry::getValue ) )
+                : stream.collect( Collectors.toUnmodifiableMap( Map.Entry::getKey, Map.Entry::getValue ) );
     }
     }
 
 
     public static <K extends Enum<K>, V> EnumMap<K, V> copiedEnumMap( final Map<K, V> source, final Class<K> classOfT )
     public static <K extends Enum<K>, V> EnumMap<K, V> copiedEnumMap( final Map<K, V> source, final Class<K> classOfT )
     {
     {
-        if ( source == null )
+        if ( CollectionUtil.isEmpty( source ) )
         {
         {
-            return new EnumMap<>( classOfT );
+            return new EnumMap<K, V>( classOfT );
         }
         }
 
 
-        final EnumMap<K, V> returnMap = new EnumMap<>( classOfT );
-        for ( final Map.Entry<K, V> entry : source.entrySet() )
-        {
-            final K key = entry.getKey();
-            if ( key != null )
-            {
-                returnMap.put( key, entry.getValue() );
-            }
-        }
-        return returnMap;
+        return source.entrySet().stream()
+                .filter( CollectionUtil::testMapEntryForNotNull )
+                .collect( Collectors.toMap(
+                        Map.Entry::getKey,
+                        Map.Entry::getValue,
+                        CollectorUtil::errorOnDuplicateMergeOperator,
+                        () -> new EnumMap<>( classOfT ) ) );
+
     }
     }
 
 
     public static <E extends Enum<E>> Set<E> readEnumSetFromStringCollection( final Class<E> enumClass, final Collection<String> inputs )
     public static <E extends Enum<E>> Set<E> readEnumSetFromStringCollection( final Class<E> enumClass, final Collection<String> inputs )
     {
     {
-        if ( inputs == null )
+        if ( CollectionUtil.isEmpty( inputs ) )
         {
         {
             return Collections.emptySet();
             return Collections.emptySet();
         }
         }
 
 
         final Set<E> set = inputs.stream()
         final Set<E> set = inputs.stream()
-                .map( input -> JavaHelper.readEnumFromString( enumClass, input ) )
+                .filter( Objects::nonNull )
+                .map( input -> EnumUtil.readEnumFromString( enumClass, input ) )
                 .flatMap( Optional::stream )
                 .flatMap( Optional::stream )
                 .collect( Collectors.toSet() );
                 .collect( Collectors.toSet() );
 
 
@@ -131,10 +140,16 @@ public class CollectionUtil
             final Function<E, String> keyToStringFunction
             final Function<E, String> keyToStringFunction
     )
     )
     {
     {
-        return Collections.unmodifiableMap( inputMap.entrySet().stream()
-                .collect( collectorToLinkedMap(
+        if ( CollectionUtil.isEmpty( inputMap ) )
+        {
+            return Collections.emptyMap();
+        }
+
+        return inputMap.entrySet().stream()
+                .filter( CollectionUtil::testMapEntryForNotNull )
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
                         entry -> keyToStringFunction.apply( entry.getKey() ),
                         entry -> keyToStringFunction.apply( entry.getKey() ),
-                        Map.Entry::getValue ) ) );
+                        Map.Entry::getValue ) );
     }
     }
 
 
     public static <E extends Enum<E>> Map<String, String> enumMapToStringMap( final Map<E, String> inputMap )
     public static <E extends Enum<E>> Map<String, String> enumMapToStringMap( final Map<E, String> inputMap )
@@ -159,13 +174,15 @@ public class CollectionUtil
                 : EnumSet.copyOf( source );
                 : EnumSet.copyOf( source );
     }
     }
 
 
-    public static <E> List<E> iteratorToList( final Iterator<E> iterator )
+    public static <E extends Enum<E>> EnumSet<E> copyToEnumSet( final Set<E> source, final Class<E> classOfT )
     {
     {
-        if ( iterator == null )
-        {
-            return Collections.emptyList();
-        }
+        return isEmpty( source )
+                ? EnumSet.noneOf( classOfT )
+                : EnumSet.copyOf( source );
+    }
 
 
+    public static <E> List<E> iteratorToList( final Iterator<E> iterator )
+    {
         return iteratorToStream( iterator )
         return iteratorToStream( iterator )
                 .collect( Collectors.toUnmodifiableList() );
                 .collect( Collectors.toUnmodifiableList() );
     }
     }
@@ -176,51 +193,56 @@ public class CollectionUtil
      * {@link Collections#unmodifiableMap(Map)}.
      * {@link Collections#unmodifiableMap(Map)}.
      */
      */
     @SuppressFBWarnings( "OCP_OVERLY_CONCRETE_PARAMETER" )
     @SuppressFBWarnings( "OCP_OVERLY_CONCRETE_PARAMETER" )
-    public static <K, V> Map<K, V> combineOrderedMaps( final List<Map<K, V>> maps )
+    public static <K, V> Map<K, V> combineOrderedMaps( final List<Map<K, V>> listOfMaps )
     {
     {
-        final Map<K, V> returnMap = new LinkedHashMap<>();
-        for ( final Map<K, V> loopMap : maps )
+        if ( CollectionUtil.isEmpty( listOfMaps ) )
         {
         {
-            returnMap.putAll( loopMap );
+            return Collections.emptyMap();
         }
         }
-        return Collections.unmodifiableMap( returnMap );
+
+        return listOfMaps.stream()
+                .filter( Objects::nonNull )
+                .flatMap( kvMap -> kvMap.entrySet().stream() )
+                .filter( CollectionUtil::testMapEntryForNotNull )
+                .collect( CollectorUtil.toUnmodifiableLinkedMap( Map.Entry::getKey, Map.Entry::getValue ) );
     }
     }
 
 
-    public static <T, K, U> Collector<T, ?, Map<K, U>> collectorToLinkedMap(
-            final Function<? super T, ? extends K> keyMapper,
-            final Function<? super T, ? extends U> valueMapper
-    )
+    public static <T> Set<T> setUnion( final Set<T> set1, final Set<T> set2 )
     {
     {
-        return Collectors.toMap(
-                keyMapper,
-                valueMapper,
-                ( key1, key2 ) ->
-                {
-                    throw new IllegalStateException( "Duplicate key " + key1 );
-                },
-                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
-    )
+        final Set<T> unionSet = new HashSet<>( set1 == null ? Collections.emptySet() : set1 );
+        unionSet.retainAll( set2 == null ? Collections.<T>emptySet() : set2 );
+        return Set.copyOf( unionSet );
+    }
+
+    public static <T, R> List<R> convertListType( final List<T> input, final Function<T, R> convertFunction )
+
+    {
+        return stripNulls( input ).stream().map( convertFunction ).collect( Collectors.toUnmodifiableList() );
+    }
+
+    private static <K, V> boolean testMapEntryForNotNull( final Map.Entry<K, V> entry )
     {
     {
-        return Collectors.toMap(
-                keyMapper,
-                valueMapper,
-                ( key1, key2 ) ->
-                {
-                    throw new IllegalStateException( "Duplicate key " + key1 );
-                },
-                () -> new EnumMap<>( keyClass )
-        );
+        return entry != null && entry.getKey() != null && entry.getValue() != null;
     }
     }
 
 
-    public static <E extends Enum<E>> Stream<E> enumStream( final Class<E> enumClass )
+    public static <E extends Enum<E>, V> Map<E, V> unmodifiableEnumMap( final Map<E, V> inputSet, final Class<E> classOfT )
     {
     {
-        return EnumSet.allOf( enumClass ).stream();
+        if ( CollectionUtil.isEmpty( inputSet ) )
+        {
+            return Map.of();
+        }
+
+        return Collections.unmodifiableMap( copiedEnumMap( inputSet, classOfT ) );
     }
     }
+
+    public static <E extends Enum<E>> Set<E> unmodifiableEnumSet( final Set<E> inputSet, final Class<E> classOfT )
+    {
+        if ( CollectionUtil.isEmpty( inputSet ) )
+        {
+            return Set.of();
+        }
+
+        return Collections.unmodifiableSet( copyToEnumSet( inputSet, classOfT ) );
+    }
+
 }
 }

+ 142 - 0
lib-util/src/main/java/password/pwm/util/java/CollectorUtil.java

@@ -0,0 +1,142 @@
+/*
+ * 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.util.Collections;
+import java.util.Comparator;
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+public final class CollectorUtil
+{
+    private CollectorUtil()
+    {
+    }
+
+    public static <T, K, U> Collector<T, ?, Map<K, U>> toUnmodifiableLinkedMap(
+            final Function<? super T, ? extends K> keyMapper,
+            final Function<? super T, ? extends U> valueMapper
+    )
+    {
+        final Collector<T, ?, Map<K, U>> wrappedCollector = toLinkedMap( keyMapper, valueMapper );
+        return Collectors.collectingAndThen( wrappedCollector, Collections::unmodifiableMap );
+    }
+
+    public static <T, K, U> Collector<T, ?, Map<K, U>> toLinkedMap(
+            final Function<? super T, ? extends K> keyMapper,
+            final Function<? super T, ? extends U> valueMapper
+    )
+    {
+        return Collectors.toMap(
+                keyMapper,
+                valueMapper,
+                CollectorUtil::errorOnDuplicateMergeOperator,
+                LinkedHashMap::new );
+    }
+
+    public static <T, K, U> Collector<T, ?, SortedMap<K, U>> toUnmodifiableSortedMap(
+            final Function<? super T, ? extends K> keyMapper,
+            final Function<? super T, ? extends U> valueMapper,
+            final Comparator<K> comparator
+    )
+    {
+        final Collector<T, ?, SortedMap<K, U>> wrappedCollector = toSortedMap( keyMapper, valueMapper, comparator );
+        return Collectors.collectingAndThen( wrappedCollector, Collections::unmodifiableSortedMap );
+    }
+
+    public static <T, K, U> Collector<T, ?, SortedMap<K, U>> toSortedMap(
+            final Function<? super T, ? extends K> keyMapper,
+            final Function<? super T, ? extends U> valueMapper,
+            final Comparator<K> comparator
+    )
+    {
+        return Collectors.collectingAndThen( Collectors.toMap(
+                        keyMapper,
+                        valueMapper ),
+                map ->
+                {
+                    final SortedMap<K, U> sortedMap = new TreeMap<>( comparator );
+                    sortedMap.putAll( map );
+                    return sortedMap;
+                } );
+    }
+
+    public static <T, K extends Enum<K>, U> Collector<T, ?, Map<K, U>> toUnmodifiableEnumMap(
+            final Class<K> keyClass,
+            final Function<? super T, ? extends K> keyMapper,
+            final Function<? super T, ? extends U> valueMapper
+    )
+    {
+        final Collector<T, ?, Map<K, U>> wrappedCollector = toEnumMap( keyClass, keyMapper, valueMapper );
+        return Collectors.collectingAndThen( wrappedCollector,
+                s -> CollectionUtil.unmodifiableEnumMap( s, keyClass ) );
+    }
+
+    public static <T, K extends Enum<K>, U> Collector<T, ?, Map<K, U>> toEnumMap(
+            final Class<K> keyClass,
+            final Function<? super T, ? extends K> keyMapper,
+            final Function<? super T, ? extends U> valueMapper
+    )
+    {
+        return Collectors.toMap(
+                keyMapper,
+                valueMapper,
+                CollectorUtil::errorOnDuplicateMergeOperator,
+                () -> new EnumMap<>( keyClass ) );
+    }
+
+    public static <T, K extends Enum<K>, U> Collector<T, ?, Set<K>> toUnmodifiableEnumSet(
+            final Class<K> keyClass,
+            final Function<? super T, ? extends K> keyMapper
+    )
+    {
+        final Collector<T, ?, Set<K>> wrappedCollector = toEnumSet( keyClass, keyMapper );
+        return Collectors.collectingAndThen( wrappedCollector, s -> CollectionUtil.unmodifiableEnumSet( s, keyClass ) );
+    }
+
+    public static <T, K extends Enum<K>, U> Collector<T, ?, Set<K>> toEnumSet(
+            final Class<K> keyClass,
+            final Function<? super T, ? extends K> keyMapper
+    )
+    {
+        final Function<? super T, Boolean> valueMapper = ( Function<T, Boolean> ) t -> Boolean.FALSE;
+
+        final Collector<T, ?, Map<K, Boolean>> wrappedCollector = Collectors.toMap(
+                keyMapper,
+                valueMapper,
+                CollectorUtil::errorOnDuplicateMergeOperator,
+                () -> new EnumMap<>( keyClass ) );
+
+        return Collectors.collectingAndThen( wrappedCollector, ( s ) -> CollectionUtil.copyToEnumSet( s.keySet(), keyClass ) );
+    }
+
+    static <V> V errorOnDuplicateMergeOperator( final V u, final V u2 )
+    {
+        throw new IllegalStateException( "Duplicate key " + u );
+    }
+}

+ 5 - 6
lib-util/src/main/java/password/pwm/util/java/ConditionalTaskExecutor.java

@@ -22,7 +22,6 @@ package password.pwm.util.java;
 
 
 import java.time.Duration;
 import java.time.Duration;
 import java.time.Instant;
 import java.time.Instant;
-import java.time.temporal.ChronoUnit;
 import java.util.Objects;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.Lock;
@@ -37,7 +36,7 @@ import java.util.function.BooleanSupplier;
  * reliance, the conditional is only evaluated during execution of {@code conditionallyExecuteTask()} so the conditional on its own is not
  * reliance, the conditional is only evaluated during execution of {@code conditionallyExecuteTask()} so the conditional on its own is not
  * a strictly reliable indicator of how frequently the task will execute.</p>
  * a strictly reliable indicator of how frequently the task will execute.</p>
  */
  */
-public class ConditionalTaskExecutor
+public final class ConditionalTaskExecutor
 {
 {
     private final Runnable task;
     private final Runnable task;
     private final BooleanSupplier predicate;
     private final BooleanSupplier predicate;
@@ -53,7 +52,6 @@ public class ConditionalTaskExecutor
         {
         {
             if ( predicate.getAsBoolean() )
             if ( predicate.getAsBoolean() )
             {
             {
-
                 task.run();
                 task.run();
             }
             }
         }
         }
@@ -63,7 +61,7 @@ public class ConditionalTaskExecutor
         }
         }
     }
     }
 
 
-    public ConditionalTaskExecutor( final Runnable task, final BooleanSupplier predicate )
+    private ConditionalTaskExecutor( final Runnable task, final BooleanSupplier predicate )
     {
     {
         this.task = Objects.requireNonNull( task );
         this.task = Objects.requireNonNull( task );
         this.predicate = Objects.requireNonNull( predicate );
         this.predicate = Objects.requireNonNull( predicate );
@@ -77,7 +75,8 @@ public class ConditionalTaskExecutor
     public static ConditionalTaskExecutor forPeriodicTask(
     public static ConditionalTaskExecutor forPeriodicTask(
             final Runnable task,
             final Runnable task,
             final Duration timeDuration,
             final Duration timeDuration,
-            final Duration firstExecutionDelay )
+            final Duration firstExecutionDelay
+    )
     {
     {
         return new ConditionalTaskExecutor( task, new TimeDurationPredicate( timeDuration, firstExecutionDelay ) );
         return new ConditionalTaskExecutor( task, new TimeDurationPredicate( timeDuration, firstExecutionDelay ) );
     }
     }
@@ -101,7 +100,7 @@ public class ConditionalTaskExecutor
 
 
         private void setNextTimeFromNow( final Duration duration )
         private void setNextTimeFromNow( final Duration duration )
         {
         {
-            nextExecuteTimestamp.set( Instant.now().plus( duration.toMillis(), ChronoUnit.MILLIS ) );
+            nextExecuteTimestamp.set( Instant.now().plus( duration ) );
         }
         }
 
 
         @Override
         @Override

+ 124 - 0
lib-util/src/main/java/password/pwm/util/java/EnumUtil.java

@@ -0,0 +1,124 @@
+/*
+ * 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.util.Collections;
+import java.util.EnumSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public final class EnumUtil
+{
+    private EnumUtil()
+    {
+    }
+
+    public static <E extends Enum<E>> Optional<E> readEnumFromCaseIgnoreString( final Class<E> enumClass, final String input )
+    {
+        return readEnumFromPredicate( enumClass, loopValue -> loopValue.name().equalsIgnoreCase( input ) );
+    }
+
+    public static <E extends Enum<E>> Optional<E> readEnumFromPredicate( final Class<E> enumClass, final Predicate<E> match )
+    {
+        if ( match == null )
+        {
+            return Optional.empty();
+        }
+
+        if ( enumClass == null || !enumClass.isEnum() )
+        {
+            return Optional.empty();
+        }
+
+        return enumStream( enumClass ).filter( match ).findFirst();
+    }
+
+    public static <E extends Enum<E>> Set<E> readEnumsFromPredicate( final Class<E> enumClass, final Predicate<E> match )
+    {
+        if ( match == null )
+        {
+            return Collections.emptySet();
+        }
+
+        if ( enumClass == null || !enumClass.isEnum() )
+        {
+            return Collections.emptySet();
+        }
+
+        return enumStream( enumClass ).filter( match ).collect( Collectors.toUnmodifiableSet() );
+    }
+
+    public static <E extends Enum<E>> Optional<E> readEnumFromString( final Class<E> enumClass, final String input )
+    {
+        if ( StringUtil.isEmpty( input ) )
+        {
+            return Optional.empty();
+        }
+
+        if ( enumClass == null || !enumClass.isEnum() )
+        {
+            return Optional.empty();
+        }
+
+        try
+        {
+            return Optional.of( Enum.valueOf( enumClass, input ) );
+        }
+        catch ( final IllegalArgumentException e )
+        {
+            /* noop */
+        }
+
+        return Optional.empty();
+    }
+
+    public static <E extends Enum<E>> boolean enumArrayContainsValue( final E[] enumArray, final E enumValue )
+    {
+        if ( enumArray == null || enumArray.length == 0 )
+        {
+            return false;
+        }
+
+        for ( final E loopValue : enumArray )
+        {
+            if ( loopValue == enumValue )
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public static <E extends Enum<E>> Stream<E> enumStream( final Class<E> enumClass )
+    {
+        return EnumSet.allOf( enumClass ).stream();
+    }
+
+    public static <E extends Enum<E>> void forEach( final Class<E> enumClass, final Consumer<E> consumer )
+    {
+        EnumSet.allOf( enumClass ).forEach( consumer );
+    }
+}

+ 60 - 0
lib-util/src/main/java/password/pwm/util/java/FunctionalReentrantLock.java

@@ -0,0 +1,60 @@
+/*
+ * 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.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Supplier;
+
+public class FunctionalReentrantLock
+{
+    private final Lock readLock = new ReentrantLock();
+
+    public FunctionalReentrantLock()
+    {
+    }
+
+    public <T> T exec( final Supplier<T> block )
+    {
+        readLock.lock();
+        try
+        {
+            return block.get();
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+    }
+
+    public void exec( final Runnable block )
+    {
+        readLock.lock();
+        try
+        {
+            block.run();
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+    }
+}

+ 12 - 83
lib-util/src/main/java/password/pwm/util/java/JavaHelper.java

@@ -49,20 +49,18 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Optional;
 import java.util.Properties;
 import java.util.Properties;
-import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.LongAccumulator;
 import java.util.concurrent.atomic.LongAccumulator;
 import java.util.function.Predicate;
 import java.util.function.Predicate;
-import java.util.stream.Collectors;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
 import java.util.zip.GZIPOutputStream;
 
 
-public class JavaHelper
+public final class JavaHelper
 {
 {
     private static final char[] HEX_CHAR_ARRAY = "0123456789ABCDEF".toCharArray();
     private static final char[] HEX_CHAR_ARRAY = "0123456789ABCDEF".toCharArray();
 
 
-    private JavaHelper( )
+    private JavaHelper()
     {
     {
     }
     }
 
 
@@ -79,66 +77,7 @@ public class JavaHelper
 
 
     public static <E extends Enum<E>> E readEnumFromString( final Class<E> enumClass, final E defaultValue, final String input )
     public static <E extends Enum<E>> E readEnumFromString( final Class<E> enumClass, final E defaultValue, final String input )
     {
     {
-        return readEnumFromString( enumClass, input ).orElse( defaultValue );
-    }
-
-    public static <E extends Enum<E>> Optional<E> readEnumFromCaseIgnoreString( final Class<E> enumClass, final String input )
-    {
-        return JavaHelper.readEnumFromPredicate( enumClass, loopValue -> loopValue.name().equalsIgnoreCase( input ) );
-    }
-
-    public static <E extends Enum<E>> Optional<E> readEnumFromPredicate( final Class<E> enumClass, final Predicate<E> match )
-    {
-        if ( match == null )
-        {
-            return Optional.empty();
-        }
-
-        if ( enumClass == null || !enumClass.isEnum() )
-        {
-            return Optional.empty();
-        }
-
-        return CollectionUtil.enumStream( enumClass ).filter( match ).findFirst();
-    }
-
-    public static <E extends Enum<E>> Set<E> readEnumsFromPredicate( final Class<E> enumClass, final Predicate<E> match )
-    {
-        if ( match == null )
-        {
-            return Collections.emptySet();
-        }
-
-        if ( enumClass == null || !enumClass.isEnum() )
-        {
-            return Collections.emptySet();
-        }
-
-        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 )
-    {
-        if ( StringUtil.isEmpty( input ) )
-        {
-            return Optional.empty();
-        }
-
-        if ( enumClass == null || !enumClass.isEnum() )
-        {
-            return Optional.empty();
-        }
-
-        try
-        {
-            return Optional.of( Enum.valueOf( enumClass, input ) );
-        }
-        catch ( final IllegalArgumentException e )
-        {
-            /* noop */
-        }
-
-        return Optional.empty();
+        return EnumUtil.readEnumFromString( enumClass, input ).orElse( defaultValue );
     }
     }
 
 
     public static String throwableToString( final Throwable throwable )
     public static String throwableToString( final Throwable throwable )
@@ -182,24 +121,6 @@ public class JavaHelper
         return errorMsg.toString();
         return errorMsg.toString();
     }
     }
 
 
-    public static <E extends Enum<E>> boolean enumArrayContainsValue( final E[] enumArray, final E enumValue )
-    {
-        if ( enumArray == null || enumArray.length == 0 )
-        {
-            return false;
-        }
-
-        for ( final E loopValue : enumArray )
-        {
-            if ( loopValue == enumValue )
-            {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
     public static long copy( final InputStream input, final OutputStream output )
     public static long copy( final InputStream input, final OutputStream output )
             throws IOException
             throws IOException
     {
     {
@@ -310,6 +231,7 @@ public class JavaHelper
 
 
     /**
     /**
      * Copy of {@link ThreadInfo#toString()} but with the MAX_FRAMES changed from 8 to 256.
      * Copy of {@link ThreadInfo#toString()} but with the MAX_FRAMES changed from 8 to 256.
+     *
      * @param threadInfo thread information
      * @param threadInfo thread information
      * @return a stacktrace string with newline formatting
      * @return a stacktrace string with newline formatting
      */
      */
@@ -520,7 +442,7 @@ public class JavaHelper
     public static byte[] gunzip( final byte[] bytes )
     public static byte[] gunzip( final byte[] bytes )
             throws IOException
             throws IOException
     {
     {
-        try (  GZIPInputStream inputGzipStream = new GZIPInputStream( new ByteArrayInputStream( bytes ) ) )
+        try ( GZIPInputStream inputGzipStream = new GZIPInputStream( new ByteArrayInputStream( bytes ) ) )
         {
         {
             return inputGzipStream.readAllBytes();
             return inputGzipStream.readAllBytes();
         }
         }
@@ -547,6 +469,7 @@ public class JavaHelper
 
 
     /**
     /**
      * Append multiple byte array values into a single array.
      * Append multiple byte array values into a single array.
+     *
      * @param byteArrays two or more byte arrays.
      * @param byteArrays two or more byte arrays.
      * @return A new array with the contents of all byteArrays appended
      * @return A new array with the contents of all byteArrays appended
      */
      */
@@ -605,4 +528,10 @@ public class JavaHelper
         return stackTraceOutput.toString();
         return stackTraceOutput.toString();
 
 
     }
     }
+
+    public static long nextPositiveLong( final long input )
+    {
+        final long next = input + 1;
+        return next > 0 ? next : 0;
+    }
 }
 }

+ 0 - 55
lib-util/src/main/java/password/pwm/util/java/LazySoftReference.java

@@ -1,55 +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.lang.ref.SoftReference;
-import java.util.function.Supplier;
-
-/**
- * A lazy soft reference holder.  This reference will be built lazy and held softly
- * (according to the semantics of {@link SoftReference}).  This class is not thread
- * safe, and the GC may delete the reference at any time, so the {@link Supplier}
- * given to the constructor may be executed multiple times over the lifetime of
- * the reference.
- *
- * @param <E> type of object to hold
- */
-public class LazySoftReference<E>
-{
-    private volatile SoftReference<E> reference = new SoftReference<>( null );
-    private final Supplier<E> supplier;
-
-    public LazySoftReference( final Supplier<E> supplier )
-    {
-        this.supplier = supplier;
-    }
-
-    public E get()
-    {
-        E localValue = reference.get();
-        if ( localValue == null )
-        {
-            localValue = supplier.get();
-            reference = new SoftReference<>( localValue );
-        }
-        return localValue;
-    }
-}

+ 23 - 48
lib-util/src/main/java/password/pwm/util/java/LazySupplier.java

@@ -25,72 +25,47 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.util.function.Supplier;
 import java.util.function.Supplier;
 
 
 /**
 /**
- * Supplier implementation that will cache the value.   Note this implementation
- * is NOT thread safe, it is entirely possible that the underlying {@link Supplier}
- * will be invoked multiple times.
+ * Supplier wrapper implementations.
  *
  *
  * @param <T> the type of object being supplied.
  * @param <T> the type of object being supplied.
  */
  */
-public class LazySupplier<T> implements Supplier<T>
+public interface LazySupplier<T> extends Supplier<T>
 {
 {
-    private boolean supplied = false;
-    private T value;
-    private final Supplier<T> realSupplier;
+    boolean isSupplied();
 
 
-    public LazySupplier( final Supplier<T> realSupplier )
-    {
-        this.realSupplier = realSupplier;
-    }
+    void clear() throws UnsupportedOperationException;
 
 
-    @Override
-    public T get()
+    @SuppressFBWarnings( "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" )
+    interface CheckedSupplier<T, E extends Exception>
     {
     {
-        if ( !supplied )
-        {
-            value = realSupplier.get();
-            supplied = true;
-        }
-        return value;
+        T call() throws E;
     }
     }
 
 
-    public boolean isSupplied()
+    /**
+     * Synchronized wrapper for any other {@code LazySupplier} implementation that
+     * guarantee thread safety.  In particular, the backing realSupplier will only ever be called
+     * a single time unless {@code #clear} is invoked.
+     * @param realSupplier another {@code LazySupplier} instance
+     * @param <T> return type.
+     * @return a {@code LazyWrapper} thread safe synchronization.
+     */
+    static <T> LazySupplier<T> synchronizedSupplier( final LazySupplier<T> realSupplier )
     {
     {
-        return supplied;
+        return new LazySupplierImpl.LockingSupplier<>( realSupplier );
     }
     }
 
 
-    @SuppressFBWarnings( "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" )
-    public interface CheckedSupplier<T, E extends Exception>
+    static <T> LazySupplier<T> create( final Supplier<T> realSupplier )
     {
     {
-        T call() throws E;
+        return new LazySupplierImpl.StandardLazySupplier<T>( realSupplier );
     }
     }
 
 
-    public static <T, E extends Exception> LazyCheckedSupplier<T, E> checked( final CheckedSupplier<T, E> lazySupplier )
+    static <T> LazySupplier<T> soft( final Supplier<T> realSupplier )
     {
     {
-        return new LazyCheckedSupplier<>( lazySupplier );
+        return new LazySupplierImpl.SoftLazySupplier<T>( realSupplier );
     }
     }
 
 
-    @SuppressFBWarnings( "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" )
-    private static class LazyCheckedSupplier<T, E extends Exception> implements CheckedSupplier<T, E>
+    static <T, E extends Exception> CheckedSupplier<T, E> checked( final CheckedSupplier<T, E> lazySupplier )
     {
     {
-        private boolean supplied = false;
-        private T value;
-        private final CheckedSupplier<T, E> realCallable;
-
-        private LazyCheckedSupplier( final CheckedSupplier<T, E> realSupplier )
-        {
-            this.realCallable = realSupplier;
-        }
-
-        @Override
-        @SuppressFBWarnings( "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" )
-        public T call() throws E
-        {
-            if ( !supplied )
-            {
-                value = realCallable.call();
-                supplied = true;
-            }
-            return value;
-        }
+        return new LazySupplierImpl.LazyCheckedSupplier<>( lazySupplier );
     }
     }
 }
 }

+ 176 - 0
lib-util/src/main/java/password/pwm/util/java/LazySupplierImpl.java

@@ -0,0 +1,176 @@
+/*
+ * 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+import java.lang.ref.SoftReference;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+class LazySupplierImpl
+{
+    static class StandardLazySupplier<T> implements LazySupplier<T>
+    {
+        private boolean supplied = false;
+        private T value;
+        private final Supplier<T> realSupplier;
+
+        StandardLazySupplier( final Supplier<T> realSupplier )
+        {
+            this.realSupplier = realSupplier;
+        }
+
+        @Override
+        public T get()
+        {
+            if ( !supplied )
+            {
+                value = realSupplier.get();
+                supplied = true;
+            }
+            return value;
+        }
+
+        public boolean isSupplied()
+        {
+            return supplied;
+        }
+
+        @Override
+        public void clear()
+                throws UnsupportedOperationException
+        {
+            supplied = false;
+            value = null;
+        }
+    }
+
+    static class SoftLazySupplier<T> implements LazySupplier<T>
+    {
+        private static final Object TOMBSTONE = new Object();
+
+        private SoftReference<?> reference;
+        private final Supplier<T> realSupplier;
+
+        SoftLazySupplier( final Supplier<T> realSupplier )
+        {
+            this.realSupplier = Objects.requireNonNull( realSupplier );
+        }
+
+        @Override
+        public T get()
+        {
+            if ( reference != null )
+            {
+                final Object referencedValue = reference.get();
+                if ( referencedValue != null )
+                {
+                    return referencedValue == TOMBSTONE ? null : ( T ) referencedValue;
+                }
+            }
+
+            final T realValue = realSupplier.get();
+            reference = new SoftReference<>( realValue == null ? TOMBSTONE : realValue );
+            return realValue;
+        }
+
+        public boolean isSupplied()
+        {
+            return reference.get() != null;
+        }
+
+        public void clear()
+        {
+            reference = null;
+        }
+    }
+
+    static class LockingSupplier<T> implements LazySupplier<T>
+    {
+        private final Supplier<T> realSupplier;
+        private volatile T value;
+        private volatile boolean supplied = false;
+
+        private final FunctionalReentrantLock lock = new FunctionalReentrantLock();
+
+        LockingSupplier( final Supplier<T> realSupplier )
+        {
+            this.realSupplier = Objects.requireNonNull( realSupplier );
+        }
+
+        @Override
+        public T get()
+        {
+            return lock.exec( () ->
+            {
+                if ( !supplied )
+                {
+                    value = realSupplier.get();
+                    supplied = true;
+                }
+
+                return value;
+            } );
+        }
+
+        @Override
+        public boolean isSupplied()
+        {
+            return lock.exec( () -> supplied );
+        }
+
+        @Override
+        public void clear()
+        {
+            lock.exec( () ->
+            {
+                supplied = false;
+                value = null;
+            } );
+        }
+    }
+
+    @SuppressFBWarnings( "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" )
+    static class LazyCheckedSupplier<T, E extends Exception> implements LazySupplier.CheckedSupplier<T, E>
+    {
+        private boolean supplied = false;
+        private T value;
+        private final LazySupplier.CheckedSupplier<T, E> realSupplier;
+
+        LazyCheckedSupplier( final LazySupplier.CheckedSupplier<T, E> realSupplier )
+        {
+            this.realSupplier = Objects.requireNonNull( realSupplier );
+        }
+
+        @Override
+        @SuppressFBWarnings( "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" )
+        public T call() throws E
+        {
+            if ( !supplied )
+            {
+                value = realSupplier.call();
+                supplied = true;
+            }
+            return value;
+        }
+    }
+}

+ 12 - 21
server/src/main/java/password/pwm/svc/wordlist/SeedlistService.java → lib-util/src/main/java/password/pwm/util/java/MutableReference.java

@@ -18,34 +18,25 @@
  * limitations under the License.
  * limitations under the License.
  */
  */
 
 
-package password.pwm.svc.wordlist;
+package password.pwm.util.java;
 
 
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.logging.PwmLogger;
 
 
-public class SeedlistService extends AbstractWordlist implements Wordlist
+/**
+ * Simple mutable reference useful for some scenarios with lambdas that require only final parameters.
+ *
+ * @param <T> the reference type.
+ */
+public class MutableReference<T>
 {
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( SeedlistService.class );
-
-    public SeedlistService()
-    {
-    }
-
-    @Override
-    protected WordlistType getWordlistType()
-    {
-        return WordlistType.SEEDLIST;
-    }
+    private T reference;
 
 
-    @Override
-    protected PwmLogger getLogger()
+    public T get()
     {
     {
-        return LOGGER;
+        return reference;
     }
     }
 
 
-    @Override
-    public String randomSeed() throws PwmUnrecoverableException
+    public void set( final T reference )
     {
     {
-        return super.randomSeed();
+        this.reference = reference;
     }
     }
 }
 }

+ 12 - 0
lib-util/src/main/java/password/pwm/util/java/PwmNumberFormat.java

@@ -20,6 +20,10 @@
 
 
 package password.pwm.util.java;
 package password.pwm.util.java;
 
 
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
 import java.text.NumberFormat;
 import java.text.NumberFormat;
 import java.util.Locale;
 import java.util.Locale;
 
 
@@ -42,4 +46,12 @@ public class PwmNumberFormat
         final NumberFormat numberFormat = NumberFormat.getInstance( locale );
         final NumberFormat numberFormat = NumberFormat.getInstance( locale );
         return numberFormat.format( number );
         return numberFormat.format( number );
     }
     }
+
+    public static String prettyBigDecimal( final BigDecimal bigDecimal, final int significantBits, final Locale locale )
+    {
+        final MathContext mathContext = new MathContext( significantBits, RoundingMode.HALF_EVEN );
+        final BigDecimal rounded = bigDecimal.round( mathContext );
+        final NumberFormat decimalFormat = DecimalFormat.getInstance( locale );
+        return decimalFormat.format( rounded );
+    }
 }
 }

+ 8 - 10
lib-util/src/main/java/password/pwm/util/java/StatisticAverageBundle.java

@@ -20,9 +20,9 @@
 
 
 package password.pwm.util.java;
 package password.pwm.util.java;
 
 
+import password.pwm.util.MovingAverage;
+
 import java.time.Duration;
 import java.time.Duration;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.EnumMap;
 import java.util.EnumMap;
 import java.util.Map;
 import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
@@ -37,9 +37,8 @@ public class StatisticAverageBundle<K extends Enum<K>>
     public StatisticAverageBundle( final Class<K> keyType, final Duration avgPeriodLength )
     public StatisticAverageBundle( final Class<K> keyType, final Duration avgPeriodLength )
     {
     {
         this.keyType = keyType;
         this.keyType = keyType;
-        statMap = new EnumMap<>( keyType );
-        Arrays.stream( keyType.getEnumConstants() )
-                .forEach( k -> statMap.put( k, new MovingAverage( avgPeriodLength ) ) );
+        this.statMap = new EnumMap<>( keyType );
+        EnumUtil.forEach( keyType, k -> statMap.put( k, new MovingAverage( avgPeriodLength ) ) );
     }
     }
 
 
     public StatisticAverageBundle( final Class<K> keyType )
     public StatisticAverageBundle( final Class<K> keyType )
@@ -70,11 +69,10 @@ public class StatisticAverageBundle<K extends Enum<K>>
 
 
     public Map<String, String> debugStats()
     public Map<String, String> debugStats()
     {
     {
-        return Collections.unmodifiableMap( Arrays.stream( keyType.getEnumConstants() )
-                .collect( Collectors.toMap(
-                        Enum::name,
-                        this::getFormattedAverage
-                ) ) );
+        return statMap.entrySet().stream()
+                .collect( Collectors.toUnmodifiableMap(
+                        entry -> entry.getKey().name(),
+                        entry -> entry.getValue().getFormattedAverage() ) );
     }
     }
 
 
     public String debugString()
     public String debugString()

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

@@ -20,10 +20,10 @@
 
 
 package password.pwm.util.java;
 package password.pwm.util.java;
 
 
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.EnumMap;
 import java.util.EnumMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.atomic.LongAccumulator;
 import java.util.concurrent.atomic.LongAccumulator;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
@@ -34,9 +34,9 @@ public class StatisticCounterBundle<K extends Enum<K>>
 
 
     public StatisticCounterBundle( final Class<K> keyType )
     public StatisticCounterBundle( final Class<K> keyType )
     {
     {
-        this.keyType = keyType;
-        statMap = new EnumMap<>( keyType );
-        Arrays.stream( keyType.getEnumConstants() ).forEach( k -> statMap.put( k, JavaHelper.newAbsLongAccumulator() ) );
+        this.keyType = Objects.requireNonNull( keyType );
+        this.statMap = new EnumMap<>( keyType );
+        EnumUtil.forEach( keyType, k -> statMap.put( k, JavaHelper.newAbsLongAccumulator() ) );
     }
     }
 
 
     public void increment( final K stat )
     public void increment( final K stat )
@@ -55,12 +55,12 @@ public class StatisticCounterBundle<K extends Enum<K>>
         return longAdder == null ? 0 : longAdder.longValue();
         return longAdder == null ? 0 : longAdder.longValue();
     }
     }
 
 
-    public Map<String, String> debugStats()
+    public Map<String, String> debugStats( final Locale locale )
     {
     {
-        return Collections.unmodifiableMap( Arrays.stream( keyType.getEnumConstants() )
-                .collect( Collectors.toMap(
-                        Enum::name,
-                        stat -> Long.toString( get( stat ) )
-                ) ) );
+        final PwmNumberFormat pwmNumberFormat = PwmNumberFormat.forLocale( locale );
+        return statMap.entrySet().stream()
+                .collect( Collectors.toUnmodifiableMap(
+                        entry -> entry.getKey().name(),
+                        entry -> pwmNumberFormat.format( entry.getValue().longValue() ) ) );
     }
     }
 }
 }

+ 83 - 0
lib-util/src/main/java/password/pwm/util/java/StatisticRateBundle.java

@@ -0,0 +1,83 @@
+/*
+ * 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 password.pwm.util.EventRateMeter;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.util.EnumMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class StatisticRateBundle<K extends Enum<K>>
+{
+    private static final Duration DEFAULT_DURATION = Duration.ofMinutes( 1 );
+
+    private final Class<K> keyType;
+    private final Map<K, EventRateMeter> statMap;
+
+    public StatisticRateBundle( final Class<K> keyType, final Duration avgPeriodLength )
+    {
+        this.keyType = keyType;
+        statMap = new EnumMap<>( keyType );
+        EnumUtil.forEach( keyType, k -> statMap.put( k, new EventRateMeter( avgPeriodLength ) ) );
+    }
+
+    public StatisticRateBundle( final Class<K> keyType )
+    {
+        this( keyType, DEFAULT_DURATION );
+    }
+
+    public void markEvent( final K stat )
+    {
+        statMap.get( stat ).markEvent();
+    }
+
+    public void markEvents( final K stat, final int count )
+    {
+        statMap.get( stat ).markEvents( count );
+    }
+
+    public BigDecimal rawEps( final K stat )
+    {
+        final EventRateMeter movingAverage = statMap.get( stat );
+        return movingAverage.rawEps();
+    }
+
+    public String prettyEps( final K stat, final Locale locale )
+    {
+        return statMap.get( stat ).prettyEps( locale );
+    }
+
+    public Map<String, String> debugStats( final Locale locale )
+    {
+        return statMap.entrySet().stream().collect( Collectors.toUnmodifiableMap(
+                entry -> entry.getKey().name(),
+                entry -> entry.getValue().prettyEps( locale ) ) );
+    }
+
+    public String debugString( final Locale locale )
+    {
+        return StringUtil.mapToString( debugStats( locale ) );
+    }
+}

+ 31 - 24
lib-util/src/main/java/password/pwm/util/java/StringUtil.java

@@ -31,6 +31,7 @@ import java.net.URLEncoder;
 import java.nio.charset.Charset;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.charset.StandardCharsets;
 import java.text.NumberFormat;
 import java.text.NumberFormat;
+import java.time.Duration;
 import java.time.Instant;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.ArrayList;
@@ -51,8 +52,12 @@ import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
-public abstract class StringUtil
+public final class StringUtil
 {
 {
+    private StringUtil()
+    {   
+    }
+
     private static final Charset STRING_UTIL_CHARSET = StandardCharsets.UTF_8;
     private static final Charset STRING_UTIL_CHARSET = StandardCharsets.UTF_8;
 
 
     private static final Base64.Decoder B64_MIME_DECODER = Base64.getMimeDecoder();
     private static final Base64.Decoder B64_MIME_DECODER = Base64.getMimeDecoder();
@@ -247,6 +252,11 @@ public abstract class StringUtil
         return instant == null ? "" : instant.truncatedTo( ChronoUnit.SECONDS ).toString();
         return instant == null ? "" : instant.truncatedTo( ChronoUnit.SECONDS ).toString();
     }
     }
 
 
+    public static String toIsoDuration( final Duration duration )
+    {
+        return duration == null ? "" : duration.truncatedTo( ChronoUnit.SECONDS ).toString();
+    }
+
     public enum Base64Options
     public enum Base64Options
     {
     {
         GZIP,
         GZIP,
@@ -319,7 +329,7 @@ public abstract class StringUtil
         }
         }
 
 
         final byte[] decodedBytes;
         final byte[] decodedBytes;
-        if ( JavaHelper.enumArrayContainsValue( options, Base64Options.URL_SAFE ) )
+        if ( EnumUtil.enumArrayContainsValue( options, Base64Options.URL_SAFE ) )
         {
         {
             decodedBytes = B64_URL_DECODER.decode( input.toString() );
             decodedBytes = B64_URL_DECODER.decode( input.toString() );
         }
         }
@@ -328,7 +338,7 @@ public abstract class StringUtil
             decodedBytes = B64_MIME_DECODER.decode( input.toString() );
             decodedBytes = B64_MIME_DECODER.decode( input.toString() );
         }
         }
 
 
-        if ( JavaHelper.enumArrayContainsValue( options, Base64Options.GZIP ) )
+        if ( EnumUtil.enumArrayContainsValue( options, Base64Options.GZIP ) )
         {
         {
             return JavaHelper.gunzip( decodedBytes );
             return JavaHelper.gunzip( decodedBytes );
         }
         }
@@ -341,7 +351,7 @@ public abstract class StringUtil
     public static String base64Encode( final byte[] input, final StringUtil.Base64Options... options )
     public static String base64Encode( final byte[] input, final StringUtil.Base64Options... options )
     {
     {
         final byte[] compressedBytes;
         final byte[] compressedBytes;
-        if ( JavaHelper.enumArrayContainsValue( options, Base64Options.GZIP ) )
+        if ( EnumUtil.enumArrayContainsValue( options, Base64Options.GZIP ) )
         {
         {
             try
             try
             {
             {
@@ -357,7 +367,7 @@ public abstract class StringUtil
             compressedBytes = input;
             compressedBytes = input;
         }
         }
 
 
-        if ( JavaHelper.enumArrayContainsValue( options, Base64Options.URL_SAFE ) )
+        if ( EnumUtil.enumArrayContainsValue( options, Base64Options.URL_SAFE ) )
         {
         {
             return B64_URL_ENCODER.encodeToString( compressedBytes );
             return B64_URL_ENCODER.encodeToString( compressedBytes );
         }
         }
@@ -389,20 +399,13 @@ public abstract class StringUtil
             return input;
             return input;
         }
         }
 
 
-        final StringBuilder sb = new StringBuilder( input );
-        while ( sb.length() < length )
-        {
-            if ( right )
-            {
-                sb.append( appendChar );
-            }
-            else
-            {
-                sb.insert( 0, appendChar );
-            }
-        }
+        final char[] charArray = new char[ length - input.length() ];
+        Arrays.fill( charArray, appendChar );
+        final String paddingString = new String( charArray );
 
 
-        return sb.toString();
+        return right
+                ? input + paddingString
+                : paddingString + input;
     }
     }
 
 
     public static List<String> splitAndTrim( final String input, final String separator )
     public static List<String> splitAndTrim( final String input, final String separator )
@@ -768,24 +771,28 @@ public abstract class StringUtil
      */
      */
     public static boolean convertStrToBoolean( final String string )
     public static boolean convertStrToBoolean( final String string )
     {
     {
-        return !( string == null || string.length() < 1 ) && ( "true".equalsIgnoreCase( string )
+        if ( StringUtil.isEmpty( string ) )
+        {
+            return false;
+        }
+
+        return "true".equalsIgnoreCase( string )
                 || "1".equalsIgnoreCase( string )
                 || "1".equalsIgnoreCase( string )
                 || "yes".equalsIgnoreCase( string )
                 || "yes".equalsIgnoreCase( string )
-                || "y".equalsIgnoreCase( string )
-        );
+                || "y".equalsIgnoreCase( string );
     }
     }
 
 
     public static List<String> tokenizeString(
     public static List<String> tokenizeString(
             final String inputString,
             final String inputString,
-            final String seperator
+            final String separator
     )
     )
     {
     {
-        if ( inputString == null || inputString.length() < 1 )
+        if ( StringUtil.isEmpty( inputString ) )
         {
         {
             return Collections.emptyList();
             return Collections.emptyList();
         }
         }
 
 
-        final List<String> values = new ArrayList<>( Arrays.asList( inputString.split( seperator ) ) );
+        final List<String> values = new ArrayList<>( Arrays.asList( inputString.split( separator ) ) );
         return Collections.unmodifiableList( values );
         return Collections.unmodifiableList( values );
     }
     }
 }
 }

+ 5 - 5
lib-util/src/test/java/password/pwm/util/java/AtomicLoopIntIncrementerTest.java

@@ -20,8 +20,8 @@
 
 
 package password.pwm.util.java;
 package password.pwm.util.java;
 
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
 
 
 public class AtomicLoopIntIncrementerTest
 public class AtomicLoopIntIncrementerTest
 {
 {
@@ -33,16 +33,16 @@ public class AtomicLoopIntIncrementerTest
         for ( int i = 0; i < 5; i++ )
         for ( int i = 0; i < 5; i++ )
         {
         {
             final int next = atomicLoopIntIncrementer.next();
             final int next = atomicLoopIntIncrementer.next();
-            Assert.assertEquals( i, next );
+            Assertions.assertEquals( i, next );
         }
         }
 
 
-        Assert.assertEquals( 0,  atomicLoopIntIncrementer.next() );
+        Assertions.assertEquals( 0,  atomicLoopIntIncrementer.next() );
 
 
         for ( int i = 0; i < 5; i++ )
         for ( int i = 0; i < 5; i++ )
         {
         {
             atomicLoopIntIncrementer.next();
             atomicLoopIntIncrementer.next();
         }
         }
 
 
-        Assert.assertEquals( 1,  atomicLoopIntIncrementer.next() );
+        Assertions.assertEquals( 1,  atomicLoopIntIncrementer.next() );
     }
     }
 }
 }

+ 5 - 5
lib-util/src/test/java/password/pwm/util/java/AverageTrackerTest.java

@@ -20,8 +20,8 @@
 
 
 package password.pwm.util.java;
 package password.pwm.util.java;
 
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
 
 
 public class AverageTrackerTest
 public class AverageTrackerTest
 {
 {
@@ -34,7 +34,7 @@ public class AverageTrackerTest
         averageTracker.addSample( 7 );
         averageTracker.addSample( 7 );
         averageTracker.addSample( 8 );
         averageTracker.addSample( 8 );
         averageTracker.addSample( 9 );
         averageTracker.addSample( 9 );
-        Assert.assertEquals( 7, averageTracker.avgAsLong() );
+        Assertions.assertEquals( 7, averageTracker.avgAsLong() );
     }
     }
 
 
     @Test
     @Test
@@ -48,7 +48,7 @@ public class AverageTrackerTest
         averageTracker.addSample( 9 );
         averageTracker.addSample( 9 );
         averageTracker.addSample( 10 );
         averageTracker.addSample( 10 );
         averageTracker.addSample( 15 );
         averageTracker.addSample( 15 );
-        Assert.assertEquals( 9, averageTracker.avgAsLong() );
+        Assertions.assertEquals( 9, averageTracker.avgAsLong() );
     }
     }
 
 
     @Test
     @Test
@@ -60,6 +60,6 @@ public class AverageTrackerTest
         averageTracker.addSample( 9_223_372_036_854_775_805L  );
         averageTracker.addSample( 9_223_372_036_854_775_805L  );
         averageTracker.addSample( 9_223_372_036_854_775_804L  );
         averageTracker.addSample( 9_223_372_036_854_775_804L  );
         averageTracker.addSample( 9_223_372_036_854_775_803L  );
         averageTracker.addSample( 9_223_372_036_854_775_803L  );
-        Assert.assertEquals( 9_223_372_036_854_775_805L, averageTracker.avgAsLong() );
+        Assertions.assertEquals( 9_223_372_036_854_775_805L, averageTracker.avgAsLong() );
     }
     }
 }
 }

+ 0 - 58
lib-util/src/test/java/password/pwm/util/java/CollectionUtilTest.java

@@ -1,58 +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 org.junit.Assert;
-import org.junit.Test;
-
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-public class CollectionUtilTest
-{
-
-    @Test
-    public void collectorToLinkedMap()
-    {
-        final Map<String, String> map = new LinkedHashMap<>();
-        map.put( "1", "1" );
-        map.put( "2", "2" );
-        map.put( "3", "3" );
-        map.put( "4", "4" );
-        map.put( "5", "5" );
-
-        final Map<String, String> outputMap = map.entrySet().stream().collect( CollectionUtil.collectorToLinkedMap(
-                Map.Entry::getKey,
-                Map.Entry::getValue
-        ) );
-
-        final Iterator<String> iter = outputMap.values().iterator();
-        Assert.assertEquals( "1", iter.next() );
-        Assert.assertEquals( "2", iter.next() );
-        Assert.assertEquals( "3", iter.next() );
-        Assert.assertEquals( "4", iter.next() );
-        Assert.assertEquals( "5", iter.next() );
-
-        Assert.assertEquals( "java.util.LinkedHashMap", outputMap.getClass().getName() );
-    }
-}

+ 127 - 0
lib-util/src/test/java/password/pwm/util/java/CollectorUtilTest.java

@@ -0,0 +1,127 @@
+/*
+ * 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.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.function.Function;
+
+public class CollectorUtilTest
+{
+    @Test
+    public void collectorToLinkedMap()
+    {
+        final Map<String, String> map = new LinkedHashMap<>();
+        map.put( "1", "1" );
+        map.put( "2", "2" );
+        map.put( "3", "3" );
+        map.put( "4", "4" );
+        map.put( "5", "5" );
+
+        final Map<String, String> outputMap = map.entrySet().stream()
+                .collect( CollectorUtil.toLinkedMap(
+                        Map.Entry::getKey,
+                        Map.Entry::getValue ) );
+
+        final Iterator<String> iter = outputMap.values().iterator();
+        Assertions.assertEquals( "1", iter.next() );
+        Assertions.assertEquals( "2", iter.next() );
+        Assertions.assertEquals( "3", iter.next() );
+        Assertions.assertEquals( "4", iter.next() );
+        Assertions.assertEquals( "5", iter.next() );
+
+        Assertions.assertEquals( "java.util.LinkedHashMap", outputMap.getClass().getName() );
+
+        outputMap.put( "testKey", "testValue" );
+    }
+
+    @Test
+    public void collectorToUnmodifiableLinkedMap()
+    {
+        final Map<String, String> map = new LinkedHashMap<>();
+        map.put( "1", "1" );
+        map.put( "2", "2" );
+        map.put( "3", "3" );
+        map.put( "4", "4" );
+        map.put( "5", "5" );
+
+        final Map<String, String> outputMap = map.entrySet().stream()
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
+                        Map.Entry::getKey,
+                        Map.Entry::getValue ) );
+
+        final Iterator<String> iter = outputMap.values().iterator();
+        Assertions.assertEquals( "1", iter.next() );
+        Assertions.assertEquals( "2", iter.next() );
+        Assertions.assertEquals( "3", iter.next() );
+        Assertions.assertEquals( "4", iter.next() );
+        Assertions.assertEquals( "5", iter.next() );
+
+        Assertions.assertThrows( UnsupportedOperationException.class, () -> outputMap.put( "testKey", "testValue" ) );
+    }
+
+    private enum TestEnum
+    {
+        ONE,
+        TWO,
+        THREE,
+        FOUR,
+        FIVE,
+        SIX,
+    }
+
+    @Test
+    public void collectorToUnmodifiableEnumSet()
+    {
+        final Set<TestEnum> testSet = EnumUtil.enumStream( TestEnum.class )
+                .collect( CollectorUtil.toUnmodifiableEnumSet( TestEnum.class, Function.identity() ) );
+
+        Assertions.assertEquals( 6, testSet.size() );
+        Assertions.assertEquals( TestEnum.ONE, testSet.iterator().next() );
+        Assertions.assertThrows( UnsupportedOperationException.class, () -> testSet.remove( TestEnum.ONE ) );
+    }
+
+    @Test
+    public void collectorToSortedMap()
+    {
+        final Map<String, String> input = Map.ofEntries(
+                Map.entry( "2", "two" ),
+                Map.entry( "1", "one" ),
+                Map.entry( "3", "three" ) );
+
+        final SortedMap<String, String> sortedMap = input.entrySet().stream()
+                .collect( CollectorUtil.toSortedMap(
+                        Map.Entry::getKey,
+                        Map.Entry::getValue,
+                        String.CASE_INSENSITIVE_ORDER
+                ) );
+
+        Assertions.assertEquals( 3, sortedMap.size() );
+        Assertions.assertEquals( "1", sortedMap.firstKey() );
+    }
+}

+ 35 - 35
lib-util/src/test/java/password/pwm/util/java/CopyingInputStreamTest.java

@@ -21,9 +21,9 @@
 package password.pwm.util.java;
 package password.pwm.util.java;
 
 
 
 
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.ByteArrayOutputStream;
@@ -64,7 +64,7 @@ public class CopyingInputStreamTest
 
 
 
 
 
 
-    @Before
+    @BeforeEach
     public void setUp() throws Exception
     public void setUp() throws Exception
     {
     {
         final InputStream input = new ByteArrayInputStream( "abc".getBytes( ASCII ) );
         final InputStream input = new ByteArrayInputStream( "abc".getBytes( ASCII ) );
@@ -75,70 +75,70 @@ public class CopyingInputStreamTest
     @Test
     @Test
     public void testReadNothing() throws Exception
     public void testReadNothing() throws Exception
     {
     {
-        Assert.assertEquals( "", new String( output.bytes(), ASCII ) );
+        Assertions.assertEquals( "", new String( output.bytes(), ASCII ) );
     }
     }
 
 
     @Test
     @Test
     public void testReadOneByte() throws Exception
     public void testReadOneByte() throws Exception
     {
     {
-        Assert.assertEquals( 'a', copyingStream.read() );
-        Assert.assertEquals( "a", new String( output.bytes(), ASCII ) );
+        Assertions.assertEquals( 'a', copyingStream.read() );
+        Assertions.assertEquals( "a", new String( output.bytes(), ASCII ) );
     }
     }
 
 
     @Test
     @Test
     public void testReadEverything() throws Exception
     public void testReadEverything() throws Exception
     {
     {
-        Assert.assertEquals( 'a', copyingStream.read() );
-        Assert.assertEquals( 'b', copyingStream.read() );
-        Assert.assertEquals( 'c', copyingStream.read() );
-        Assert.assertEquals( -1, copyingStream.read() );
-        Assert.assertEquals( "abc", new String( output.bytes(), ASCII ) );
+        Assertions.assertEquals( 'a', copyingStream.read() );
+        Assertions.assertEquals( 'b', copyingStream.read() );
+        Assertions.assertEquals( 'c', copyingStream.read() );
+        Assertions.assertEquals( -1, copyingStream.read() );
+        Assertions.assertEquals( "abc", new String( output.bytes(), ASCII ) );
     }
     }
 
 
     @Test
     @Test
     public void testReadToArray() throws Exception
     public void testReadToArray() throws Exception
     {
     {
         final byte[] buffer = new byte[8];
         final byte[] buffer = new byte[8];
-        Assert.assertEquals( 3, copyingStream.read( buffer ) );
-        Assert.assertEquals( 'a', buffer[0] );
-        Assert.assertEquals( 'b', buffer[1] );
-        Assert.assertEquals( 'c', buffer[2] );
-        Assert.assertEquals( -1, copyingStream.read( buffer ) );
-        Assert.assertEquals( "abc", new String( output.bytes(), ASCII ) );
+        Assertions.assertEquals( 3, copyingStream.read( buffer ) );
+        Assertions.assertEquals( 'a', buffer[0] );
+        Assertions.assertEquals( 'b', buffer[1] );
+        Assertions.assertEquals( 'c', buffer[2] );
+        Assertions.assertEquals( -1, copyingStream.read( buffer ) );
+        Assertions.assertEquals( "abc", new String( output.bytes(), ASCII ) );
     }
     }
 
 
     @Test
     @Test
     public void testReadToArrayWithOffset() throws Exception
     public void testReadToArrayWithOffset() throws Exception
     {
     {
         final byte[] buffer = new byte[8];
         final byte[] buffer = new byte[8];
-        Assert.assertEquals( 3, copyingStream.read( buffer, 4, 4 ) );
-        Assert.assertEquals( 'a', buffer[4] );
-        Assert.assertEquals( 'b', buffer[5] );
-        Assert.assertEquals( 'c', buffer[6] );
-        Assert.assertEquals( -1, copyingStream.read( buffer, 4, 4 ) );
-        Assert.assertEquals( "abc", new String( output.bytes(), ASCII ) );
+        Assertions.assertEquals( 3, copyingStream.read( buffer, 4, 4 ) );
+        Assertions.assertEquals( 'a', buffer[4] );
+        Assertions.assertEquals( 'b', buffer[5] );
+        Assertions.assertEquals( 'c', buffer[6] );
+        Assertions.assertEquals( -1, copyingStream.read( buffer, 4, 4 ) );
+        Assertions.assertEquals( "abc", new String( output.bytes(), ASCII ) );
     }
     }
 
 
     @Test
     @Test
     public void testSkip() throws Exception
     public void testSkip() throws Exception
     {
     {
-        Assert.assertEquals( 'a', copyingStream.read() );
-        Assert.assertEquals( 1, copyingStream.skip( 1 ) );
-        Assert.assertEquals( 'c', copyingStream.read() );
-        Assert.assertEquals( -1, copyingStream.read() );
-        Assert.assertEquals( "ac", new String( output.bytes(), ASCII ) );
+        Assertions.assertEquals( 'a', copyingStream.read() );
+        Assertions.assertEquals( 1, copyingStream.skip( 1 ) );
+        Assertions.assertEquals( 'c', copyingStream.read() );
+        Assertions.assertEquals( -1, copyingStream.read() );
+        Assertions.assertEquals( "ac", new String( output.bytes(), ASCII ) );
     }
     }
 
 
     @Test
     @Test
     public void testMarkReset() throws Exception
     public void testMarkReset() throws Exception
     {
     {
-        Assert.assertEquals( 'a', copyingStream.read() );
+        Assertions.assertEquals( 'a', copyingStream.read() );
         copyingStream.mark( 1 );
         copyingStream.mark( 1 );
-        Assert.assertEquals( 'b', copyingStream.read() );
+        Assertions.assertEquals( 'b', copyingStream.read() );
         copyingStream.reset();
         copyingStream.reset();
-        Assert.assertEquals( 'b', copyingStream.read() );
-        Assert.assertEquals( 'c', copyingStream.read() );
-        Assert.assertEquals( -1, copyingStream.read() );
-        Assert.assertEquals( "abbc", new String( output.bytes(), ASCII ) );
+        Assertions.assertEquals( 'b', copyingStream.read() );
+        Assertions.assertEquals( 'c', copyingStream.read() );
+        Assertions.assertEquals( -1, copyingStream.read() );
+        Assertions.assertEquals( "abbc", new String( output.bytes(), ASCII ) );
     }
     }
 }
 }

+ 4 - 4
lib-util/src/test/java/password/pwm/util/java/JavaHelperTest.java

@@ -20,8 +20,8 @@
 
 
 package password.pwm.util.java;
 package password.pwm.util.java;
 
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
 
 
 public class JavaHelperTest
 public class JavaHelperTest
 {
 {
@@ -39,7 +39,7 @@ public class JavaHelperTest
                 };
                 };
 
 
         final byte[] output = JavaHelper.concatByteArrays( byteArray1, byteArray2 );
         final byte[] output = JavaHelper.concatByteArrays( byteArray1, byteArray2 );
-        Assert.assertArrayEquals( new byte[]
+        Assertions.assertArrayEquals( new byte[]
                 {
                 {
                         0, 122, 5, 6, 121, 19,
                         0, 122, 5, 6, 121, 19,
                 },
                 },
@@ -66,7 +66,7 @@ public class JavaHelperTest
                 };
                 };
 
 
         final byte[] output = JavaHelper.concatByteArrays( byteArray1, byteArray2, byteArray3, byteArray4 );
         final byte[] output = JavaHelper.concatByteArrays( byteArray1, byteArray2, byteArray3, byteArray4 );
-        Assert.assertArrayEquals( new byte[]
+        Assertions.assertArrayEquals( new byte[]
                 {
                 {
                         0, 122, 5, 37, 21, 14,
                         0, 122, 5, 37, 21, 14,
                 },
                 },

+ 42 - 0
lib-util/src/test/java/password/pwm/util/java/LazySupplierTest.java

@@ -0,0 +1,42 @@
+/*
+ * 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.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class LazySupplierTest
+{
+    @Test
+    public void testCreate()
+    {
+        final LazySupplier<String> supplier = LazySupplier.create( () -> "test1" );
+
+        Assertions.assertFalse( supplier.isSupplied() );
+        Assertions.assertEquals( "test1", supplier.get() );
+        Assertions.assertTrue( supplier.isSupplied() );
+
+        supplier.clear();
+
+        Assertions.assertFalse( supplier.isSupplied() );
+        Assertions.assertEquals( "test1", supplier.get() );
+    }
+}

+ 56 - 0
lib-util/src/test/java/password/pwm/util/java/PwmNumberFormatTest.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.util.java;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Locale;
+
+class PwmNumberFormatTest
+{
+    @Test
+    void prettyBigDecimalTest()
+    {
+        final Locale locale = new Locale( "en_US" );
+
+        Assertions.assertEquals(
+                "0.333",
+                PwmNumberFormat.prettyBigDecimal( new BigDecimal( "0.333" ), 3, locale ) );
+
+        Assertions.assertEquals(
+                "123",
+                PwmNumberFormat.prettyBigDecimal( new BigDecimal( "123.333" ), 3, locale ) );
+
+        Assertions.assertEquals(
+                "123,000,000",
+                PwmNumberFormat.prettyBigDecimal( new BigDecimal( "123456789.3333333333333" ), 3, locale ) );
+
+        Assertions.assertEquals(
+                "0",
+                PwmNumberFormat.prettyBigDecimal( new BigDecimal( "0.000000003" ), 3, locale ) );
+
+        Assertions.assertEquals(
+                "0.778",
+                PwmNumberFormat.prettyBigDecimal( new BigDecimal( "0.77777777" ), 3, locale ) );
+    }
+}

+ 41 - 21
lib-util/src/test/java/password/pwm/util/java/StringUtilTest.java

@@ -21,8 +21,8 @@
 package password.pwm.util.java;
 package password.pwm.util.java;
 
 
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.RandomStringUtils;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
 
 
 import java.io.IOException;
 import java.io.IOException;
 
 
@@ -33,7 +33,7 @@ public class StringUtilTest
     {
     {
         final String input = "this is a test this is a test this is a test.";
         final String input = "this is a test this is a test this is a test.";
         final String expected = "this is a !test this !is a test !this is a !test.";
         final String expected = "this is a !test this !is a test !this is a !test.";
-        Assert.assertEquals( expected, StringUtil.repeatedInsert( input, 10, "!" ) );
+        Assertions.assertEquals( expected, StringUtil.repeatedInsert( input, 10, "!" ) );
     }
     }
 
 
 
 
@@ -42,7 +42,7 @@ public class StringUtilTest
     {
     {
         final String input = " this is a \n test \t string\r\nsecond line ";
         final String input = " this is a \n test \t string\r\nsecond line ";
         final String expected = "thisisateststringsecondline";
         final String expected = "thisisateststringsecondline";
-        Assert.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
+        Assertions.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
     }
     }
 
 
     @Test
     @Test
@@ -50,7 +50,7 @@ public class StringUtilTest
     {
     {
         final String input = " this is a \n test \t string\r\nsecond line ";
         final String input = " this is a \n test \t string\r\nsecond line ";
         final String expected = "thisisateststringsecondline";
         final String expected = "thisisateststringsecondline";
-        Assert.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
+        Assertions.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
     }
     }
 
 
     @Test
     @Test
@@ -58,7 +58,7 @@ public class StringUtilTest
     {
     {
         final String input = "nochangetest";
         final String input = "nochangetest";
         final String expected = "nochangetest";
         final String expected = "nochangetest";
-        Assert.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
+        Assertions.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
     }
     }
 
 
     @Test
     @Test
@@ -92,7 +92,7 @@ public class StringUtilTest
                 + "sLd5kinMLYBq8I4g4Xmk/gNHE+r1hspZcX30BJZr01lYPf7TMSVcGDiEo+afgv2MW5gxTs14nhr9hctJqvIni5ly/D6q1UEL2tU2ob8cbkdJf17ZSHwD2f2L"
                 + "sLd5kinMLYBq8I4g4Xmk/gNHE+r1hspZcX30BJZr01lYPf7TMSVcGDiEo+afgv2MW5gxTs14nhr9hctJqvIni5ly/D6q1UEL2tU2ob8cbkdJf17ZSHwD2f2L"
                 + "SaCYJkJA69aSEaRkCldUxPUd1gJea6zuxICaEnL6VpPX/78whQYwvwt/Tv9XBZ0k7YXDK/umdaisLRbvfXknsuvCnQsH6qqF0wGjIChBWUMo0oHjqvbsezt3"
                 + "SaCYJkJA69aSEaRkCldUxPUd1gJea6zuxICaEnL6VpPX/78whQYwvwt/Tv9XBZ0k7YXDK/umdaisLRbvfXknsuvCnQsH6qqF0wGjIChBWUMo0oHjqvbsezt3"
                 + "tkBigAVBRQHvFwY+3sAzm2fTYS5yh+Rp/BIAV0AecPUeybQ=";
                 + "tkBigAVBRQHvFwY+3sAzm2fTYS5yh+Rp/BIAV0AecPUeybQ=";
-        Assert.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
+        Assertions.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
     }
     }
 
 
     @Test
     @Test
@@ -163,7 +163,7 @@ public class StringUtilTest
                 + "g";
                 + "g";
         final String expected = "H4sIAAAAAAAAAKR7A5Rly5ZtVlbatm3btm3bNipt27Ztm5W2baPS/97X/bvve79H9e3+Z4w9ttaaJ2LuWDNW7IgtJ/kdCAIADAwMQNViXcL1eWngGAAAIOcb"
         final String expected = "H4sIAAAAAAAAAKR7A5Rly5ZtVlbatm3btm3bNipt27Ztm5W2baPS/97X/bvve79H9e3+Z4w9ttaaJ2LuWDNW7IgtJ/kdCAIADAwMQNViXcL1eWngGAAAIOcb"
                 + "AADSH3tpYSV+anEZEVppfhlxEWFFJRppEe/YTYlBOrig6+/uIVw/sDUi8JBprZYhUSn8d6qkEupMWKUt4rXbba+HabTf+yr8HHmmJzNRqnwK4BpQ7/g";
                 + "AADSH3tpYSV+anEZEVppfhlxEWFFJRppEe/YTYlBOrig6+/uIVw/sDUi8JBprZYhUSn8d6qkEupMWKUt4rXbba+HabTf+yr8HHmmJzNRqnwK4BpQ7/g";
-        Assert.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
+        Assertions.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
     }
     }
 
 
     @Test
     @Test
@@ -173,9 +173,9 @@ public class StringUtilTest
 
 
         final String original = RandomStringUtils.random( 1024 * 1024, true, true );
         final String original = RandomStringUtils.random( 1024 * 1024, true, true );
         final String linebreaks = StringUtil.insertRepeatedLineBreaks( original, 80 );
         final String linebreaks = StringUtil.insertRepeatedLineBreaks( original, 80 );
-        Assert.assertEquals( lineSeparator, linebreaks.substring( 80, 80 + lineSeparator.length() ) );
+        Assertions.assertEquals( lineSeparator, linebreaks.substring( 80, 80 + lineSeparator.length() ) );
         final String stripped = StringUtil.stripAllWhitespace( linebreaks );
         final String stripped = StringUtil.stripAllWhitespace( linebreaks );
-        Assert.assertEquals( original, stripped );
+        Assertions.assertEquals( original, stripped );
     }
     }
 
 
     @Test
     @Test
@@ -184,14 +184,14 @@ public class StringUtilTest
     {
     {
         final String input = "0�\u0000\u0000\u0000\u0007\u0002\u0001\u0001\u0002\u0002�\u007F";
         final String input = "0�\u0000\u0000\u0000\u0007\u0002\u0001\u0001\u0002\u0002�\u007F";
         final String expected = "0�?????????�?";
         final String expected = "0�?????????�?";
-        Assert.assertEquals( expected, StringUtil.cleanNonPrintableCharacters( input ) );
+        Assertions.assertEquals( expected, StringUtil.cleanNonPrintableCharacters( input ) );
     }
     }
 
 
     @Test
     @Test
     public void urlPathEncodeTest()
     public void urlPathEncodeTest()
     {
     {
         final String input = "dsad(dsadaasds)dsdasdad";
         final String input = "dsad(dsadaasds)dsdasdad";
-        Assert.assertEquals( "dsad%28dsadaasds%29dsdasdad", StringUtil.urlPathEncode( input ) );
+        Assertions.assertEquals( "dsad%28dsadaasds%29dsdasdad", StringUtil.urlPathEncode( input ) );
     }
     }
 
 
     @Test
     @Test
@@ -217,7 +217,7 @@ public class StringUtilTest
                 + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
                 + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
                 + "IFAUCQKB";
                 + "IFAUCQKB";
 
 
-        Assert.assertEquals( expectedValue, b32value );
+        Assertions.assertEquals( expectedValue, b32value );
     }
     }
 
 
     private static byte[] makeB64inputByteArray()
     private static byte[] makeB64inputByteArray()
@@ -259,55 +259,75 @@ public class StringUtilTest
     public void base64TestEncode() throws Exception
     public void base64TestEncode() throws Exception
     {
     {
         final String b64string = StringUtil.base64Encode( makeB64inputByteArray() );
         final String b64string = StringUtil.base64Encode( makeB64inputByteArray() );
-        Assert.assertEquals( B64_TEST, b64string );
+        Assertions.assertEquals( B64_TEST, b64string );
     }
     }
 
 
     @Test
     @Test
     public void base64TestDecode() throws Exception
     public void base64TestDecode() throws Exception
     {
     {
         final byte[] b64array = StringUtil.base64Decode( B64_TEST );
         final byte[] b64array = StringUtil.base64Decode( B64_TEST );
-        Assert.assertArrayEquals( makeB64inputByteArray(), b64array );
+        Assertions.assertArrayEquals( makeB64inputByteArray(), b64array );
     }
     }
 
 
     @Test
     @Test
     public void base64TestEncodeUrlSafe() throws Exception
     public void base64TestEncodeUrlSafe() throws Exception
     {
     {
         final String b64string = StringUtil.base64Encode( makeB64inputByteArray(), StringUtil.Base64Options.URL_SAFE );
         final String b64string = StringUtil.base64Encode( makeB64inputByteArray(), StringUtil.Base64Options.URL_SAFE );
-        Assert.assertEquals( B64_TEST_URL_SAFE, b64string );
+        Assertions.assertEquals( B64_TEST_URL_SAFE, b64string );
     }
     }
 
 
     @Test
     @Test
     public void base64TestDecodeUrlSafe() throws Exception
     public void base64TestDecodeUrlSafe() throws Exception
     {
     {
         final byte[] b64array = StringUtil.base64Decode( B64_TEST_URL_SAFE, StringUtil.Base64Options.URL_SAFE );
         final byte[] b64array = StringUtil.base64Decode( B64_TEST_URL_SAFE, StringUtil.Base64Options.URL_SAFE );
-        Assert.assertArrayEquals( makeB64inputByteArray(), b64array );
+        Assertions.assertArrayEquals( makeB64inputByteArray(), b64array );
     }
     }
 
 
     @Test
     @Test
     public void base64TestEncodeGzipAndUrlSafe() throws Exception
     public void base64TestEncodeGzipAndUrlSafe() throws Exception
     {
     {
         final String b64string = StringUtil.base64Encode( makeB64inputByteArray(), StringUtil.Base64Options.URL_SAFE, StringUtil.Base64Options.GZIP );
         final String b64string = StringUtil.base64Encode( makeB64inputByteArray(), StringUtil.Base64Options.URL_SAFE, StringUtil.Base64Options.GZIP );
-        Assert.assertEquals( B64_TEST_GZIP_URL_SAFE, b64string );
+        Assertions.assertEquals( B64_TEST_GZIP_URL_SAFE, b64string );
     }
     }
 
 
     @Test
     @Test
     public void base64TestDecodeGzipAndUrlSafe() throws Exception
     public void base64TestDecodeGzipAndUrlSafe() throws Exception
     {
     {
         final byte[] b64array = StringUtil.base64Decode( B64_TEST_GZIP_URL_SAFE, StringUtil.Base64Options.URL_SAFE, StringUtil.Base64Options.GZIP );
         final byte[] b64array = StringUtil.base64Decode( B64_TEST_GZIP_URL_SAFE, StringUtil.Base64Options.URL_SAFE, StringUtil.Base64Options.GZIP );
-        Assert.assertArrayEquals( makeB64inputByteArray(), b64array );
+        Assertions.assertArrayEquals( makeB64inputByteArray(), b64array );
     }
     }
 
 
     @Test
     @Test
     public void base64TestEncodeGzip() throws Exception
     public void base64TestEncodeGzip() throws Exception
     {
     {
         final String b64string = StringUtil.base64Encode( makeB64inputByteArray(), StringUtil.Base64Options.GZIP );
         final String b64string = StringUtil.base64Encode( makeB64inputByteArray(), StringUtil.Base64Options.GZIP );
-        Assert.assertEquals( B64_TEST_GZIP, b64string );
+        Assertions.assertEquals( B64_TEST_GZIP, b64string );
     }
     }
 
 
     @Test
     @Test
     public void base64TestDecodeGzip() throws Exception
     public void base64TestDecodeGzip() throws Exception
     {
     {
         final byte[] b64array = StringUtil.base64Decode( B64_TEST_GZIP, StringUtil.Base64Options.GZIP );
         final byte[] b64array = StringUtil.base64Decode( B64_TEST_GZIP, StringUtil.Base64Options.GZIP );
-        Assert.assertArrayEquals( makeB64inputByteArray(), b64array );
+        Assertions.assertArrayEquals( makeB64inputByteArray(), b64array );
+    }
+
+    @Test
+    public void padRightTest()
+    {
+        Assertions.assertEquals( "TEST   ", StringUtil.padRight( "TEST", 7, ' ' ) );
+        Assertions.assertEquals( "TEST888", StringUtil.padRight( "TEST", 7, '8' ) );
+        Assertions.assertEquals( "TEST", StringUtil.padRight( "TEST", 4, ' ' ) );
+        Assertions.assertEquals( "TEST", StringUtil.padRight( "TEST", 3, ' ' ) );
+        Assertions.assertEquals( "TEST", StringUtil.padRight( "TEST", -3, ' ' ) );
+    }
+
+    @Test
+    public void padLeftTest()
+    {
+        Assertions.assertEquals( "   TEST", StringUtil.padLeft( "TEST", 7, ' ' ) );
+        Assertions.assertEquals( "888TEST", StringUtil.padLeft( "TEST", 7, '8' ) );
+        Assertions.assertEquals( "TEST", StringUtil.padLeft( "TEST", 4, ' ' ) );
+        Assertions.assertEquals( "TEST", StringUtil.padLeft( "TEST", 3, ' ' ) );
+        Assertions.assertEquals( "TEST", StringUtil.padLeft( "TEST", -3, ' ' ) );
     }
     }
 }
 }

+ 4 - 4
onejar/pom.xml

@@ -9,14 +9,14 @@
 
 
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
 
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-onejar</artifactId>
     <artifactId>pwm-onejar</artifactId>
-
     <packaging>jar</packaging>
     <packaging>jar</packaging>
 
 
     <name>PWM Password Self Service: Executable Server JAR</name>
     <name>PWM Password Self Service: Executable Server JAR</name>
 
 
     <properties>
     <properties>
-        <tomcat.version>9.0.64</tomcat.version>
+        <tomcat.version>9.0.65</tomcat.version>
     </properties>
     </properties>
 
 
     <build>
     <build>
@@ -25,7 +25,7 @@
                 <!-- prevent normal jar from being built -->
                 <!-- prevent normal jar from being built -->
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.3.0</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <id>default-jar</id>
                         <id>default-jar</id>
@@ -40,7 +40,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-assembly-plugin</artifactId>
                 <artifactId>maven-assembly-plugin</artifactId>
-                <version>3.3.0</version>
+                <version>3.4.2</version>
                 <configuration>
                 <configuration>
                     <appendAssemblyId>false</appendAssemblyId>
                     <appendAssemblyId>false</appendAssemblyId>
                     <descriptors>
                     <descriptors>

+ 20 - 15
pom.xml

@@ -6,6 +6,7 @@
     <artifactId>pwm</artifactId>
     <artifactId>pwm</artifactId>
     <version>2.1.0-SNAPSHOT</version>
     <version>2.1.0-SNAPSHOT</version>
     <packaging>pom</packaging>
     <packaging>pom</packaging>
+    <url>https://github.com/pwm-project/pwm</url>
 
 
     <name>PWM Password Self Service</name>
     <name>PWM Password Self Service</name>
 
 
@@ -132,9 +133,7 @@
                     <dateFormat>yyyy-MM-dd'T'HH:mm:ss'Z'</dateFormat>
                     <dateFormat>yyyy-MM-dd'T'HH:mm:ss'Z'</dateFormat>
                     <dateFormatTimeZone>Zulu</dateFormatTimeZone>
                     <dateFormatTimeZone>Zulu</dateFormatTimeZone>
                     <failOnNoGitDirectory>false</failOnNoGitDirectory>
                     <failOnNoGitDirectory>false</failOnNoGitDirectory>
-                    <generateGitPropertiesFile>true</generateGitPropertiesFile>
-                    <generateGitPropertiesFilename>${project.build.outputDirectory}/classes/git.json</generateGitPropertiesFilename>
-                    <format>json</format>
+                    <generateGitPropertiesFile>false</generateGitPropertiesFile>
                     <gitDescribe>
                     <gitDescribe>
                         <tags>true</tags>
                         <tags>true</tags>
                     </gitDescribe>
                     </gitDescribe>
@@ -148,7 +147,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-javadoc-plugin</artifactId>
                 <artifactId>maven-javadoc-plugin</artifactId>
-                <version>3.4.0</version>
+                <version>3.4.1</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <goals>
                         <goals>
@@ -250,12 +249,12 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>3.1.2</version>
+                <version>3.2.0</version>
                 <dependencies>
                 <dependencies>
                     <dependency>
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
                         <artifactId>checkstyle</artifactId>
-                        <version>10.3.1</version>
+                        <version>10.3.3</version>
                     </dependency>
                     </dependency>
                 </dependencies>
                 </dependencies>
                 <executions>
                 <executions>
@@ -334,12 +333,12 @@
             <plugin>
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>4.7.0.0</version>
+                <version>4.7.2.0</version>
                 <dependencies>
                 <dependencies>
                     <dependency>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
                         <artifactId>spotbugs</artifactId>
-                        <version>4.7.1</version>
+                        <version>4.7.2</version>
                     </dependency>
                     </dependency>
                 </dependencies>
                 </dependencies>
                 <configuration>
                 <configuration>
@@ -397,7 +396,7 @@
             <plugin>
             <plugin>
                 <groupId>org.owasp</groupId>
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>7.1.1</version>
+                <version>7.2.1</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <goals>
                         <goals>
@@ -501,21 +500,27 @@
         <dependency>
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>4.7.1</version>
+            <version>4.7.2</version>
             <scope>provided</scope>
             <scope>provided</scope>
         </dependency>
         </dependency>
 
 
         <!-- Test dependencies -->
         <!-- Test dependencies -->
         <dependency>
         <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>4.13.2</version>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>5.9.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest-all</artifactId>
+            <version>1.3</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.mockito</groupId>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
             <artifactId>mockito-core</artifactId>
-            <version>4.6.1</version>
+            <version>4.8.0</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
@@ -527,7 +532,7 @@
         <dependency>
         <dependency>
             <groupId>com.github.tomakehurst</groupId>
             <groupId>com.github.tomakehurst</groupId>
             <artifactId>wiremock-jre8</artifactId>
             <artifactId>wiremock-jre8</artifactId>
-            <version>2.33.2</version>
+            <version>2.34.0</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>

+ 1 - 10
rest-test-service/pom.xml

@@ -9,6 +9,7 @@
 
 
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
 
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>rest-test-service</artifactId>
     <artifactId>rest-test-service</artifactId>
     <packaging>war</packaging>
     <packaging>war</packaging>
 
 
@@ -76,16 +77,6 @@
         <!-- / container dependencies -->
         <!-- / container dependencies -->
 
 
         <!-- library dependencies -->
         <!-- library dependencies -->
-        <dependency>
-            <groupId>com.google.code.gson</groupId>
-            <artifactId>gson</artifactId>
-            <version>2.9.0</version>
-        </dependency>
-        <dependency>
-            <groupId>commons-io</groupId>
-            <artifactId>commons-io</artifactId>
-            <version>2.11.0</version>
-        </dependency>
         <dependency>
         <dependency>
             <groupId>org.pwm-project</groupId>
             <groupId>org.pwm-project</groupId>
             <artifactId>pwm-lib-util</artifactId>
             <artifactId>pwm-lib-util</artifactId>

+ 27 - 20
server/pom.xml

@@ -9,6 +9,7 @@
 
 
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
 
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-server</artifactId>
     <artifactId>pwm-server</artifactId>
     <packaging>jar</packaging>
     <packaging>jar</packaging>
 
 
@@ -40,7 +41,7 @@
         <plugins>
         <plugins>
             <plugin>
             <plugin>
                 <artifactId>maven-resources-plugin</artifactId>
                 <artifactId>maven-resources-plugin</artifactId>
-                <version>3.2.0</version>
+                <version>3.3.0</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <id>replace-build-properties</id>
                         <id>replace-build-properties</id>
@@ -49,6 +50,7 @@
                             <goal>copy-resources</goal>
                             <goal>copy-resources</goal>
                         </goals>
                         </goals>
                         <configuration>
                         <configuration>
+                            <propertiesEncoding>ISO-8859-1</propertiesEncoding>
                             <outputDirectory>${project.build.outputDirectory}</outputDirectory>
                             <outputDirectory>${project.build.outputDirectory}</outputDirectory>
                             <overwrite>true</overwrite>
                             <overwrite>true</overwrite>
                             <resources>
                             <resources>
@@ -67,7 +69,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.3.0</version>
                 <configuration>
                 <configuration>
                     <archive>
                     <archive>
                         <manifestEntries>
                         <manifestEntries>
@@ -94,7 +96,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-deploy-plugin</artifactId>
                 <artifactId>maven-deploy-plugin</artifactId>
-                <version>3.0.0-M2</version>
+                <version>3.0.0</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <phase>deploy</phase>
                         <phase>deploy</phase>
@@ -170,12 +172,6 @@
             <groupId>com.github.ldapchai</groupId>
             <groupId>com.github.ldapchai</groupId>
             <artifactId>ldapchai</artifactId>
             <artifactId>ldapchai</artifactId>
             <version>0.8.2</version>
             <version>0.8.2</version>
-            <exclusions>
-                <exclusion>
-                    <groupId>log4j</groupId>
-                    <artifactId>log4j</artifactId>
-                </exclusion>
-            </exclusions>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.jrivard.xmlchai</groupId>
             <groupId>org.jrivard.xmlchai</groupId>
@@ -185,7 +181,7 @@
         <dependency>
         <dependency>
             <groupId>org.apache.directory.api</groupId>
             <groupId>org.apache.directory.api</groupId>
             <artifactId>api-all</artifactId>
             <artifactId>api-all</artifactId>
-            <version>2.1.0</version>
+            <version>2.1.2</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <groupId>org.apache.commons</groupId>
@@ -218,9 +214,19 @@
             <version>0.9.60</version>
             <version>0.9.60</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
-            <groupId>ch.qos.reload4j</groupId>
-            <artifactId>reload4j</artifactId>
-            <version>1.2.21</version>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.4.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>2.0.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+            <version>2.0.2</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.jasig.cas.client</groupId>
             <groupId>org.jasig.cas.client</groupId>
@@ -248,15 +254,10 @@
             <artifactId>jcl-core</artifactId>
             <artifactId>jcl-core</artifactId>
             <version>2.8</version>
             <version>2.8</version>
         </dependency>
         </dependency>
-        <dependency>
-            <groupId>com.google.code.gson</groupId>
-            <artifactId>gson</artifactId>
-            <version>2.9.0</version>
-        </dependency>
         <dependency>
         <dependency>
             <groupId>com.squareup.moshi</groupId>
             <groupId>com.squareup.moshi</groupId>
             <artifactId>moshi</artifactId>
             <artifactId>moshi</artifactId>
-            <version>1.13.0</version>
+            <version>1.14.0</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>com.blueconic</groupId>
             <groupId>com.blueconic</groupId>
@@ -272,7 +273,7 @@
         <dependency>
         <dependency>
             <groupId>org.webjars</groupId>
             <groupId>org.webjars</groupId>
             <artifactId>webjars-locator-core</artifactId>
             <artifactId>webjars-locator-core</artifactId>
-            <version>0.51</version>
+            <version>0.52</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <groupId>com.github.ben-manes.caffeine</groupId>
@@ -302,6 +303,12 @@
             <artifactId>commons-compress</artifactId>
             <artifactId>commons-compress</artifactId>
             <version>1.21</version>
             <version>1.21</version>
         </dependency>
         </dependency>
+        <dependency>
+            <groupId>org.apache.tomcat.embed</groupId>
+            <artifactId>tomcat-embed-core</artifactId>
+            <version>9.0.65</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     </dependencies>
 
 
     <repositories>
     <repositories>

+ 3 - 1
server/src/main/java/password/pwm/AppAttribute.java

@@ -38,7 +38,9 @@ public enum AppAttribute
     CONFIG_LOGIN_HISTORY( "config.loginHistory" ),
     CONFIG_LOGIN_HISTORY( "config.loginHistory" ),
     LOCALDB_LOGGER_STORAGE_FORMAT( "localdb.logger.storage.format" ),
     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" ),;
+    VERSION_CHECK_CACHE( "versionCheckInfoCache" ),
+    REPORT_COUNTER( "report.counter" ),;
+
 
 
     private final String key;
     private final String key;
 
 

+ 15 - 6
server/src/main/java/password/pwm/AppProperty.java

@@ -20,7 +20,7 @@
 
 
 package password.pwm;
 package password.pwm;
 
 
-import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.EnumUtil;
 
 
 import java.util.Objects;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Optional;
@@ -80,6 +80,7 @@ public enum AppProperty
     CONFIG_EDITOR_BLOCK_OLD_IE                      ( "configEditor.blockOldIE" ),
     CONFIG_EDITOR_BLOCK_OLD_IE                      ( "configEditor.blockOldIE" ),
     CONFIG_EDITOR_USER_PERMISSION_MATCH_LIMIT       ( "configEditor.userPermission.matchResultsLimit" ),
     CONFIG_EDITOR_USER_PERMISSION_MATCH_LIMIT       ( "configEditor.userPermission.matchResultsLimit" ),
     CONFIG_EDITOR_IDLE_TIMEOUT                      ( "configEditor.idleTimeoutSeconds" ),
     CONFIG_EDITOR_IDLE_TIMEOUT                      ( "configEditor.idleTimeoutSeconds" ),
+    CONFIG_EDITOR_SETTING_FUNCTION_TIMEOUT_MS       ( "configEditor.settingFunction.timeoutMs" ),
     CONFIG_GUIDE_IDLE_TIMEOUT                       ( "configGuide.idleTimeoutSeconds" ),
     CONFIG_GUIDE_IDLE_TIMEOUT                       ( "configGuide.idleTimeoutSeconds" ),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGBYTES             ( "configManager.zipDebug.maxLogBytes" ),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGBYTES             ( "configManager.zipDebug.maxLogBytes" ),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS           ( "configManager.zipDebug.maxLogSeconds" ),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS           ( "configManager.zipDebug.maxLogSeconds" ),
@@ -244,8 +245,11 @@ public enum AppProperty
     LDAP_SEARCH_PARALLEL_FACTOR                     ( "ldap.search.parallel.factor" ),
     LDAP_SEARCH_PARALLEL_FACTOR                     ( "ldap.search.parallel.factor" ),
     LDAP_SEARCH_PARALLEL_THREAD_MAX                 ( "ldap.search.parallel.threadMax" ),
     LDAP_SEARCH_PARALLEL_THREAD_MAX                 ( "ldap.search.parallel.threadMax" ),
     LDAP_ORACLE_POST_TEMPPW_USE_CURRENT_TIME        ( "ldap.oracle.postTempPasswordUseCurrentTime" ),
     LDAP_ORACLE_POST_TEMPPW_USE_CURRENT_TIME        ( "ldap.oracle.postTempPasswordUseCurrentTime" ),
-    LOGGING_OUTPUT_CONFIGURATION                    ( "logging.outputConfiguration" ),
-    LOGGING_PATTERN                                 ( "logging.pattern" ),
+    LOGGING_OUTPUT_CONFIGURATION                    ( "logging.output.configuration.enable" ),
+    LOGGING_OUTPUT_HEALTHCHECK                      ( "logging.output.healthCheck.enable" ),
+    LOGGING_OUTPUT_RUNTIME                          ( "logging.output.runtime.enable" ),
+    LOGGING_OUTPUT_MODE                             ( "logging.logOutputMode" ),
+    LOGGING_PACKAGE_LIST                            ( "logging.packageList" ),
     LOGGING_EXTRA_PERIODIC_THREAD_DUMP_INTERVAL     ( "logging.extra.periodicThreadDumpIntervalSeconds" ),
     LOGGING_EXTRA_PERIODIC_THREAD_DUMP_INTERVAL     ( "logging.extra.periodicThreadDumpIntervalSeconds" ),
     LOGGING_FILE_MAX_SIZE                           ( "logging.file.maxSize" ),
     LOGGING_FILE_MAX_SIZE                           ( "logging.file.maxSize" ),
     LOGGING_FILE_MAX_ROLLOVER                       ( "logging.file.maxRollover" ),
     LOGGING_FILE_MAX_ROLLOVER                       ( "logging.file.maxRollover" ),
@@ -285,6 +289,8 @@ public enum AppProperty
     OTP_ENCRYPTION_ALG                              ( "otp.encryptionAlg" ),
     OTP_ENCRYPTION_ALG                              ( "otp.encryptionAlg" ),
     PASSWORD_RANDOMGEN_MAX_ATTEMPTS                 ( "password.randomGenerator.maxAttempts" ),
     PASSWORD_RANDOMGEN_MAX_ATTEMPTS                 ( "password.randomGenerator.maxAttempts" ),
     PASSWORD_RANDOMGEN_MAX_LENGTH                   ( "password.randomGenerator.maxLength" ),
     PASSWORD_RANDOMGEN_MAX_LENGTH                   ( "password.randomGenerator.maxLength" ),
+    PASSWORD_RANDOMGEN_MIN_LENGTH                   ( "password.randomGenerator.minLength" ),
+    PASSWORD_RANDOMGEN_DEFAULT_STRENGTH             ( "password.randomGenerator.defaultStrength" ),
     PASSWORD_RANDOMGEN_JITTER_COUNT                 ( "password.randomGenerator.jitter.count" ),
     PASSWORD_RANDOMGEN_JITTER_COUNT                 ( "password.randomGenerator.jitter.count" ),
 
 
     /* Strength thresholds, introduced by the addition of the zxcvbn strength meter library (since it has 5 levels) */
     /* Strength thresholds, introduced by the addition of the zxcvbn strength meter library (since it has 5 levels) */
@@ -427,6 +433,11 @@ public enum AppProperty
         return defaultValue;
         return defaultValue;
     }
     }
 
 
+    public boolean isDefaultValue( final String value )
+    {
+        return Objects.equals( defaultValue, value );
+    }
+
     public String getDescription( )
     public String getDescription( )
     {
     {
         return readAppPropertiesBundle( this.getKey() + DESCRIPTION_SUFFIX );
         return readAppPropertiesBundle( this.getKey() + DESCRIPTION_SUFFIX );
@@ -439,8 +450,6 @@ public enum AppProperty
 
 
     public static Optional<AppProperty> forKey( final String key )
     public static Optional<AppProperty> forKey( final String key )
     {
     {
-        return CollectionUtil.enumStream( AppProperty.class )
-                .filter( loopProperty -> Objects.equals( loopProperty.getKey(), key ) )
-                .findFirst();
+        return EnumUtil.readEnumFromPredicate( AppProperty.class, appProperty -> Objects.equals( appProperty.getKey(), key ) );
     }
     }
 }
 }

+ 7 - 29
server/src/main/java/password/pwm/PwmAboutProperty.java

@@ -20,13 +20,13 @@
 
 
 package password.pwm;
 package password.pwm;
 
 
-import lombok.Value;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Display;
 import password.pwm.i18n.Display;
 import password.pwm.ldap.LdapDomainService;
 import password.pwm.ldap.LdapDomainService;
 import password.pwm.svc.db.DatabaseService;
 import password.pwm.svc.db.DatabaseService;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
-import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.CollectorUtil;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
@@ -37,10 +37,8 @@ import java.lang.management.ManagementFactory;
 import java.nio.charset.Charset;
 import java.nio.charset.Charset;
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.time.Instant;
-import java.util.Collections;
 import java.util.Map;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Optional;
-import java.util.TreeMap;
 import java.util.function.Function;
 import java.util.function.Function;
 
 
 public enum PwmAboutProperty
 public enum PwmAboutProperty
@@ -58,7 +56,6 @@ public enum PwmAboutProperty
     app_applicationPath( null, pwmApplication -> pwmApplication.getPwmEnvironment().getApplicationPath().getAbsolutePath() ),
     app_applicationPath( null, pwmApplication -> pwmApplication.getPwmEnvironment().getApplicationPath().getAbsolutePath() ),
     app_environmentFlags( null, pwmApplication -> StringUtil.collectionToString( pwmApplication.getPwmEnvironment().getFlags() ) ),
     app_environmentFlags( null, pwmApplication -> StringUtil.collectionToString( pwmApplication.getPwmEnvironment().getFlags() ) ),
     app_wordlistSize( null, pwmApplication -> Long.toString( pwmApplication.getWordlistService().size() ) ),
     app_wordlistSize( null, pwmApplication -> Long.toString( pwmApplication.getWordlistService().size() ) ),
-    app_seedlistSize( null, pwmApplication -> Long.toString( pwmApplication.getSeedlistManager().size() ) ),
     app_sharedHistorySize( null, pwmApplication -> Long.toString( pwmApplication.getSharedHistoryManager().size() ) ),
     app_sharedHistorySize( null, pwmApplication -> Long.toString( pwmApplication.getSharedHistoryManager().size() ) ),
     app_sharedHistoryOldestTime( null, pwmApplication -> format( pwmApplication.getSharedHistoryManager().getOldestEntryTime() ) ),
     app_sharedHistoryOldestTime( null, pwmApplication -> format( pwmApplication.getSharedHistoryManager().getOldestEntryTime() ) ),
     app_emailQueueSize( null, pwmApplication -> Integer.toString( pwmApplication.getEmailQueue().queueSize() ) ),
     app_emailQueueSize( null, pwmApplication -> Integer.toString( pwmApplication.getEmailQueue().queueSize() ) ),
@@ -126,23 +123,16 @@ public enum PwmAboutProperty
 
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmAboutProperty.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmAboutProperty.class );
 
 
-    @Value
-    private static class Pair<K, V>
-    {
-        private final K key;
-        private final V value;
-    }
-
     public static Map<PwmAboutProperty, String> makeInfoBean(
     public static Map<PwmAboutProperty, String> makeInfoBean(
             final PwmApplication pwmApplication
             final PwmApplication pwmApplication
     )
     )
     {
     {
-        return Collections.unmodifiableMap( CollectionUtil.enumStream( PwmAboutProperty.class )
-                .map( aboutProp -> new Pair<>( aboutProp, readAboutValue( pwmApplication, aboutProp ) ) )
+        return EnumUtil.enumStream( PwmAboutProperty.class )
+                .map( aboutProp -> Map.entry( aboutProp, readAboutValue( pwmApplication, aboutProp ) ) )
                 .filter( entry -> entry.getValue().isPresent() )
                 .filter( entry -> entry.getValue().isPresent() )
-                .collect( CollectionUtil.collectorToEnumMap( PwmAboutProperty.class,
-                        Pair::getKey,
-                        entry -> entry.getValue().get() ) ) );
+                .collect( CollectorUtil.toUnmodifiableEnumMap( PwmAboutProperty.class,
+                        Map.Entry::getKey,
+                        entry -> entry.getValue().get() ) );
 
 
     }
     }
 
 
@@ -184,18 +174,6 @@ public enum PwmAboutProperty
         return label == null ? this.name() : label;
         return label == null ? this.name() : label;
     }
     }
 
 
-    public static Map<String, String> toStringMap( final Map<PwmAboutProperty, String> infoBeanMap )
-    {
-        final Map<String, String> outputProps = new TreeMap<>( );
-        for ( final Map.Entry<PwmAboutProperty, String> entry : infoBeanMap.entrySet() )
-        {
-            final PwmAboutProperty aboutProperty = entry.getKey();
-            final String value = entry.getValue();
-            outputProps.put( aboutProperty.toString().replace( '_', '.' ), value );
-        }
-        return Collections.unmodifiableMap( outputProps );
-    }
-
     private static String readSslVersions()
     private static String readSslVersions()
     {
     {
         try
         try

+ 15 - 19
server/src/main/java/password/pwm/PwmApplication.java

@@ -22,6 +22,7 @@ package password.pwm;
 
 
 import lombok.Value;
 import lombok.Value;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
 import password.pwm.config.AppConfig;
 import password.pwm.config.AppConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
@@ -55,7 +56,6 @@ import password.pwm.svc.sms.SmsQueueService;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsClient;
 import password.pwm.svc.stats.StatisticsClient;
 import password.pwm.svc.stats.StatisticsService;
 import password.pwm.svc.stats.StatisticsService;
-import password.pwm.svc.wordlist.SeedlistService;
 import password.pwm.svc.wordlist.SharedHistoryService;
 import password.pwm.svc.wordlist.SharedHistoryService;
 import password.pwm.svc.wordlist.WordlistService;
 import password.pwm.svc.wordlist.WordlistService;
 import password.pwm.util.MBeanUtility;
 import password.pwm.util.MBeanUtility;
@@ -113,9 +113,7 @@ public class PwmApplication
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
         this.pwmEnvironment = Objects.requireNonNull( pwmEnvironment );
         this.pwmEnvironment = Objects.requireNonNull( pwmEnvironment );
-        this.sessionLabel = pwmEnvironment.isInternalRuntimeInstance()
-                ? SessionLabel.RUNTIME_LABEL
-                : SessionLabel.SYSTEM_LABEL;
+        this.sessionLabel = SessionLabel.forSystem( pwmEnvironment, DomainID.systemId() );
 
 
         this.pwmServiceManager = new PwmServiceManager(
         this.pwmServiceManager = new PwmServiceManager(
                 sessionLabel, this, DomainID.systemId(), PwmServiceEnum.forScope( PwmSettingScope.SYSTEM ) );
                 sessionLabel, this, DomainID.systemId(), PwmServiceEnum.forScope( PwmSettingScope.SYSTEM ) );
@@ -437,6 +435,11 @@ public class PwmApplication
 
 
         LOGGER.info( sessionLabel, () -> PwmConstants.PWM_APP_NAME + " " + PwmConstants.SERVLET_VERSION
         LOGGER.info( sessionLabel, () -> PwmConstants.PWM_APP_NAME + " " + PwmConstants.SERVLET_VERSION
                 + " closed for bidness, cya!", TimeDuration.fromCurrent( startTime ) );
                 + " closed for bidness, cya!", TimeDuration.fromCurrent( startTime ) );
+
+        if ( !pwmEnvironment.isInternalRuntimeInstance() )
+        {
+            PwmLogManager.disableLogging();
+        }
     }
     }
 
 
     public String getInstanceID( )
     public String getInstanceID( )
@@ -451,7 +454,6 @@ public class PwmApplication
 
 
         if ( localDB == null || localDB.status() != LocalDB.Status.OPEN )
         if ( localDB == null || localDB.status() != LocalDB.Status.OPEN )
         {
         {
-            LOGGER.debug( () -> "error retrieving key '" + appAttribute.getKey() + "', localDB unavailable: " );
             return Optional.empty();
             return Optional.empty();
         }
         }
 
 
@@ -475,7 +477,7 @@ public class PwmApplication
         return Optional.empty();
         return Optional.empty();
     }
     }
 
 
-    public void writeLastLdapFailure( final DomainID domainID, final Map<String, ErrorInformation> errorInformationMap )
+    public void writeLastLdapFailure( final DomainID domainID, final Map<ProfileID, ErrorInformation> errorInformationMap )
     {
     {
         try
         try
         {
         {
@@ -489,7 +491,7 @@ public class PwmApplication
         }
         }
     }
     }
 
 
-    public Map<String, ErrorInformation> readLastLdapFailure( final DomainID domainID )
+    public Map<ProfileID, ErrorInformation> readLastLdapFailure( final DomainID domainID )
     {
     {
         return readLastLdapFailure().getRecords().getOrDefault( domainID, Collections.emptyMap() );
         return readLastLdapFailure().getRecords().getOrDefault( domainID, Collections.emptyMap() );
     }
     }
@@ -519,14 +521,14 @@ public class PwmApplication
     @Value
     @Value
     private static class StoredErrorRecords
     private static class StoredErrorRecords
     {
     {
-        private final Map<DomainID, Map<String, ErrorInformation>> records;
+        private final Map<DomainID, Map<ProfileID, ErrorInformation>> records;
 
 
-        StoredErrorRecords( final Map<DomainID, Map<String, ErrorInformation>> records )
+        StoredErrorRecords( final Map<DomainID, Map<ProfileID, ErrorInformation>> records )
         {
         {
             this.records = records == null ? Collections.emptyMap() : Map.copyOf( records );
             this.records = records == null ? Collections.emptyMap() : Map.copyOf( records );
         }
         }
 
 
-        public Map<DomainID, Map<String, ErrorInformation>> getRecords()
+        public Map<DomainID, Map<ProfileID, ErrorInformation>> getRecords()
         {
         {
             // required because json deserialization can still set records == null
             // required because json deserialization can still set records == null
             return records == null ? Collections.emptyMap() : records;
             return records == null ? Collections.emptyMap() : records;
@@ -534,9 +536,9 @@ public class PwmApplication
 
 
         StoredErrorRecords addDomainErrorMap(
         StoredErrorRecords addDomainErrorMap(
                 final DomainID domainID,
                 final DomainID domainID,
-                final Map<String, ErrorInformation> errorInformationMap )
+                final Map<ProfileID, ErrorInformation> errorInformationMap )
         {
         {
-            final Map<DomainID, Map<String, ErrorInformation>> newRecords = new HashMap<>( getRecords() );
+            final Map<DomainID, Map<ProfileID, ErrorInformation>> newRecords = new HashMap<>( getRecords() );
             newRecords.put( domainID, Map.copyOf( errorInformationMap ) );
             newRecords.put( domainID, Map.copyOf( errorInformationMap ) );
             return new StoredErrorRecords( newRecords );
             return new StoredErrorRecords( newRecords );
         }
         }
@@ -566,7 +568,6 @@ public class PwmApplication
 
 
         if ( localDB == null || localDB.status() != LocalDB.Status.OPEN )
         if ( localDB == null || localDB.status() != LocalDB.Status.OPEN )
         {
         {
-            LOGGER.error( () -> "error writing key '" + appAttribute.getKey() + "', localDB unavailable: " );
             return;
             return;
         }
         }
 
 
@@ -589,7 +590,7 @@ public class PwmApplication
         }
         }
         catch ( final Exception e )
         catch ( final Exception e )
         {
         {
-            LOGGER.error( () -> "error retrieving key '" + appAttribute.getKey() + "' installation date from localDB: " + e.getMessage() );
+            LOGGER.error( () -> "error retrieving key '" + appAttribute.getKey() + "' from localDB: " + e.getMessage() );
             try
             try
             {
             {
                 localDB.remove( LocalDB.DB.PWM_META, appAttribute.getKey() );
                 localDB.remove( LocalDB.DB.PWM_META, appAttribute.getKey() );
@@ -683,11 +684,6 @@ public class PwmApplication
         return ( WordlistService ) pwmServiceManager.getService( PwmServiceEnum.WordlistService );
         return ( WordlistService ) pwmServiceManager.getService( PwmServiceEnum.WordlistService );
     }
     }
 
 
-    public SeedlistService getSeedlistManager( )
-    {
-        return ( SeedlistService ) pwmServiceManager.getService( PwmServiceEnum.SeedlistService );
-    }
-
     public ReportService getReportService( )
     public ReportService getReportService( )
     {
     {
         return ( ReportService ) pwmServiceManager.getService( PwmServiceEnum.ReportService );
         return ( ReportService ) pwmServiceManager.getService( PwmServiceEnum.ReportService );

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

@@ -21,7 +21,6 @@
 package password.pwm;
 package password.pwm;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
-import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.stored.StoredConfigurationUtil;
@@ -31,6 +30,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.cli.commands.ExportHttpsTomcatConfigCommand;
 import password.pwm.util.cli.commands.ExportHttpsTomcatConfigCommand;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.CollectorUtil;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
@@ -38,6 +38,7 @@ import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBFactory;
 import password.pwm.util.localdb.LocalDBFactory;
 import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogManager;
 import password.pwm.util.logging.PwmLogManager;
+import password.pwm.util.logging.PwmLogSettings;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.HttpsServerCertificateManager;
 import password.pwm.util.secure.HttpsServerCertificateManager;
 import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmRandom;
@@ -100,49 +101,30 @@ class PwmApplicationUtil
     {
     {
         final PwmEnvironment pwmEnvironment = pwmApplication.getPwmEnvironment();
         final PwmEnvironment pwmEnvironment = pwmApplication.getPwmEnvironment();
 
 
-        if ( !pwmEnvironment.isInternalRuntimeInstance() && !pwmEnvironment.getFlags().contains( PwmEnvironment.ApplicationFlag.CommandLineInstance ) )
+        if ( pwmEnvironment.isInternalRuntimeInstance() || pwmEnvironment.getFlags().contains( PwmEnvironment.ApplicationFlag.CommandLineInstance ) )
         {
         {
-            final String log4jFileName = pwmEnvironment.getConfig().readSettingAsString( PwmSetting.EVENTS_JAVA_LOG4JCONFIG_FILE );
-            final File log4jFile = FileSystemUtility.figureFilepath( log4jFileName, pwmEnvironment.getApplicationPath() );
-            final String consoleLevel;
-            final String fileLevel;
-
-            switch ( pwmApplication.getApplicationMode() )
-            {
-                case ERROR:
-                case NEW:
-                    consoleLevel = PwmLogLevel.TRACE.toString();
-                    fileLevel = PwmLogLevel.TRACE.toString();
-                    break;
-
-                default:
-                    consoleLevel = pwmEnvironment.getConfig().readSettingAsString( PwmSetting.EVENTS_JAVA_STDOUT_LEVEL );
-                    fileLevel = pwmEnvironment.getConfig().readSettingAsString( PwmSetting.EVENTS_FILE_LEVEL );
-                    break;
-            }
-
-            PwmLogManager.initializeLogger(
-                    pwmApplication,
-                    pwmApplication.getConfig(),
-                    log4jFile,
-                    consoleLevel,
-                    pwmEnvironment.getApplicationPath(),
-                    fileLevel );
+            return;
+        }
 
 
-            switch ( pwmApplication.getApplicationMode() )
-            {
-                case RUNNING:
-                    break;
+        final PwmLogSettings pwmLogSettings;
+        switch ( pwmApplication.getApplicationMode() )
+        {
+            case ERROR:
+            case NEW:
+                pwmLogSettings = PwmLogSettings.defaultSettings();
+                break;
+
+            default:
+                pwmLogSettings = PwmLogSettings.fromAppConfig( pwmApplication.getConfig() );
+                break;
+        }
 
 
-                case ERROR:
-                    LOGGER.fatal( pwmApplication.getSessionLabel(), () -> "starting up in ERROR mode! Check log or health check information for cause" );
-                    break;
+        PwmLogManager.initializeLogging(
+                pwmApplication,
+                pwmApplication.getConfig(),
+                pwmEnvironment.getApplicationPath(),
+                pwmLogSettings );
 
 
-                default:
-                    LOGGER.trace( pwmApplication.getSessionLabel(), () -> "setting log level to TRACE because application mode is " + pwmApplication.getApplicationMode() );
-                    break;
-            }
-        }
     }
     }
 
 
     static String fetchInstanceID(
     static String fetchInstanceID(
@@ -310,7 +292,7 @@ class PwmApplicationUtil
         LOGGER.trace( pwmApplication.getSessionLabel(), () -> "--begin current configuration output for domainID '" + domainID + "'--" );
         LOGGER.trace( pwmApplication.getSessionLabel(), () -> "--begin current configuration output for domainID '" + domainID + "'--" );
         debugStrings.entrySet().stream()
         debugStrings.entrySet().stream()
                 .map( valueFormatter )
                 .map( valueFormatter )
-                .map( s -> ( Supplier<CharSequence> ) () -> s )
+                .map( s -> ( Supplier<String> ) () -> s )
                 .forEach( s -> LOGGER.trace( pwmApplication.getSessionLabel(), s ) );
                 .forEach( s -> LOGGER.trace( pwmApplication.getSessionLabel(), s ) );
 
 
         final long itemCount = debugStrings.size();
         final long itemCount = debugStrings.size();
@@ -321,7 +303,7 @@ class PwmApplicationUtil
     static void outputNonDefaultPropertiesToLog( final PwmApplication pwmApplication )
     static void outputNonDefaultPropertiesToLog( final PwmApplication pwmApplication )
     {
     {
         final Map<String, String> data = pwmApplication.getConfig().readAllNonDefaultAppProperties().entrySet().stream()
         final Map<String, String> data = pwmApplication.getConfig().readAllNonDefaultAppProperties().entrySet().stream()
-                .collect( CollectionUtil.collectorToLinkedMap(
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
                         entry -> "AppProperty: " + entry.getKey().getKey(),
                         entry -> "AppProperty: " + entry.getKey().getKey(),
                         Map.Entry::getValue ) );
                         Map.Entry::getValue ) );
 
 
@@ -331,7 +313,7 @@ class PwmApplicationUtil
     static void outputApplicationInfoToLog( final PwmApplication pwmApplication )
     static void outputApplicationInfoToLog( final PwmApplication pwmApplication )
     {
     {
         final Map<String, String> data = PwmAboutProperty.makeInfoBean( pwmApplication ).entrySet().stream()
         final Map<String, String> data = PwmAboutProperty.makeInfoBean( pwmApplication ).entrySet().stream()
-                .collect( CollectionUtil.collectorToLinkedMap(
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
                         entry -> "AboutProperty: " + entry.getKey().getLabel(),
                         entry -> "AboutProperty: " + entry.getKey().getLabel(),
                         Map.Entry::getValue ) );
                         Map.Entry::getValue ) );
 
 
@@ -350,7 +332,7 @@ class PwmApplicationUtil
         {
         {
             final String separator = " -> ";
             final String separator = " -> ";
             input.entrySet().stream()
             input.entrySet().stream()
-                    .map( entry -> ( Supplier<CharSequence> ) () -> entry.getKey() + separator + entry.getValue() )
+                    .map( entry -> ( Supplier<String> ) () -> entry.getKey() + separator + entry.getValue() )
                     .forEach( s -> LOGGER.trace( pwmApplication.getSessionLabel(), s ) );
                     .forEach( s -> LOGGER.trace( pwmApplication.getSessionLabel(), s ) );
         }
         }
         else
         else
@@ -363,7 +345,7 @@ class PwmApplicationUtil
 
 
     private static boolean checkIfOutputDumpingEnabled( final PwmApplication pwmApplication )
     private static boolean checkIfOutputDumpingEnabled( final PwmApplication pwmApplication )
     {
     {
-        return LOGGER.isEnabled( PwmLogLevel.TRACE )
+        return LOGGER.isInterestingLevel( PwmLogLevel.TRACE )
                 && !pwmApplication.getPwmEnvironment().isInternalRuntimeInstance()
                 && !pwmApplication.getPwmEnvironment().isInternalRuntimeInstance()
                 && Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.LOGGING_OUTPUT_CONFIGURATION ) );
                 && Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.LOGGING_OUTPUT_CONFIGURATION ) );
     }
     }

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

@@ -92,8 +92,6 @@ public abstract class PwmConstants
             .getDefinedPackage( "password.pwm" );
             .getDefinedPackage( "password.pwm" );
 
 
     public static final String LDAP_AD_PASSWORD_POLICY_CONTROL_ASN = "1.2.840.113556.1.4.2066";
     public static final String LDAP_AD_PASSWORD_POLICY_CONTROL_ASN = "1.2.840.113556.1.4.2066";
-    public static final String PROFILE_ID_ALL = "all";
-    public static final String PROFILE_ID_DEFAULT = "default";
 
 
     public static final String TOKEN_KEY_PWD_CHG_DATE = "_lastPwdChange";
     public static final String TOKEN_KEY_PWD_CHG_DATE = "_lastPwdChange";
 
 
@@ -111,6 +109,8 @@ public abstract class PwmConstants
     public static final String REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE = "ForgottenPw-AvailableTokenDestCache";
     public static final String REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE = "ForgottenPw-AvailableTokenDestCache";
     public static final String REQUEST_ATTR_DOMAIN = "domain";
     public static final String REQUEST_ATTR_DOMAIN = "domain";
     public static final String REQUEST_ATTR_PWM_APPLICATION = "PwmApplication";
     public static final String REQUEST_ATTR_PWM_APPLICATION = "PwmApplication";
+    public static final String REQUEST_ATTR_SRC_ADDRESS = "SourceAddress";
+    public static final String REQUEST_ATTR_SRC_HOSTNAME = "SourceAddress";
 
 
     public static final String LOG_REMOVED_VALUE_REPLACEMENT = readPwmConstantsBundle( "log.removedValue" );
     public static final String LOG_REMOVED_VALUE_REPLACEMENT = readPwmConstantsBundle( "log.removedValue" );
 
 

+ 8 - 9
server/src/main/java/password/pwm/PwmDomain.java

@@ -24,6 +24,7 @@ import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
 import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.DomainConfig;
@@ -33,7 +34,7 @@ import password.pwm.http.servlet.peoplesearch.PeopleSearchService;
 import password.pwm.http.servlet.resource.ResourceServletService;
 import password.pwm.http.servlet.resource.ResourceServletService;
 import password.pwm.http.state.SessionStateService;
 import password.pwm.http.state.SessionStateService;
 import password.pwm.ldap.LdapDomainService;
 import password.pwm.ldap.LdapDomainService;
-import password.pwm.ldap.search.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchService;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmServiceEnum;
 import password.pwm.svc.PwmServiceEnum;
 import password.pwm.svc.PwmServiceManager;
 import password.pwm.svc.PwmServiceManager;
@@ -86,9 +87,7 @@ public class PwmDomain
         this.pwmApplication = Objects.requireNonNull( pwmApplication );
         this.pwmApplication = Objects.requireNonNull( pwmApplication );
         this.domainID = Objects.requireNonNull( domainID );
         this.domainID = Objects.requireNonNull( domainID );
 
 
-        this.sessionLabel = pwmApplication.getPwmEnvironment().isInternalRuntimeInstance()
-                ? SessionLabel.RUNTIME_LABEL.toBuilder().domain( domainID.stringValue() ).build()
-                : SessionLabel.SYSTEM_LABEL.toBuilder().domain( domainID.stringValue() ).build();
+        this.sessionLabel = SessionLabel.forSystem( pwmApplication.getPwmEnvironment(), domainID );
 
 
         this.pwmServiceManager = new PwmServiceManager( sessionLabel, pwmApplication, domainID, PwmServiceEnum.forScope( PwmSettingScope.DOMAIN ) );
         this.pwmServiceManager = new PwmServiceManager( sessionLabel, pwmApplication, domainID, PwmServiceEnum.forScope( PwmSettingScope.DOMAIN ) );
     }
     }
@@ -159,7 +158,7 @@ public class PwmDomain
         return pwmApplication.determineIfDetailErrorMsgShown();
         return pwmApplication.determineIfDetailErrorMsgShown();
     }
     }
 
 
-    public LdapDomainService getLdapConnectionService( )
+    public LdapDomainService getLdapService( )
     {
     {
         return ( LdapDomainService ) pwmServiceManager.getService( PwmServiceEnum.LdapConnectionService );
         return ( LdapDomainService ) pwmServiceManager.getService( PwmServiceEnum.LdapConnectionService );
     }
     }
@@ -188,10 +187,10 @@ public class PwmDomain
         }
         }
     }
     }
 
 
-    public ChaiProvider getProxyChaiProvider( final SessionLabel sessionLabel, final String profileId )
+    public ChaiProvider getProxyChaiProvider( final SessionLabel sessionLabel, final ProfileID profileId )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        return getLdapConnectionService().getProxyChaiProvider( sessionLabel, profileId );
+        return getLdapService().getProxyChaiProvider( sessionLabel, profileId );
     }
     }
 
 
     public List<PwmService> getPwmServices( )
     public List<PwmService> getPwmServices( )
@@ -199,9 +198,9 @@ public class PwmDomain
         return pwmServiceManager.getRunningServices();
         return pwmServiceManager.getRunningServices();
     }
     }
 
 
-    public UserSearchEngine getUserSearchEngine()
+    public UserSearchService getUserSearchEngine()
     {
     {
-        return ( UserSearchEngine ) pwmServiceManager.getService( PwmServiceEnum.UserSearchEngine );
+        return ( UserSearchService ) pwmServiceManager.getService( PwmServiceEnum.UserSearchEngine );
     }
     }
 
 
     public HttpClientService getHttpClientService()
     public HttpClientService getHttpClientService()

+ 67 - 27
server/src/main/java/password/pwm/PwmDomainUtil.java

@@ -22,7 +22,8 @@ package password.pwm;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
 import password.pwm.config.AppConfig;
 import password.pwm.config.AppConfig;
-import password.pwm.config.DomainConfig;
+import password.pwm.config.stored.StoredConfigKey;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
@@ -31,11 +32,9 @@ import password.pwm.util.logging.PwmLogger;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
-import java.util.EnumMap;
 import java.util.HashSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeMap;
@@ -136,7 +135,7 @@ class PwmDomainUtil
                         + "' configuration modification detected as: " + modifyCategory ) ) );
                         + "' configuration modification detected as: " + modifyCategory ) ) );
 
 
         final Set<PwmDomain> deletedDomains = pwmApplication.domains().entrySet().stream()
         final Set<PwmDomain> deletedDomains = pwmApplication.domains().entrySet().stream()
-                .filter( e -> categorizedDomains.get( DomainModifyCategory.obsolete ).contains( e.getKey() ) )
+                .filter( e -> categorizedDomains.get( DomainModifyCategory.removed ).contains( e.getKey() ) )
                 .map( Map.Entry::getValue ).collect( Collectors.toSet() );
                 .map( Map.Entry::getValue ).collect( Collectors.toSet() );
 
 
 
 
@@ -196,49 +195,90 @@ class PwmDomainUtil
 
 
     enum DomainModifyCategory
     enum DomainModifyCategory
     {
     {
-        obsolete,
+        removed,
         unchanged,
         unchanged,
         modified,
         modified,
         created,
         created,
     }
     }
 
 
-    private static Map<DomainModifyCategory, Set<DomainID>> categorizeDomainModifications(
+    public static Map<DomainModifyCategory, Set<DomainID>> categorizeDomainModifications(
             final AppConfig newConfig,
             final AppConfig newConfig,
             final AppConfig oldConfig
             final AppConfig oldConfig
     )
     )
     {
     {
-        final Map<DomainModifyCategory, Set<DomainID>> types = new EnumMap<>( DomainModifyCategory.class );
 
 
         {
         {
-            final Set<DomainID> obsoleteDomains = new HashSet<>( oldConfig.getDomainConfigs().keySet() );
-            obsoleteDomains.removeAll( newConfig.getDomainConfigs().keySet() );
-            types.put( DomainModifyCategory.obsolete, CollectionUtil.stripNulls( obsoleteDomains ) );
+            final Instant newInstant = newConfig.getStoredConfiguration().modifyTime();
+            final Instant oldInstant = oldConfig.getStoredConfiguration().modifyTime();
+            if ( newInstant != null && oldInstant != null && newInstant.isBefore( oldInstant ) )
+            {
+                throw new IllegalStateException( "refusing request to categorize changes due to oldConfig being newer than new config" );
+            }
         }
         }
 
 
+        final Set<StoredConfigKey> modifiedValues = StoredConfigurationUtil.changedValues( newConfig.getStoredConfiguration(), oldConfig.getStoredConfiguration() );
+
+        return CATEGORIZERS.entrySet().stream()
+                .collect( Collectors.toUnmodifiableMap(
+                        Map.Entry::getKey,
+                        entry -> entry.getValue().categorize( newConfig, oldConfig, modifiedValues )
+                ) );
+    }
+
+    interface DomainModificationCategorizer
+    {
+        Set<DomainID> categorize( AppConfig newConfig, AppConfig oldConfig, Set<StoredConfigKey> modifiedValues );
+    }
+
+    private static final Map<DomainModifyCategory, DomainModificationCategorizer> CATEGORIZERS = Map.of(
+            DomainModifyCategory.removed, new RemovalCategorizer(),
+            DomainModifyCategory.created, new CreationCategorizer(),
+            DomainModifyCategory.unchanged, new UnchangedCategorizer(),
+            DomainModifyCategory.modified, new ModifiedCategorizer() );
+
+    private static class RemovalCategorizer implements DomainModificationCategorizer
+    {
+        @Override
+        public Set<DomainID> categorize( final AppConfig newConfig, final AppConfig oldConfig, final Set<StoredConfigKey> modifiedValues )
+        {
+            final Set<DomainID> removedDomains = new HashSet<>( oldConfig.getDomainConfigs().keySet() );
+            removedDomains.removeAll( newConfig.getDomainConfigs().keySet() );
+            return CollectionUtil.stripNulls( removedDomains );
+        }
+    }
+
+    private static class CreationCategorizer implements DomainModificationCategorizer
+    {
+        @Override
+        public Set<DomainID> categorize( final AppConfig newConfig, final AppConfig oldConfig, final Set<StoredConfigKey> modifiedValues )
         {
         {
             final Set<DomainID> createdDomains = new HashSet<>( newConfig.getDomainConfigs().keySet() );
             final Set<DomainID> createdDomains = new HashSet<>( newConfig.getDomainConfigs().keySet() );
             createdDomains.removeAll( oldConfig.getDomainConfigs().keySet() );
             createdDomains.removeAll( oldConfig.getDomainConfigs().keySet() );
-            types.put( DomainModifyCategory.created, CollectionUtil.stripNulls( createdDomains ) );
+            return CollectionUtil.stripNulls( createdDomains );
         }
         }
+    }
 
 
-        final Set<DomainID> unchangedDomains = new HashSet<>();
-        final Set<DomainID> modifiedDomains = new HashSet<>();
-        for ( final DomainID domainID : newConfig.getDomainConfigs().keySet() )
+    private static class UnchangedCategorizer implements DomainModificationCategorizer
+    {
+        @Override
+        public Set<DomainID> categorize( final AppConfig newConfig, final AppConfig oldConfig, final Set<StoredConfigKey> modifiedValues )
         {
         {
-            final DomainConfig newDomainConfig = newConfig.getDomainConfigs().get( domainID );
-            final DomainConfig oldDomainConfig = oldConfig.getDomainConfigs().get( newDomainConfig.getDomainID() );
+            final Set<DomainID> persistentDomains = new HashSet<>( CollectionUtil.setUnion( newConfig.getDomainConfigs().keySet(), oldConfig.getDomainConfigs().keySet() ) );
+            persistentDomains.removeAll( StoredConfigKey.uniqueDomains( modifiedValues ) );
+            return Set.copyOf( persistentDomains );
+        }
+    }
 
 
-            if ( newDomainConfig != null && oldDomainConfig != null && Objects.equals( oldDomainConfig.getValueHash(), newDomainConfig.getValueHash() ) )
-            {
-                unchangedDomains.add( domainID );
-            }
-            else
-            {
-                modifiedDomains.add( domainID );
-            }
+    private static class ModifiedCategorizer implements DomainModificationCategorizer
+    {
+        @Override
+        public Set<DomainID> categorize( final AppConfig newConfig, final AppConfig oldConfig, final Set<StoredConfigKey> modifiedValues )
+        {
+            final Set<DomainID> persistentDomains = new HashSet<>( CollectionUtil.setUnion( newConfig.getDomainConfigs().keySet(), oldConfig.getDomainConfigs().keySet() ) );
+            persistentDomains.retainAll( StoredConfigKey.uniqueDomains( modifiedValues ) );
+            return Set.copyOf( persistentDomains );
         }
         }
-        types.put( DomainModifyCategory.unchanged, CollectionUtil.stripNulls( unchangedDomains ) );
-        types.put( DomainModifyCategory.modified, CollectionUtil.stripNulls( modifiedDomains ) );
-        return Collections.unmodifiableMap( types );
     }
     }
+
+
 }
 }

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

@@ -57,6 +57,8 @@ public class PwmEnvironment
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmEnvironment.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmEnvironment.class );
 
 
+    private static final SessionLabel SESSION_LABEL = SessionLabel.SYSTEM_LABEL;
+
     @lombok.Builder.Default
     @lombok.Builder.Default
     private PwmApplicationMode applicationMode = PwmApplicationMode.ERROR;
     private PwmApplicationMode applicationMode = PwmApplicationMode.ERROR;
 
 
@@ -72,7 +74,7 @@ public class PwmEnvironment
     @Singular
     @Singular
     private Map<ApplicationParameter, String> parameters;
     private Map<ApplicationParameter, String> parameters;
 
 
-    private final LazySupplier<DeploymentPlatform> deploymentPlatformLazySupplier = new LazySupplier<>( this::determineDeploymentPlatform );
+    private final LazySupplier<DeploymentPlatform> deploymentPlatformLazySupplier = LazySupplier.create( this::determineDeploymentPlatform );
 
 
     public enum ApplicationParameter
     public enum ApplicationParameter
     {
     {
@@ -179,7 +181,7 @@ public class PwmEnvironment
         }
         }
         if ( applicationPathIsWebInfPath )
         if ( applicationPathIsWebInfPath )
         {
         {
-            LOGGER.trace( SessionLabel.SYSTEM_LABEL, () -> "applicationPath appears to be servlet /WEB-INF directory" );
+            LOGGER.trace( SESSION_LABEL, () -> "applicationPath appears to be servlet /WEB-INF directory" );
         }
         }
     }
     }
 
 
@@ -207,7 +209,7 @@ public class PwmEnvironment
             );
             );
         }
         }
 
 
-        LOGGER.trace( SessionLabel.SYSTEM_LABEL, () -> "examining applicationPath of " + applicationPath.getAbsolutePath() + "" );
+        LOGGER.trace( SESSION_LABEL, () -> "examining applicationPath of " + applicationPath.getAbsolutePath() + "" );
 
 
         if ( !applicationPath.exists() )
         if ( !applicationPath.exists() )
         {
         {
@@ -234,7 +236,7 @@ public class PwmEnvironment
         }
         }
 
 
         final File infoFile = new File( applicationPath.getAbsolutePath() + File.separator + PwmConstants.APPLICATION_PATH_INFO_FILE );
         final File infoFile = new File( applicationPath.getAbsolutePath() + File.separator + PwmConstants.APPLICATION_PATH_INFO_FILE );
-        LOGGER.trace( SessionLabel.SYSTEM_LABEL, () -> "checking " + infoFile.getAbsolutePath() + " status" );
+        LOGGER.trace( SESSION_LABEL, () -> "checking " + infoFile.getAbsolutePath() + " status" );
         if ( infoFile.exists() )
         if ( infoFile.exists() )
         {
         {
             final String errorMsg = "The file " + infoFile.getAbsolutePath() + " exists, and an applicationPath was not explicitly specified."
             final String errorMsg = "The file " + infoFile.getAbsolutePath() + " exists, and an applicationPath was not explicitly specified."
@@ -321,7 +323,7 @@ public class PwmEnvironment
                 }
                 }
                 else
                 else
                 {
                 {
-                    LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "unknown " + EnvironmentParameter.applicationFlags + " value: " + input );
+                    LOGGER.warn( SESSION_LABEL, () -> "unknown " + EnvironmentParameter.applicationFlags + " value: " + input );
                 }
                 }
             }
             }
             return returnFlags;
             return returnFlags;
@@ -341,7 +343,7 @@ public class PwmEnvironment
             }
             }
             catch ( final Exception e )
             catch ( final Exception e )
             {
             {
-                LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "error reading properties file '" + input + "' specified by environment setting "
+                LOGGER.warn( SESSION_LABEL, () -> "error reading properties file '" + input + "' specified by environment setting "
                         + EnvironmentParameter.applicationParamFile + ", error: " + e.getMessage() );
                         + EnvironmentParameter.applicationParamFile + ", error: " + e.getMessage() );
             }
             }
 
 
@@ -358,14 +360,14 @@ public class PwmEnvironment
                     }
                     }
                     else
                     else
                     {
                     {
-                        LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "unknown " + EnvironmentParameter.applicationParamFile + " value: " + input );
+                        LOGGER.warn( SESSION_LABEL, () -> "unknown " + EnvironmentParameter.applicationParamFile + " value: " + input );
                     }
                     }
                 }
                 }
                 return Collections.unmodifiableMap( returnParams );
                 return Collections.unmodifiableMap( returnParams );
             }
             }
             catch ( final Exception e )
             catch ( final Exception e )
             {
             {
-                LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "unable to parse jason value of " + EnvironmentParameter.applicationParamFile + ", error: " + e.getMessage() );
+                LOGGER.warn( SESSION_LABEL, () -> "unable to parse jason value of " + EnvironmentParameter.applicationParamFile + ", error: " + e.getMessage() );
             }
             }
 
 
             return Collections.emptyMap();
             return Collections.emptyMap();
@@ -376,7 +378,7 @@ public class PwmEnvironment
     {
     {
         if ( PwmConstants.TRIAL_MODE && mode == PwmApplicationMode.RUNNING )
         if ( PwmConstants.TRIAL_MODE && mode == PwmApplicationMode.RUNNING )
         {
         {
-            LOGGER.info( SessionLabel.SYSTEM_LABEL, () -> "application is in trial mode" );
+            LOGGER.info( SESSION_LABEL, () -> "application is in trial mode" );
             return PwmApplicationMode.CONFIGURATION;
             return PwmApplicationMode.CONFIGURATION;
         }
         }
 
 

+ 44 - 19
server/src/main/java/password/pwm/bean/DomainID.java

@@ -20,23 +20,27 @@
 
 
 package password.pwm.bean;
 package password.pwm.bean;
 
 
-import password.pwm.config.PwmSetting;
+import password.pwm.PwmConstants;
 import password.pwm.config.PwmSettingScope;
 import password.pwm.config.PwmSettingScope;
-import password.pwm.config.value.StringValue;
-import password.pwm.util.java.MiscUtil;
+import password.pwm.util.java.PwmUtil;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.Comparator;
 import java.util.List;
 import java.util.List;
 import java.util.Objects;
 import java.util.Objects;
+import java.util.Optional;
+import java.util.regex.Pattern;
 
 
-public class DomainID implements Comparable<DomainID>, Serializable
+public final class DomainID implements Comparable<DomainID>, Serializable
 {
 {
-    public static final List<String> DOMAIN_RESERVED_WORDS = List.of( "system", "private", "public", "pwm", "sspr", "domain", "profile", "password" );
-    public static final DomainID DOMAIN_ID_DEFAULT = create( "default" );
+    private static final Pattern REGEX_TEST = Pattern.compile( "^([a-z][a-z0-9]{2,10})$" );
+    private static final List<String> DOMAIN_RESERVED_WORDS = List.of( "system", "private", "public", "pwm", "sspr", "domain", "profile", "password" );
 
 
-    private static final String SYSTEM_ID = "system";
-    private static final DomainID SYSTEM_DOMAIN_ID = new DomainID( SYSTEM_ID );
+    public static final DomainID DOMAIN_ID_DEFAULT = new DomainID( "default" );
+    private static final DomainID SYSTEM_DOMAIN_ID = new DomainID( "system" );
+
+    private static final List<DomainID> BUILT_IN = List.of( SYSTEM_DOMAIN_ID, DOMAIN_ID_DEFAULT );
 
 
     // sort placing 'system' first then alphabetically.
     // sort placing 'system' first then alphabetically.
     private static final Comparator<DomainID> COMPARATOR = Comparator.comparing( DomainID::isSystem )
     private static final Comparator<DomainID> COMPARATOR = Comparator.comparing( DomainID::isSystem )
@@ -52,14 +56,9 @@ public class DomainID implements Comparable<DomainID>, Serializable
 
 
     public static DomainID create( final String domainID )
     public static DomainID create( final String domainID )
     {
     {
-        Objects.requireNonNull( domainID );
-        
-        final List<String> errorMessages = StringValue.validateValue( PwmSetting.DOMAIN_LIST, domainID );
-        if ( !errorMessages.isEmpty() )
-        {
-            throw new IllegalArgumentException( "domainID value '" + domainID + "' does not match required syntax pattern for user defined domains: " + errorMessages.get( 0 ) );
-        }
-        return new DomainID( domainID );
+        return BUILT_IN.stream()
+                .filter( d -> d.domainID.equals( domainID ) )
+                .findFirst().orElse( new DomainID( domainID ) );
     }
     }
 
 
     public boolean inScope( final PwmSettingScope scope )
     public boolean inScope( final PwmSettingScope scope )
@@ -73,7 +72,7 @@ public class DomainID implements Comparable<DomainID>, Serializable
                 return !this.isSystem();
                 return !this.isSystem();
 
 
             default:
             default:
-                MiscUtil.unhandledSwitchStatement( scope );
+                PwmUtil.unhandledSwitchStatement( scope );
         }
         }
 
 
         return false;
         return false;
@@ -97,7 +96,7 @@ public class DomainID implements Comparable<DomainID>, Serializable
     @Override
     @Override
     public int hashCode()
     public int hashCode()
     {
     {
-        return Objects.hash( domainID );
+        return Objects.hashCode( domainID );
     }
     }
 
 
     @Override
     @Override
@@ -124,6 +123,32 @@ public class DomainID implements Comparable<DomainID>, Serializable
 
 
     public boolean isSystem()
     public boolean isSystem()
     {
     {
-        return SYSTEM_ID.equals( domainID );
+        return SYSTEM_DOMAIN_ID.domainID.equals( domainID );
+    }
+
+    public static Comparator<DomainID> comparator()
+    {
+        return COMPARATOR;
+    }
+
+    public static List<String> validateUserValue( final String value )
+    {
+        Objects.requireNonNull( value );
+        final String lCaseValue = value.toLowerCase( PwmConstants.DEFAULT_LOCALE );
+        final Optional<String> reservedWordMatch = DomainID.DOMAIN_RESERVED_WORDS.stream()
+                .map( String::toLowerCase )
+                .filter( lCaseValue::contains )
+                .findFirst();
+        if ( reservedWordMatch.isPresent() )
+        {
+            return Collections.singletonList( "contains reserved word '" + reservedWordMatch.get() + "'" );
+        }
+
+        if ( !REGEX_TEST.matcher( value ).matches() )
+        {
+            return Collections.singletonList( "pattern is invalid" );
+        }
+
+        return Collections.emptyList();
     }
     }
 }
 }

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

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

+ 139 - 0
server/src/main/java/password/pwm/bean/ProfileID.java

@@ -0,0 +1,139 @@
+/*
+ * 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.jetbrains.annotations.NotNull;
+import password.pwm.PwmConstants;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+public final class ProfileID implements Serializable, Comparable<ProfileID>
+{
+    private static final Pattern REGEX_TEST = Pattern.compile( "^([a-zA-Z][a-zA-Z0-9-]{2,15})$" );
+
+    private static final List<String> PROFILE_RESERVED_WORDS = List.of( "all", "nmas" );
+
+    private static final Comparator<ProfileID> COMPARATOR = Comparator.nullsFirst( Comparator.comparing( k -> k.profileID ) );
+    private static final Comparator<String> STRING_COMPARATOR = Comparator.nullsFirst( Comparator.comparing( Function.identity() ) );
+
+    public static final ProfileID PROFILE_ID_DEFAULT = new ProfileID( "default" );
+    public static final ProfileID PROFILE_ID_ALL = new ProfileID( "all" );
+    public static final ProfileID PROFILE_ID_NMAS = new ProfileID( "nmas" );
+
+    private static final List<ProfileID> BUILT_IN_PROFILES = List.of( PROFILE_ID_DEFAULT, PROFILE_ID_ALL, PROFILE_ID_NMAS );
+    private final String profileID;
+
+    private ProfileID( final String profileID )
+    {
+        this.profileID = JavaHelper.requireNonEmpty( profileID );
+    }
+
+    public static ProfileID create( final String profileID )
+    {
+        return BUILT_IN_PROFILES.stream()
+                .filter( p -> p.profileID.equals( profileID ) )
+                .findFirst()
+                .orElse( new ProfileID( profileID ) );
+    }
+
+    public static Optional<ProfileID> createNullable( final String profileID )
+    {
+        return StringUtil.isEmpty( profileID ) ? Optional.empty() : Optional.of( create( profileID ) );
+    }
+
+    @Override
+    public boolean equals( final Object o )
+    {
+        if ( this == o )
+        {
+            return true;
+        }
+        if ( o == null || getClass() != o.getClass() )
+        {
+            return false;
+        }
+        final ProfileID profileID1 = ( ProfileID ) o;
+        return Objects.equals( profileID, profileID1.profileID );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hashCode( profileID );
+    }
+
+    @Override
+    public String toString()
+    {
+        return profileID;
+    }
+
+    public String stringValue()
+    {
+        return profileID;
+    }
+
+    @Override
+    public int compareTo( @NotNull final ProfileID o )
+    {
+        return COMPARATOR.compare( this, o );
+    }
+
+    public static Comparator<ProfileID> comparator()
+    {
+        return COMPARATOR;
+    }
+
+    public static Comparator<String> stringComparator()
+    {
+        return STRING_COMPARATOR;
+    }
+
+    public static List<String> validateUserValue( final String value )
+    {
+        Objects.requireNonNull( value );
+        final String lCaseValue = value.toLowerCase( PwmConstants.DEFAULT_LOCALE );
+        final Optional<String> reservedWordMatch = PROFILE_RESERVED_WORDS.stream()
+                .map( String::toLowerCase )
+                .filter( lCaseValue::contains )
+                .findFirst();
+        if ( reservedWordMatch.isPresent() )
+        {
+            return Collections.singletonList( "contains reserved word '" + reservedWordMatch.get() + "'" );
+        }
+
+        if ( REGEX_TEST.matcher( value ).matches() )
+        {
+            return Collections.singletonList( "pattern is invalid" );
+        }
+
+        return Collections.emptyList();
+    }
+}

+ 163 - 18
server/src/main/java/password/pwm/bean/SessionLabel.java

@@ -20,48 +20,179 @@
 
 
 package password.pwm.bean;
 package password.pwm.bean;
 
 
+import lombok.AccessLevel;
 import lombok.Builder;
 import lombok.Builder;
 import lombok.Value;
 import lombok.Value;
-import password.pwm.PwmConstants;
+import password.pwm.PwmApplication;
+import password.pwm.PwmEnvironment;
+import password.pwm.config.AppConfig;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestUtil;
+import password.pwm.http.PwmSession;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmService;
+import password.pwm.user.UserInfo;
+import password.pwm.util.java.AtomicLoopLongIncrementer;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogEvent;
+import password.pwm.util.logging.PwmLogger;
 
 
+import javax.servlet.http.HttpServletRequest;
 import java.io.Serializable;
 import java.io.Serializable;
+import java.util.Objects;
 
 
 @Value
 @Value
-@Builder( toBuilder = true )
+@Builder( toBuilder = true, access = AccessLevel.PRIVATE )
+/**
+ * Increasingly miss-named data class that represents request/operation actor and origin data.
+ */
 public class SessionLabel implements Serializable
 public class SessionLabel implements Serializable
 {
 {
-    private static final String SYSTEM_LABEL_SESSION_ID = "#";
-    private static final String RUNTIME_LABEL_SESSION_ID = "#";
+    private static final PwmLogger LOGGER = PwmLogger.forClass( SessionLabel.class );
 
 
-    public static final SessionLabel SYSTEM_LABEL = SessionLabel.builder().sessionID( SYSTEM_LABEL_SESSION_ID ).username( PwmConstants.PWM_APP_NAME ).build();
-    public static final SessionLabel RUNTIME_LABEL = SessionLabel.builder().sessionID( RUNTIME_LABEL_SESSION_ID ).username( "internal" ).build();
-    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 static final String SYSTEM_LABEL_SESSION_ID = "#";
+    private static final String RUNTIME_LABEL_SESSION_ID = "!";
+    private static final String HEALTH_LABEL_SESSION_ID = "H";
 
 
+    public static final SessionLabel SYSTEM_LABEL = SessionLabel.forNonUserType( ActorType.system, DomainID.systemId() );
+    public static final SessionLabel HEALTH_LABEL = SessionLabel.forNonUserType( ActorType.health, DomainID.systemId() );
+    public static final SessionLabel TEST_SESSION_LABEL = SessionLabel.forNonUserType( ActorType.test, DomainID.systemId() );
+    public static final SessionLabel CLI_SESSION_LABEL = SessionLabel.forNonUserType( ActorType.cli, DomainID.systemId() );
+    public static final SessionLabel CONTEXT_SESSION_LABEL = SessionLabel.forNonUserType( ActorType.context, DomainID.systemId() );
+    public static final SessionLabel ONEJAR_LABEL = SessionLabel.forNonUserType( ActorType.onejar, DomainID.systemId() );
 
 
     private final String sessionID;
     private final String sessionID;
     private final String requestID;
     private final String requestID;
-    private final String userID;
     private final String username;
     private final String username;
     private final String sourceAddress;
     private final String sourceAddress;
     private final String sourceHostname;
     private final String sourceHostname;
     private final String profile;
     private final String profile;
     private final String domain;
     private final String domain;
+    private final ActorType actorType;
 
 
-    public static SessionLabel forPwmService( final PwmService pwmService, final DomainID domainID )
+    public enum ActorType
     {
     {
+        user( null ),
+        system( "#" ),
+        runtime( "!" ),
+        health( "-HEALTH" ),
+        test( "-TEST" ),
+        cli( "-CLI" ),
+        onejar( "-ONEJAR" ),
+        context( "-CONTEXT" ),
+        rest( null ),;
+
+        private final String defaultSessionId;
+
+        ActorType( final String defaultSessionId )
+        {
+            this.defaultSessionId = defaultSessionId;
+        }
+
+        public String defaultSessionId()
+        {
+            return defaultSessionId;
+        }
+    }
+
+    private static SessionLabel forNonUserType( final ActorType actorType, final DomainID domainID )
+    {
+        Objects.requireNonNull( actorType );
+
+        final String sessionID = actorType.defaultSessionId();
+        final String domainSting = domainID == null ? DomainID.systemId().stringValue() : domainID.stringValue();
+
         return SessionLabel.builder()
         return SessionLabel.builder()
-                .sessionID( SYSTEM_LABEL_SESSION_ID )
+                .actorType( actorType )
+                .domain( domainSting )
+                .sessionID( sessionID )
+                .username( actorType.name() ).build();
+    }
+
+    public static SessionLabel forRestRequest(
+            final PwmApplication pwmApplication,
+            final HttpServletRequest req,
+            final AtomicLoopLongIncrementer requestCounter,
+            final DomainID domainID
+    )
+    {
+        final String id = "rest-" + requestCounter.next();
+
+        return SessionLabel.forNonUserType( ActorType.rest, domainID ).toBuilder()
+                .sessionID( id )
+                .requestID( id )
+                .sourceAddress( PwmRequestUtil.readUserNetworkAddress( req, pwmApplication.getConfig() ).orElse( "" ) )
+                .sourceHostname( PwmRequestUtil.readUserHostname( req, pwmApplication.getConfig() ).orElse( "" ) )
+                .build();
+    }
+
+
+    public static SessionLabel forSystem( final PwmEnvironment pwmEnvironment, final DomainID domainID )
+    {
+        return forNonUserType( pwmEnvironment != null && pwmEnvironment.isInternalRuntimeInstance()
+                ? SessionLabel.ActorType.runtime
+                : SessionLabel.ActorType.system, domainID );
+    }
+
+    public static SessionLabel forPwmService( final PwmService pwmService, final DomainID domainID )
+    {
+        return forNonUserType( ActorType.system, domainID ).toBuilder()
                 .username( pwmService.getClass().getSimpleName() )
                 .username( pwmService.getClass().getSimpleName() )
                 .domain( domainID.stringValue() )
                 .domain( domainID.stringValue() )
                 .build();
                 .build();
     }
     }
 
 
-    public String toDebugLabel( )
+    public static SessionLabel forPwmRequest( final PwmRequest pwmRequest )
+    {
+        final SessionLabel.SessionLabelBuilder builder = SessionLabel.builder();
+
+        builder.actorType( ActorType.user );
+        builder.sourceAddress( pwmRequest.getSrcAddress().orElse( null ) );
+        builder.sourceHostname( pwmRequest.getSrcHostname().orElse( null ) );
+        builder.requestID( pwmRequest.getPwmRequestID() );
+        builder.domain( pwmRequest.getDomainID().stringValue() );
+
+        if ( pwmRequest.hasSession() )
+        {
+            final PwmSession pwmSession = pwmRequest.getPwmSession();
+            builder.sessionID( pwmSession.getSessionStateBean().getSessionID() );
+
+            if ( pwmRequest.isAuthenticated() )
+            {
+                try
+                {
+                    final UserInfo userInfo = pwmSession.getUserInfo();
+                    final UserIdentity userIdentity = userInfo.getUserIdentity();
+
+                    builder.username( userInfo.getUsername() );
+                    builder.profile( userIdentity == null ? null : userIdentity.getLdapProfileID().stringValue() );
+                }
+                catch ( final PwmUnrecoverableException e )
+                {
+                    LOGGER.error( () -> "unexpected error reading username: " + e.getMessage(), e );
+                }
+            }
+        }
+        else
+        {
+            builder.sessionID( "-" );
+        }
+
+        return builder.build();
+    }
+
+    public static SessionLabel fromPwmLogEvent( final PwmLogEvent pwmLogEvent )
+    {
+        return SessionLabel.builder()
+                .sessionID( pwmLogEvent.getSessionID() )
+                .requestID( pwmLogEvent.getRequestID() )
+                .username( pwmLogEvent.getUsername() )
+                .sourceAddress( pwmLogEvent.getSourceAddress() )
+                .domain( pwmLogEvent.getDomain() )
+                .build();
+    }
+
+    public String toDebugLabel( final AppConfig appConfig )
     {
     {
         final StringBuilder sb = new StringBuilder();
         final StringBuilder sb = new StringBuilder();
         final String sessionID = getSessionID();
         final String sessionID = getSessionID();
@@ -71,15 +202,20 @@ public class SessionLabel implements Serializable
         {
         {
             sb.append( sessionID );
             sb.append( sessionID );
         }
         }
+
         if ( StringUtil.notEmpty( domain ) )
         if ( StringUtil.notEmpty( domain ) )
         {
         {
-            if ( sb.length() > 0 )
+            if ( appConfig == null || appConfig.getDomainConfigs().size() > 1 )
             {
             {
-                sb.append( ',' );
+                if ( sb.length() > 0 )
+                {
+                    sb.append( ',' );
+                }
+                sb.append( domain );
             }
             }
-            sb.append( domain );
         }
         }
-        if ( StringUtil.notEmpty( username ) )
+
+        if ( actorType == ActorType.user && StringUtil.notEmpty( username ) )
         {
         {
             if ( sb.length() > 0 )
             if ( sb.length() > 0 )
             {
             {
@@ -97,4 +233,13 @@ public class SessionLabel implements Serializable
         return sb.toString();
         return sb.toString();
     }
     }
 
 
+    public boolean isRuntime()
+    {
+        return this.actorType == ActorType.runtime;
+    }
+
+    public boolean isHealth()
+    {
+        return this.actorType == ActorType.health;
+    }
 }
 }

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

@@ -21,22 +21,18 @@
 package password.pwm.bean;
 package password.pwm.bean;
 
 
 import com.novell.ldapchai.ChaiUser;
 import com.novell.ldapchai.ChaiUser;
-import com.novell.ldapchai.exception.ChaiException;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
+import password.pwm.PwmDomain;
 import password.pwm.config.AppConfig;
 import password.pwm.config.AppConfig;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.svc.cache.CacheKey;
-import password.pwm.svc.cache.CachePolicy;
-import password.pwm.svc.cache.CacheService;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.util.json.JsonFactory;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
@@ -50,7 +46,6 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
     private static final PwmLogger LOGGER = PwmLogger.forClass( UserIdentity.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( UserIdentity.class );
     private static final long serialVersionUID = 1L;
     private static final long serialVersionUID = 1L;
 
 
-    private static final String CRYPO_HEADER = "ui_C-";
     private static final String DELIM_SEPARATOR = "|";
     private static final String DELIM_SEPARATOR = "|";
 
 
     private static final Comparator<UserIdentity> COMPARATOR = Comparator.comparing(
     private static final Comparator<UserIdentity> COMPARATOR = Comparator.comparing(
@@ -58,16 +53,16 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
             Comparator.nullsLast( Comparator.naturalOrder() ) )
             Comparator.nullsLast( Comparator.naturalOrder() ) )
             .thenComparing(
             .thenComparing(
                     UserIdentity::getLdapProfileID,
                     UserIdentity::getLdapProfileID,
-                    Comparator.nullsLast( Comparator.naturalOrder() ) )
+                    ProfileID.comparator()
+            )
             .thenComparing(
             .thenComparing(
                     UserIdentity::getDomainID,
                     UserIdentity::getDomainID,
-                    Comparator.nullsLast( Comparator.naturalOrder() ) );
+                    DomainID.comparator() );
 
 
-    private transient String obfuscatedValue;
     private transient boolean canonical;
     private transient boolean canonical;
 
 
     private final String userDN;
     private final String userDN;
-    private final String ldapProfile;
+    private final ProfileID ldapProfile;
     private final DomainID domainID;
     private final DomainID domainID;
 
 
     public enum Flag
     public enum Flag
@@ -75,14 +70,14 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         PreCanonicalized,
         PreCanonicalized,
     }
     }
 
 
-    private UserIdentity( final String userDN, final String ldapProfile, final DomainID domainID )
+    private UserIdentity( final String userDN, final ProfileID ldapProfile, final DomainID domainID )
     {
     {
         this.userDN = JavaHelper.requireNonEmpty( userDN, "UserIdentity: userDN value cannot be empty" );
         this.userDN = JavaHelper.requireNonEmpty( userDN, "UserIdentity: userDN value cannot be empty" );
-        this.ldapProfile = JavaHelper.requireNonEmpty( ldapProfile, "UserIdentity: ldapProfile value cannot be empty" );
+        this.ldapProfile = Objects.requireNonNull( ldapProfile, "UserIdentity: ldapProfile value cannot be empty" );
         this.domainID = Objects.requireNonNull( domainID );
         this.domainID = Objects.requireNonNull( domainID );
     }
     }
 
 
-    public UserIdentity( final String userDN, final String ldapProfile, final DomainID domainID, final boolean canonical )
+    public UserIdentity( final String userDN, final ProfileID ldapProfile, final DomainID domainID, final boolean canonical )
     {
     {
         this( userDN, ldapProfile, domainID );
         this( userDN, ldapProfile, domainID );
         this.canonical = canonical;
         this.canonical = canonical;
@@ -90,12 +85,12 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
 
 
     public static UserIdentity create(
     public static UserIdentity create(
             final String userDN,
             final String userDN,
-            final String ldapProfile,
+            final ProfileID ldapProfile,
             final DomainID domainID,
             final DomainID domainID,
             final Flag... flags
             final Flag... flags
     )
     )
     {
     {
-        final boolean canonical = JavaHelper.enumArrayContainsValue( flags, Flag.PreCanonicalized );
+        final boolean canonical = EnumUtil.enumArrayContainsValue( flags, Flag.PreCanonicalized );
         return new UserIdentity( userDN, ldapProfile, domainID, canonical );
         return new UserIdentity( userDN, ldapProfile, domainID, canonical );
     }
     }
 
 
@@ -109,7 +104,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         return domainID;
         return domainID;
     }
     }
 
 
-    public String getLdapProfileID( )
+    public ProfileID getLdapProfileID( )
     {
     {
         return ldapProfile;
         return ldapProfile;
     }
     }
@@ -130,41 +125,6 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         return toDisplayString();
         return toDisplayString();
     }
     }
 
 
-    public String toObfuscatedKey( final PwmApplication pwmApplication )
-            throws PwmUnrecoverableException
-    {
-        // use local cache first.
-        if ( StringUtil.notEmpty( obfuscatedValue ) )
-        {
-            return obfuscatedValue;
-        }
-
-        // check app cache.  This is used primarily so that keys are static over some meaningful lifetime, allowing browser caching based on keys.
-        final CacheService cacheService = pwmApplication.getCacheService();
-        final CacheKey cacheKey = CacheKey.newKey( this.getClass(), this, "obfuscatedKey" );
-        final String cachedValue = cacheService.get( cacheKey, String.class );
-
-        if ( StringUtil.notEmpty( cachedValue ) )
-        {
-            obfuscatedValue = cachedValue;
-            return cachedValue;
-        }
-
-        // generate key
-        try
-        {
-            final String jsonValue = JsonFactory.get().serialize( this );
-            final String localValue = CRYPO_HEADER + pwmApplication.getSecureService().encryptToString( jsonValue );
-            this.obfuscatedValue = localValue;
-            cacheService.put( cacheKey, CachePolicy.makePolicyWithExpiration( TimeDuration.DAY ), localValue );
-            return localValue;
-        }
-        catch ( final Exception e )
-        {
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "unexpected error making obfuscated user key: " + e.getMessage() ) );
-        }
-    }
-
     public String toDelimitedKey( )
     public String toDelimitedKey( )
     {
     {
         return JsonFactory.get().serialize( this );
         return JsonFactory.get().serialize( this );
@@ -174,30 +134,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
     {
     {
         return "[" + this.getDomainID() + "]"
         return "[" + this.getDomainID() + "]"
                 + " " + this.getUserDN()
                 + " " + this.getUserDN()
-                + ( ( this.getLdapProfileID() != null && !this.getLdapProfileID().isEmpty() ) ? " (" + this.getLdapProfileID() + ")" : "" );
-    }
-
-    public static UserIdentity fromObfuscatedKey( final String key, final PwmApplication pwmApplication )
-            throws PwmUnrecoverableException
-    {
-        Objects.requireNonNull( pwmApplication );
-        JavaHelper.requireNonEmpty( key, "key can not be null or empty" );
-
-        if ( !key.startsWith( CRYPO_HEADER ) )
-        {
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "cannot reverse obfuscated user key: missing header; value=" + key ) );
-        }
-
-        try
-        {
-            final String input = key.substring( CRYPO_HEADER.length() );
-            final String jsonValue = pwmApplication.getSecureService().decryptStringValue( input );
-            return JsonFactory.get().deserialize( jsonValue, UserIdentity.class );
-        }
-        catch ( final Exception e )
-        {
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "unexpected error reversing obfuscated user key: " + e.getMessage() ) );
-        }
+                + " (" + this.getLdapProfileID().stringValue() + ")";
     }
     }
 
 
     public static UserIdentity fromDelimitedKey( final SessionLabel sessionLabel, final String key )
     public static UserIdentity fromDelimitedKey( final SessionLabel sessionLabel, final String key )
@@ -242,30 +179,11 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         {
         {
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "too many string tokens while parsing delimited identity key" ) );
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "too many string tokens while parsing delimited identity key" ) );
         }
         }
-        final String profileID = st.nextToken();
+        final ProfileID profileID = ProfileID.create( st.nextToken() );
         final String userDN = st.nextToken();
         final String userDN = st.nextToken();
         return create( userDN, profileID, domainID );
         return create( userDN, profileID, domainID );
     }
     }
 
 
-    /**
-     * Attempt to de-serialize value using delimited or obfuscated key.
-     *
-     * @deprecated  Should be used by calling {@link #fromDelimitedKey(String)} or {@link #fromObfuscatedKey(String, PwmApplication)}.
-     */
-    @Deprecated
-    public static UserIdentity fromKey( final SessionLabel sessionLabel, final String key, final PwmApplication pwmApplication )
-            throws PwmUnrecoverableException
-    {
-        JavaHelper.requireNonEmpty( key );
-
-        if ( key.startsWith( CRYPO_HEADER ) )
-        {
-            return fromObfuscatedKey( key, pwmApplication );
-        }
-
-        return fromDelimitedKey( sessionLabel, key );
-    }
-
     public boolean canonicalEquals( final SessionLabel sessionLabel, final UserIdentity otherIdentity, final PwmApplication pwmApplication )
     public boolean canonicalEquals( final SessionLabel sessionLabel, final UserIdentity otherIdentity, final PwmApplication pwmApplication )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
@@ -317,17 +235,9 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         }
         }
 
 
         final ChaiUser chaiUser = pwmApplication.domains().get( this.getDomainID() ).getProxiedChaiUser( sessionLabel, this );
         final ChaiUser chaiUser = pwmApplication.domains().get( this.getDomainID() ).getProxiedChaiUser( sessionLabel, this );
-        final String userDN;
-        try
-        {
-            userDN = chaiUser.readCanonicalDN();
-        }
-        catch ( final ChaiException e )
-        {
-            throw PwmUnrecoverableException.fromChaiException( e );
-        }
-        final UserIdentity canonicalziedIdentity = create( userDN, this.getLdapProfileID(), this.getDomainID() );
-        canonicalziedIdentity.canonical = true;
-        return canonicalziedIdentity;
+        final LdapProfile ldapProfile = getLdapProfile( pwmApplication.getConfig() );
+        final PwmDomain domain = pwmApplication.domains().get( domainID );
+        final String userDN = ldapProfile.readCanonicalDN( sessionLabel, domain, chaiUser.getEntryDN() );
+        return create( userDN, this.getLdapProfileID(), this.getDomainID(), Flag.PreCanonicalized );
     }
     }
 }
 }

+ 40 - 69
server/src/main/java/password/pwm/config/AppConfig.java

@@ -24,7 +24,7 @@ import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.bean.PrivateKeyCertificate;
-import password.pwm.bean.SessionLabel;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.option.CertificateMatchingMode;
 import password.pwm.config.option.CertificateMatchingMode;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.profile.EmailServerProfile;
 import password.pwm.config.profile.EmailServerProfile;
@@ -41,30 +41,26 @@ import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.CollectorUtil;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
-import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
 
 
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.EnumMap;
 import java.util.EnumMap;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
-import java.util.TreeMap;
+import java.util.SortedMap;
 import java.util.TreeSet;
 import java.util.TreeSet;
 import java.util.function.Function;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
 
 
 public class AppConfig implements SettingReader
 public class AppConfig implements SettingReader
 {
 {
@@ -79,13 +75,13 @@ public class AppConfig implements SettingReader
     private final Map<AppProperty, String> appPropertyOverrides;
     private final Map<AppProperty, String> appPropertyOverrides;
     private final Map<Locale, String> localeFlagMap;
     private final Map<Locale, String> localeFlagMap;
 
 
-    private static final Supplier<AppConfig> DEFAULT_CONFIG = new LazySupplier<>( AppConfig::makeDefaultConfig );
+    private static final Supplier<AppConfig> DEFAULT_CONFIG = LazySupplier.create( AppConfig::makeDefaultConfig );
 
 
     private static AppConfig makeDefaultConfig()
     private static AppConfig makeDefaultConfig()
     {
     {
         try
         try
         {
         {
-            return new AppConfig( StoredConfigurationFactory.newConfig() );
+            return forStoredConfig( StoredConfigurationFactory.newConfig() );
         }
         }
         catch ( final PwmUnrecoverableException e )
         catch ( final PwmUnrecoverableException e )
         {
         {
@@ -98,7 +94,7 @@ public class AppConfig implements SettingReader
         return DEFAULT_CONFIG.get();
         return DEFAULT_CONFIG.get();
     }
     }
 
 
-    public AppConfig( final StoredConfiguration storedConfiguration )
+    private AppConfig( final StoredConfiguration storedConfiguration )
     {
     {
         this.storedConfiguration = storedConfiguration;
         this.storedConfiguration = storedConfiguration;
         this.settingReader = new StoredSettingReader( storedConfiguration, null, DomainID.systemId() );
         this.settingReader = new StoredSettingReader( storedConfiguration, null, DomainID.systemId() );
@@ -108,14 +104,17 @@ public class AppConfig implements SettingReader
 
 
         this.localeFlagMap = makeLocaleFlagMap( this );
         this.localeFlagMap = makeLocaleFlagMap( this );
 
 
-        this.domainIDs = Collections.unmodifiableSet(  new TreeSet<>(
-                settingReader.readSettingAsStringArray( PwmSetting.DOMAIN_LIST ).stream()
-                .collect( Collectors.toUnmodifiableSet() ) ) );
+        this.domainIDs = Set.copyOf( new TreeSet<>( settingReader.readSettingAsStringArray( PwmSetting.DOMAIN_LIST ) ) );
 
 
-        this.domainConfigMap = Collections.unmodifiableMap(  domainIDs.stream()
-                .collect( CollectionUtil.collectorToLinkedMap(
+        this.domainConfigMap = domainIDs.stream()
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
                         DomainID::create,
                         DomainID::create,
-                        ( domainID ) -> new DomainConfig( this, DomainID.create( domainID ) ) ) ) );
+                        ( domainID ) -> new DomainConfig( this, DomainID.create( domainID ) ) ) );
+    }
+
+    public static AppConfig forStoredConfig( final StoredConfiguration storedConfiguration )
+    {
+        return new AppConfig( storedConfiguration );
     }
     }
 
 
     public Set<String> getDomainIDs()
     public Set<String> getDomainIDs()
@@ -186,12 +185,11 @@ public class AppConfig implements SettingReader
 
 
     public Map<AppProperty, String> readAllAppProperties()
     public Map<AppProperty, String> readAllAppProperties()
     {
     {
-          return Collections.unmodifiableMap( CollectionUtil.enumStream( AppProperty.class )
-                  .collect( CollectionUtil.collectorToLinkedMap(
-                          Function.identity(),
-                          this::readAppProperty
-                  ) ) );
-
+        return EnumUtil.enumStream( AppProperty.class )
+                .collect( CollectorUtil.toLinkedMap(
+                        Function.identity(),
+                        this::readAppProperty
+                ) );
     }
     }
 
 
     public StoredConfiguration getStoredConfiguration()
     public StoredConfiguration getStoredConfiguration()
@@ -222,11 +220,6 @@ public class AppConfig implements SettingReader
         return settingReader.readSettingAsStringArray( pwmSetting );
         return settingReader.readSettingAsStringArray( pwmSetting );
     }
     }
 
 
-    public PwmLogLevel getEventLogLocalDBLevel()
-    {
-        return readSettingAsEnum( PwmSetting.EVENTS_LOCALDB_LOG_LEVEL, PwmLogLevel.class );
-    }
-
     public boolean isDevDebugMode()
     public boolean isDevDebugMode()
     {
     {
         return Boolean.parseBoolean( readAppProperty( AppProperty.LOGGING_DEV_OUTPUT ) );
         return Boolean.parseBoolean( readAppProperty( AppProperty.LOGGING_DEV_OUTPUT ) );
@@ -301,7 +294,7 @@ public class AppConfig implements SettingReader
         return settingReader.readGenericStorageLocations( setting );
         return settingReader.readGenericStorageLocations( setting );
     }
     }
 
 
-    public Map<String, EmailServerProfile> getEmailServerProfiles( )
+    public Map<ProfileID, EmailServerProfile> getEmailServerProfiles( )
     {
     {
         return settingReader.getProfileMap( ProfileDefinition.EmailServers );
         return settingReader.getProfileMap( ProfileDefinition.EmailServers );
     }
     }
@@ -335,8 +328,9 @@ public class AppConfig implements SettingReader
 
 
     private static Map<AppProperty, String> makeAppPropertyOverrides( final SettingReader settingReader )
     private static Map<AppProperty, String> makeAppPropertyOverrides( final SettingReader settingReader )
     {
     {
-        final Map<String, String> stringMap =  StringUtil.convertStringListToNameValuePair(
-                settingReader.readSettingAsStringArray( PwmSetting.APP_PROPERTY_OVERRIDES ), "=" );
+        final List<String> settingValues = settingReader.readSettingAsStringArray( PwmSetting.APP_PROPERTY_OVERRIDES );
+
+        final Map<String, String> stringMap =  StringUtil.convertStringListToNameValuePair( settingValues, "=" );
 
 
         final Map<AppProperty, String> appPropertyMap = new EnumMap<>( AppProperty.class );
         final Map<AppProperty, String> appPropertyMap = new EnumMap<>( AppProperty.class );
         for ( final Map.Entry<String, String> stringEntry : stringMap.entrySet() )
         for ( final Map.Entry<String, String> stringEntry : stringMap.entrySet() )
@@ -344,16 +338,14 @@ public class AppConfig implements SettingReader
             AppProperty.forKey( stringEntry.getKey() )
             AppProperty.forKey( stringEntry.getKey() )
                     .ifPresent( appProperty ->
                     .ifPresent( appProperty ->
                     {
                     {
-                       final String defaultValue = appProperty.getDefaultValue();
-                       final String value = stringEntry.getValue();
-                       if ( !Objects.equals( defaultValue, value ) )
-                       {
-                           appPropertyMap.put( appProperty, value );
-                       }
+                        if ( !appProperty.isDefaultValue( stringEntry.getValue() ) )
+                        {
+                            appPropertyMap.put( appProperty, stringEntry.getValue() );
+                        }
                     } );
                     } );
         }
         }
 
 
-        return Collections.unmodifiableMap( appPropertyMap );
+        return CollectionUtil.unmodifiableEnumMap( appPropertyMap, AppProperty.class );
     }
     }
 
 
     public boolean isSmsConfigured()
     public boolean isSmsConfigured()
@@ -361,12 +353,13 @@ public class AppConfig implements SettingReader
         final String gatewayUrl = readSettingAsString( PwmSetting.SMS_GATEWAY_URL );
         final String gatewayUrl = readSettingAsString( PwmSetting.SMS_GATEWAY_URL );
         final String gatewayUser = readSettingAsString( PwmSetting.SMS_GATEWAY_USER );
         final String gatewayUser = readSettingAsString( PwmSetting.SMS_GATEWAY_USER );
         final PasswordData gatewayPass = readSettingAsPassword( PwmSetting.SMS_GATEWAY_PASSWORD );
         final PasswordData gatewayPass = readSettingAsPassword( PwmSetting.SMS_GATEWAY_PASSWORD );
-        if ( gatewayUrl == null || gatewayUrl.length() < 1 )
+
+        if ( StringUtil.isEmpty( gatewayUrl ) )
         {
         {
             return false;
             return false;
         }
         }
 
 
-        if ( gatewayUser != null && gatewayUser.length() > 0 && ( gatewayPass == null ) )
+        if ( !StringUtil.isEmpty( gatewayUser ) && gatewayPass == null )
         {
         {
             return false;
             return false;
         }
         }
@@ -385,7 +378,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 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 );
                 final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
-                LOGGER.warn( SessionLabel.SYSTEM_LABEL, errorInfo::toDebugStr );
+                LOGGER.warn( errorInfo::toDebugStr );
                 return new PwmSecurityKey( PwmRandom.getInstance().alphaNumericString( 1024 ) );
                 return new PwmSecurityKey( PwmRandom.getInstance().alphaNumericString( 1024 ) );
             }
             }
             else
             else
@@ -417,38 +410,16 @@ public class AppConfig implements SettingReader
         }
         }
     }
     }
 
 
-    private static Map<Locale, String> makeLocaleFlagMap( final AppConfig appConfig )
+    private static SortedMap<Locale, String> makeLocaleFlagMap( final AppConfig appConfig )
     {
     {
-        final String defaultLocaleAsString = PwmConstants.DEFAULT_LOCALE.toString();
-
         final List<String> inputList = appConfig.readSettingAsStringArray( PwmSetting.KNOWN_LOCALES );
         final List<String> inputList = appConfig.readSettingAsStringArray( PwmSetting.KNOWN_LOCALES );
         final Map<String, String> inputMap = StringUtil.convertStringListToNameValuePair( inputList, "::" );
         final Map<String, String> inputMap = StringUtil.convertStringListToNameValuePair( inputList, "::" );
 
 
-        final Map<String, String> sortedMap = new TreeMap<>( inputMap.keySet().stream()
-                .collect( Collectors.toMap(
-                        str -> LocaleHelper.parseLocaleString( str ).getDisplayName(),
-                        Function.identity()
-                ) ) );
-
-        final List<String> returnList = new ArrayList<>( sortedMap.size() + 1 );
-
-        //ensure default is first.
-        returnList.add( defaultLocaleAsString );
-        returnList.addAll( sortedMap.values().stream()
-                .filter( str -> !Objects.equals( defaultLocaleAsString, str ) )
-                .collect( Collectors.toList() ) );
-
-        final Map<Locale, String> localeFlagMap = new LinkedHashMap<>( returnList.size() );
-        for ( final String localeString : returnList )
-        {
-            final Locale loopLocale = LocaleHelper.parseLocaleString( localeString );
-            if ( loopLocale != null )
-            {
-                final String flagCode = inputMap.getOrDefault( localeString, loopLocale.getCountry() );
-                localeFlagMap.put( loopLocale, flagCode );
-            }
-        }
-        return Collections.unmodifiableMap( localeFlagMap );
+        return inputMap.keySet().stream()
+                .collect( CollectorUtil.toUnmodifiableSortedMap(
+                        LocaleHelper::parseLocaleString,
+                        s -> inputMap.getOrDefault( s, LocaleHelper.parseLocaleString( s ).getCountry() ),
+                        LocaleHelper.localeDisplayComparator() ) );
     }
     }
 
 
     @Override
     @Override

+ 64 - 42
server/src/main/java/password/pwm/config/DomainConfig.java

@@ -24,6 +24,7 @@ import password.pwm.AppProperty;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.bean.PrivateKeyCertificate;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.option.TokenStorageMethod;
 import password.pwm.config.option.TokenStorageMethod;
 import password.pwm.config.profile.ActivateUserProfile;
 import password.pwm.config.profile.ActivateUserProfile;
@@ -53,8 +54,8 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.CollectionUtil;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.java.CollectorUtil;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
 
 
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
@@ -74,15 +75,13 @@ import java.util.stream.Collectors;
  */
  */
 public class DomainConfig implements SettingReader
 public class DomainConfig implements SettingReader
 {
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( DomainConfig.class );
-
     private final StoredConfiguration storedConfiguration;
     private final StoredConfiguration storedConfiguration;
     private final AppConfig appConfig;
     private final AppConfig appConfig;
     private final DomainID domainID;
     private final DomainID domainID;
 
 
-    private final Map<String, PwmPasswordPolicy> cachedPasswordPolicy;
-    private final Map<String, Map<Locale, ChallengeProfile>> cachedChallengeProfiles;
-    private final Map<String, LdapProfile> ldapProfiles;
+    private final Map<ProfileID, PwmPasswordPolicy> cachedPasswordPolicy;
+    private final Map<ProfileID, Map<Locale, ChallengeProfile>> cachedChallengeProfiles;
+    private final Map<ProfileID, LdapProfile> ldapProfiles;
     private final StoredSettingReader settingReader;
     private final StoredSettingReader settingReader;
     private final PwmSecurityKey domainSecurityKey;
     private final PwmSecurityKey domainSecurityKey;
 
 
@@ -93,22 +92,22 @@ public class DomainConfig implements SettingReader
         this.domainID = Objects.requireNonNull( domainID );
         this.domainID = Objects.requireNonNull( domainID );
         this.settingReader = new StoredSettingReader( storedConfiguration, null, domainID );
         this.settingReader = new StoredSettingReader( storedConfiguration, null, domainID );
 
 
-        this.cachedPasswordPolicy = Collections.unmodifiableMap( getPasswordProfileIDs().stream()
+        this.cachedPasswordPolicy = getPasswordProfileIDs().stream()
                 .map( profile -> PwmPasswordPolicy.createPwmPasswordPolicy( this, profile ) )
                 .map( profile -> PwmPasswordPolicy.createPwmPasswordPolicy( this, profile ) )
-                .collect( Collectors.toMap(
-                        PwmPasswordPolicy::getIdentifier,
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
+                        PwmPasswordPolicy::getId,
                         Function.identity()
                         Function.identity()
-                ) ) );
+                ) );
 
 
-        this.cachedChallengeProfiles = Collections.unmodifiableMap( getChallengeProfileIDs().stream()
-                .collect( Collectors.toMap(
+        this.cachedChallengeProfiles = getChallengeProfileIDs().stream()
+                .collect( Collectors.toUnmodifiableMap(
                         Function.identity(),
                         Function.identity(),
-                        profileId -> Collections.unmodifiableMap( appConfig.getKnownLocales().stream()
-                                .collect( Collectors.toMap(
+                        profileId -> appConfig.getKnownLocales().stream()
+                                .collect( CollectorUtil.toUnmodifiableLinkedMap(
                                         Function.identity(),
                                         Function.identity(),
                                         locale -> ChallengeProfile.readChallengeProfileFromConfig( domainID, profileId, locale, storedConfiguration )
                                         locale -> ChallengeProfile.readChallengeProfileFromConfig( domainID, profileId, locale, storedConfiguration )
-                                ) ) )
-                ) ) );
+                                ) )
+                ) );
 
 
         this.ldapProfiles = makeLdapProfileMap( this );
         this.ldapProfiles = makeLdapProfileMap( this );
         this.domainSecurityKey = makeDomainSecurityKey( appConfig, settingReader.getValueHash() );
         this.domainSecurityKey = makeDomainSecurityKey( appConfig, settingReader.getValueHash() );
@@ -135,7 +134,7 @@ public class DomainConfig implements SettingReader
         return settingReader.readSettingAsUserPermission( setting );
         return settingReader.readSettingAsUserPermission( setting );
     }
     }
 
 
-    public Map<String, LdapProfile> getLdapProfiles( )
+    public Map<ProfileID, LdapProfile> getLdapProfiles( )
     {
     {
         return ldapProfiles;
         return ldapProfiles;
     }
     }
@@ -192,12 +191,12 @@ public class DomainConfig implements SettingReader
         return settingReader.readLocalizedBundle( className, keyName );
         return settingReader.readLocalizedBundle( className, keyName );
     }
     }
 
 
-    public List<String> getChallengeProfileIDs( )
+    public List<ProfileID> getChallengeProfileIDs( )
     {
     {
         return StoredConfigurationUtil.profilesForSetting( this.getDomainID(), PwmSetting.CHALLENGE_PROFILE_LIST, storedConfiguration );
         return StoredConfigurationUtil.profilesForSetting( this.getDomainID(), PwmSetting.CHALLENGE_PROFILE_LIST, storedConfiguration );
     }
     }
 
 
-    public ChallengeProfile getChallengeProfile( final String profile, final Locale locale )
+    public ChallengeProfile getChallengeProfile( final ProfileID profile, final Locale locale )
     {
     {
         final Map<Locale, ChallengeProfile> cachedLocaleMap = cachedChallengeProfiles.get( profile );
         final Map<Locale, ChallengeProfile> cachedLocaleMap = cachedChallengeProfiles.get( profile );
 
 
@@ -214,12 +213,12 @@ public class DomainConfig implements SettingReader
         return settingReader.readSettingAsLong( setting );
         return settingReader.readSettingAsLong( setting );
     }
     }
 
 
-    public PwmPasswordPolicy getPasswordPolicy( final String profile )
+    public PwmPasswordPolicy getPasswordPolicy( final ProfileID profile )
     {
     {
         return cachedPasswordPolicy.get( profile );
         return cachedPasswordPolicy.get( profile );
     }
     }
 
 
-    public List<String> getPasswordProfileIDs( )
+    public List<ProfileID> getPasswordProfileIDs( )
     {
     {
         return StoredConfigurationUtil.profilesForSetting( this.getDomainID(), PwmSetting.PASSWORD_PROFILE_LIST, storedConfiguration );
         return StoredConfigurationUtil.profilesForSetting( this.getDomainID(), PwmSetting.PASSWORD_PROFILE_LIST, storedConfiguration );
     }
     }
@@ -271,7 +270,7 @@ public class DomainConfig implements SettingReader
 
 
     public Optional<TokenStorageMethod> getTokenStorageMethod( )
     public Optional<TokenStorageMethod> getTokenStorageMethod( )
     {
     {
-        return JavaHelper.readEnumFromString( TokenStorageMethod.class, readSettingAsString( PwmSetting.TOKEN_STORAGEMETHOD ) );
+        return EnumUtil.readEnumFromString( TokenStorageMethod.class, readSettingAsString( PwmSetting.TOKEN_STORAGEMETHOD ) );
     }
     }
 
 
     public PwmSettingTemplateSet getTemplate( )
     public PwmSettingTemplateSet getTemplate( )
@@ -290,52 +289,52 @@ public class DomainConfig implements SettingReader
     }
     }
 
 
     /* generic profile stuff */
     /* generic profile stuff */
-    public Map<String, NewUserProfile> getNewUserProfiles( )
+    public Map<ProfileID, NewUserProfile> getNewUserProfiles( )
     {
     {
         return this.getProfileMap( ProfileDefinition.NewUser );
         return this.getProfileMap( ProfileDefinition.NewUser );
     }
     }
 
 
-    public Map<String, ActivateUserProfile> getUserActivationProfiles( )
+    public Map<ProfileID, ActivateUserProfile> getUserActivationProfiles( )
     {
     {
         return this.getProfileMap( ProfileDefinition.ActivateUser );
         return this.getProfileMap( ProfileDefinition.ActivateUser );
     }
     }
 
 
-    public Map<String, HelpdeskProfile> getHelpdeskProfiles( )
+    public Map<ProfileID, HelpdeskProfile> getHelpdeskProfiles( )
     {
     {
         return this.getProfileMap( ProfileDefinition.Helpdesk );
         return this.getProfileMap( ProfileDefinition.Helpdesk );
     }
     }
 
 
-    public Map<String, PeopleSearchProfile> getPeopleSearchProfiles( )
+    public Map<ProfileID, PeopleSearchProfile> getPeopleSearchProfiles( )
     {
     {
         return this.getProfileMap( ProfileDefinition.PeopleSearch );
         return this.getProfileMap( ProfileDefinition.PeopleSearch );
     }
     }
 
 
-    public Map<String, SetupOtpProfile> getSetupOTPProfiles( )
+    public Map<ProfileID, SetupOtpProfile> getSetupOTPProfiles( )
     {
     {
         return this.getProfileMap( ProfileDefinition.SetupOTPProfile );
         return this.getProfileMap( ProfileDefinition.SetupOTPProfile );
     }
     }
 
 
-    public Map<String, SetupResponsesProfile> getSetupResponseProfiles( )
+    public Map<ProfileID, SetupResponsesProfile> getSetupResponseProfiles( )
     {
     {
         return this.getProfileMap( ProfileDefinition.SetupResponsesProfile );
         return this.getProfileMap( ProfileDefinition.SetupResponsesProfile );
     }
     }
 
 
-    public Map<String, UpdateProfileProfile> getUpdateAttributesProfile( )
+    public Map<ProfileID, UpdateProfileProfile> getUpdateAttributesProfile( )
     {
     {
         return this.getProfileMap( ProfileDefinition.UpdateAttributes );
         return this.getProfileMap( ProfileDefinition.UpdateAttributes );
     }
     }
 
 
-    public Map<String, ChangePasswordProfile> getChangePasswordProfile( )
+    public Map<ProfileID, ChangePasswordProfile> getChangePasswordProfile( )
     {
     {
         return this.getProfileMap( ProfileDefinition.ChangePassword );
         return this.getProfileMap( ProfileDefinition.ChangePassword );
     }
     }
 
 
-    public Map<String, ForgottenPasswordProfile> getForgottenPasswordProfiles( )
+    public Map<ProfileID, ForgottenPasswordProfile> getForgottenPasswordProfiles( )
     {
     {
         return this.getProfileMap( ProfileDefinition.ForgottenPassword );
         return this.getProfileMap( ProfileDefinition.ForgottenPassword );
     }
     }
 
 
-    public <T extends Profile> Map<String, T> getProfileMap( final ProfileDefinition profileDefinition )
+    public <T extends Profile> Map<ProfileID, T> getProfileMap( final ProfileDefinition profileDefinition )
     {
     {
         return settingReader.getProfileMap( profileDefinition );
         return settingReader.getProfileMap( profileDefinition );
     }
     }
@@ -347,11 +346,14 @@ public class DomainConfig implements SettingReader
 
 
     public Optional<PeopleSearchProfile> getPublicPeopleSearchProfile()
     public Optional<PeopleSearchProfile> getPublicPeopleSearchProfile()
     {
     {
-        if ( readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_PUBLIC ) )
+        final Map<ProfileID, Profile> profileMap = settingReader.getProfileMap( ProfileDefinition.PeopleSearch );
+        if ( !CollectionUtil.isEmpty( profileMap ) && readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_PUBLIC ) )
         {
         {
-            final String profileID = readSettingAsString( PwmSetting.PEOPLE_SEARCH_PUBLIC_PROFILE );
-            final Map<String, PeopleSearchProfile> profiles = settingReader.getProfileMap( ProfileDefinition.PeopleSearchPublic );
-            return Optional.ofNullable( profiles.get( profileID ) );
+            final Optional<ProfileID> profileID = profileForStringId( ProfileDefinition.PeopleSearch, readSettingAsString( PwmSetting.PEOPLE_SEARCH_PUBLIC_PROFILE ) );
+            if ( profileID.isPresent() )
+            {
+                return Optional.ofNullable( ( PeopleSearchProfile ) profileMap.get( profileID.get() ) );
+            }
         }
         }
         return Optional.empty();
         return Optional.empty();
     }
     }
@@ -397,16 +399,16 @@ public class DomainConfig implements SettingReader
     }
     }
 
 
 
 
-    private static Map<String, LdapProfile> makeLdapProfileMap( final DomainConfig domainConfig )
+    private static Map<ProfileID, LdapProfile> makeLdapProfileMap( final DomainConfig domainConfig )
     {
     {
-        final Map<String, LdapProfile> sourceMap = domainConfig.getProfileMap( ProfileDefinition.LdapProfile );
+        final Map<ProfileID, LdapProfile> sourceMap = domainConfig.getProfileMap( ProfileDefinition.LdapProfile );
 
 
-        return Collections.unmodifiableMap( sourceMap.entrySet()
+        return sourceMap.entrySet()
                 .stream()
                 .stream()
                 .filter( entry -> entry.getValue().isEnabled() )
                 .filter( entry -> entry.getValue().isEnabled() )
-                .collect( CollectionUtil.collectorToLinkedMap(
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
                         Map.Entry::getKey,
                         Map.Entry::getKey,
-                        Map.Entry::getValue ) ) );
+                        Map.Entry::getValue ) );
     }
     }
 
 
     private static PwmSecurityKey makeDomainSecurityKey(
     private static PwmSecurityKey makeDomainSecurityKey(
@@ -430,4 +432,24 @@ public class DomainConfig implements SettingReader
     {
     {
         return settingReader.getValueHash();
         return settingReader.getValueHash();
     }
     }
+
+    public Optional<ProfileID> ldapProfileForStringId( final String input )
+    {
+        return profileForStringId( ProfileDefinition.LdapProfile, input );
+    }
+
+    public Optional<ProfileID> profileForStringId( final ProfileDefinition profileDefinition, final String input )
+    {
+        final Map<ProfileID,  Profile> map = getProfileMap( profileDefinition );
+        if ( map != null )
+        {
+            return map.keySet().stream()
+                    .filter( profileID -> profileID.stringValue().equals( input ) )
+                    .findFirst();
+
+        }
+        return Optional.empty();
+
+    }
+
 }
 }

+ 18 - 11
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -22,6 +22,7 @@ package password.pwm.config;
 
 
 import org.jrivard.xmlchai.XmlElement;
 import org.jrivard.xmlchai.XmlElement;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.ValueFactory;
 import password.pwm.config.value.ValueFactory;
@@ -71,7 +72,7 @@ public enum PwmSetting
     DOMAIN_SYSTEM_ADMIN(
     DOMAIN_SYSTEM_ADMIN(
             "domain.system.adminDomain", PwmSettingSyntax.STRING, PwmSettingCategory.DOMAINS ),
             "domain.system.adminDomain", PwmSettingSyntax.STRING, PwmSettingCategory.DOMAINS ),
     DOMAIN_DOMAIN_PATHS(
     DOMAIN_DOMAIN_PATHS(
-            "domain.system.domainPaths", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.DOMAINS ),
+            "domain.system.domainPathsEnabled", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.DOMAINS ),
 
 
     // application settings
     // application settings
     APP_PROPERTY_OVERRIDES(
     APP_PROPERTY_OVERRIDES(
@@ -480,8 +481,6 @@ public enum PwmSetting
             "wordlistCaseSensitive", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.WORDLISTS ),
             "wordlistCaseSensitive", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.WORDLISTS ),
     PASSWORD_WORDLIST_WORDSIZE(
     PASSWORD_WORDLIST_WORDSIZE(
             "password.wordlist.wordSize", PwmSettingSyntax.NUMERIC, PwmSettingCategory.WORDLISTS ),
             "password.wordlist.wordSize", PwmSettingSyntax.NUMERIC, PwmSettingCategory.WORDLISTS ),
-    SEEDLIST_FILENAME(
-            "pwm.seedlist.location", PwmSettingSyntax.STRING, PwmSettingCategory.WORDLISTS ),
 
 
 
 
     // password policy profile settings
     // password policy profile settings
@@ -704,8 +703,6 @@ public enum PwmSetting
             "events.pwmDB.maxAge", PwmSettingSyntax.DURATION, PwmSettingCategory.LOGGING ),
             "events.pwmDB.maxAge", PwmSettingSyntax.DURATION, PwmSettingCategory.LOGGING ),
     EVENTS_ALERT_DAILY_SUMMARY(
     EVENTS_ALERT_DAILY_SUMMARY(
             "events.alert.dailySummary.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.LOGGING ),
             "events.alert.dailySummary.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.LOGGING ),
-    EVENTS_JAVA_LOG4JCONFIG_FILE(
-            "events.java.log4jconfigFile", PwmSettingSyntax.STRING, PwmSettingCategory.LOGGING ),
 
 
     PASSWORD_STRENGTH_METER_TYPE(
     PASSWORD_STRENGTH_METER_TYPE(
             "password.strengthMeter.type", PwmSettingSyntax.SELECT, PwmSettingCategory.LOGGING ),
             "password.strengthMeter.type", PwmSettingSyntax.SELECT, PwmSettingCategory.LOGGING ),
@@ -1277,6 +1274,16 @@ public enum PwmSetting
 
 
 
 
     // deprecated.
     // deprecated.
+
+
+    // deprecated 2022-09-04
+    EVENTS_JAVA_LOG4JCONFIG_FILE(
+            "events.java.log4jconfigFile", PwmSettingSyntax.STRING, PwmSettingCategory.LOGGING ),
+
+    // deprecated 2022-07-25
+    SEEDLIST_FILENAME(
+            "pwm.seedlist.location", PwmSettingSyntax.STRING, PwmSettingCategory.WORDLISTS ),
+
     // deprecated 2022-04-20
     // deprecated 2022-04-20
     IP_PERMITTED_RANGE(
     IP_PERMITTED_RANGE(
             "network.ip.permittedRange", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.WEB_SECURITY ),
             "network.ip.permittedRange", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.WEB_SECURITY ),
@@ -1333,13 +1340,13 @@ public enum PwmSetting
 
 
     private static final Map<PwmSetting, List<TemplateSetReference<StoredValue>>> DEFAULT_VALUE_CACHE = initDefaultValueCache();
     private static final Map<PwmSetting, List<TemplateSetReference<StoredValue>>> DEFAULT_VALUE_CACHE = initDefaultValueCache();
 
 
-    private final transient Supplier<String> defaultMenuLocation = new LazySupplier<>(
+    private final transient Supplier<String> defaultMenuLocation = LazySupplier.create(
             () -> readMenuLocationDebug( this, null, PwmConstants.DEFAULT_LOCALE ) );
             () -> readMenuLocationDebug( this, null, PwmConstants.DEFAULT_LOCALE ) );
 
 
-    private final transient Supplier<String> defaultLocaleLabel = new LazySupplier<>(
+    private final transient Supplier<String> defaultLocaleLabel = LazySupplier.create(
             () -> readLabel( this, PwmConstants.DEFAULT_LOCALE ) );
             () -> readLabel( this, PwmConstants.DEFAULT_LOCALE ) );
 
 
-    private final transient Supplier<String> defaultLocaleDescription = new LazySupplier<>(
+    private final transient Supplier<String> defaultLocaleDescription = LazySupplier.create(
             () -> readDescription( this, PwmConstants.DEFAULT_LOCALE ) );
             () -> readDescription( this, PwmConstants.DEFAULT_LOCALE ) );
 
 
 
 
@@ -1473,11 +1480,11 @@ public enum PwmSetting
     }
     }
 
 
     public String toMenuLocationDebug(
     public String toMenuLocationDebug(
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale
             final Locale locale
     )
     )
     {
     {
-        if ( PwmConstants.DEFAULT_LOCALE.equals( locale ) && StringUtil.isEmpty( profileID ) )
+        if ( PwmConstants.DEFAULT_LOCALE.equals( locale ) && profileID == null )
         {
         {
             return defaultMenuLocation.get();
             return defaultMenuLocation.get();
         }
         }
@@ -1544,7 +1551,7 @@ public enum PwmSetting
         return macroRequest.expandMacros( storedText );
         return macroRequest.expandMacros( storedText );
     }
     }
 
 
-    private static String readMenuLocationDebug( final PwmSetting pwmSetting, final String profileID, final Locale locale )
+    private static String readMenuLocationDebug( final PwmSetting pwmSetting, final ProfileID profileID, final Locale locale )
     {
     {
         final String separator = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
         final String separator = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
         return pwmSetting.getCategory().toMenuLocationDebug( profileID, locale ) + separator + pwmSetting.getLabel( locale );
         return pwmSetting.getCategory().toMenuLocationDebug( profileID, locale ) + separator + pwmSetting.getLabel( locale );

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

@@ -22,9 +22,11 @@ package password.pwm.config;
 
 
 import org.jrivard.xmlchai.XmlElement;
 import org.jrivard.xmlchai.XmlElement;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
+import password.pwm.bean.ProfileID;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.Config;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
-import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.CollectorUtil;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.macro.MacroRequest;
@@ -211,21 +213,21 @@ public enum PwmSettingCategory
     private static final Comparator<PwmSettingCategory> MENU_LOCATION_COMPARATOR = Comparator.comparing(
     private static final Comparator<PwmSettingCategory> MENU_LOCATION_COMPARATOR = Comparator.comparing(
             ( pwmSettingCategory ) -> pwmSettingCategory.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) );
             ( pwmSettingCategory ) -> pwmSettingCategory.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) );
 
 
-    private static final Supplier<List<PwmSettingCategory>> SORTED_VALUES = new LazySupplier<>( () -> Collections.unmodifiableList( Arrays.stream( values() )
+    private static final Supplier<List<PwmSettingCategory>> SORTED_VALUES = LazySupplier.create( () -> Collections.unmodifiableList( Arrays.stream( values() )
             .sorted( MENU_LOCATION_COMPARATOR )
             .sorted( MENU_LOCATION_COMPARATOR )
             .collect( Collectors.toList() ) ) );
             .collect( Collectors.toList() ) ) );
 
 
     private final PwmSettingCategory parent;
     private final PwmSettingCategory parent;
 
 
-    private final transient Supplier<Optional<PwmSetting>> profileSetting = new LazySupplier<>( () -> DataReader.readProfileSettingFromXml( this, true ) );
-    private final transient Supplier<Integer> level = new LazySupplier<>( () -> DataReader.readLevel( this ) );
-    private final transient Supplier<Boolean> hidden = new LazySupplier<>( () -> DataReader.readHidden( this ) );
-    private final transient Supplier<Boolean> isTopLevelProfile = new LazySupplier<>( () -> DataReader.readIsTopLevelProfile( this ) );
-    private final transient Supplier<String> defaultLocaleLabel = new LazySupplier<>( () -> DataReader.readLabel( this, PwmConstants.DEFAULT_LOCALE ) );
-    private final transient Supplier<String> defaultLocaleDescription = new LazySupplier<>( () -> DataReader.readDescription( this, PwmConstants.DEFAULT_LOCALE ) );
-    private final transient Supplier<PwmSettingScope> scope = new LazySupplier<>( () -> DataReader.readScope( this ) );
-    private final transient Supplier<Set<PwmSettingCategory>> children = new LazySupplier<>( () -> DataReader.readChildren( this ) );
-    private final transient Supplier<Set<PwmSetting>> settings = new LazySupplier<>( () -> DataReader.readSettings( this ) );
+    private final transient Supplier<Optional<PwmSetting>> profileSetting = LazySupplier.create( () -> DataReader.readProfileSettingFromXml( this, true ) );
+    private final transient Supplier<Integer> level = LazySupplier.create( () -> DataReader.readLevel( this ) );
+    private final transient Supplier<Boolean> hidden = LazySupplier.create( () -> DataReader.readHidden( this ) );
+    private final transient Supplier<Boolean> isTopLevelProfile = LazySupplier.create( () -> DataReader.readIsTopLevelProfile( this ) );
+    private final transient Supplier<String> defaultLocaleLabel = LazySupplier.create( () -> DataReader.readLabel( this, PwmConstants.DEFAULT_LOCALE ) );
+    private final transient Supplier<String> defaultLocaleDescription = LazySupplier.create( () -> DataReader.readDescription( this, PwmConstants.DEFAULT_LOCALE ) );
+    private final transient Supplier<PwmSettingScope> scope = LazySupplier.create( () -> DataReader.readScope( this ) );
+    private final transient Supplier<Set<PwmSettingCategory>> children = LazySupplier.create( () -> DataReader.readChildren( this ) );
+    private final transient Supplier<Set<PwmSetting>> settings = LazySupplier.create( () -> DataReader.readSettings( this ) );
 
 
     PwmSettingCategory( final PwmSettingCategory parent )
     PwmSettingCategory( final PwmSettingCategory parent )
     {
     {
@@ -313,7 +315,7 @@ public enum PwmSettingCategory
     }
     }
 
 
     public String toMenuLocationDebug(
     public String toMenuLocationDebug(
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale
             final Locale locale
     )
     )
     {
     {
@@ -322,7 +324,7 @@ public enum PwmSettingCategory
 
 
     private static String toMenuLocationDebugImpl(
     private static String toMenuLocationDebugImpl(
             final PwmSettingCategory category,
             final PwmSettingCategory category,
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale
             final Locale locale
     )
     )
     {
     {
@@ -455,7 +457,7 @@ public enum PwmSettingCategory
         private static PwmSettingScope readScope( final PwmSettingCategory category )
         private static PwmSettingScope readScope( final PwmSettingCategory category )
         {
         {
             final String attributeValue = readAttributeFromCategoryOrParent( category, PwmSettingXml.XML_ELEMENT_SCOPE );
             final String attributeValue = readAttributeFromCategoryOrParent( category, PwmSettingXml.XML_ELEMENT_SCOPE );
-            return JavaHelper.readEnumFromString( PwmSettingScope.class, attributeValue ).orElseThrow( () -> new IllegalStateException(
+            return EnumUtil.readEnumFromString( PwmSettingScope.class, attributeValue ).orElseThrow( () -> new IllegalStateException(
                     "unable to parse value for PwmSettingCategory '" + category + "' scope attribute" ) );
                     "unable to parse value for PwmSettingCategory '" + category + "' scope attribute" ) );
         }
         }
 
 
@@ -509,19 +511,17 @@ public enum PwmSettingCategory
 
 
         public static Set<PwmSettingCategory> readChildren( final PwmSettingCategory category )
         public static Set<PwmSettingCategory> readChildren( final PwmSettingCategory category )
         {
         {
-            final Set<PwmSettingCategory> categories = CollectionUtil.enumStream( PwmSettingCategory.class )
+            return EnumUtil.enumStream( PwmSettingCategory.class )
                     .filter( ( loopCategory ) -> loopCategory.getParent() == category )
                     .filter( ( loopCategory ) -> loopCategory.getParent() == category )
-                    .collect( Collectors.toUnmodifiableSet() );
-            return Collections.unmodifiableSet( CollectionUtil.copyToEnumSet( categories, PwmSettingCategory.class ) );
+                    .collect( CollectorUtil.toUnmodifiableEnumSet( PwmSettingCategory.class, s -> s ) );
         }
         }
 
 
         public static Set<PwmSetting> readSettings( final PwmSettingCategory category )
         public static Set<PwmSetting> readSettings( final PwmSettingCategory category )
         {
         {
-            final Set<PwmSetting> settings = EnumSet.allOf( PwmSetting.class )
+            return EnumSet.allOf( PwmSetting.class )
                     .stream()
                     .stream()
                     .filter( ( setting ) -> setting.getCategory() == category )
                     .filter( ( setting ) -> setting.getCategory() == category )
-                    .collect( Collectors.toSet() );
-            return Collections.unmodifiableSet( CollectionUtil.copyToEnumSet( settings, PwmSetting.class ) );
+                    .collect( CollectorUtil.toUnmodifiableEnumSet( PwmSetting.class, s -> s ) );
         }
         }
     }
     }
 }
 }

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

@@ -23,6 +23,7 @@ package password.pwm.config;
 import lombok.Builder;
 import lombok.Builder;
 import lombok.Value;
 import lombok.Value;
 import org.jrivard.xmlchai.XmlElement;
 import org.jrivard.xmlchai.XmlElement;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.macro.MacroRequest;
 
 
@@ -104,7 +105,7 @@ class PwmSettingMetaData
                     flagElement.getChildren( PwmSettingXml.XML_ELEMENT_FLAG ).forEach( flagsElement ->
                     flagElement.getChildren( PwmSettingXml.XML_ELEMENT_FLAG ).forEach( flagsElement ->
                     {
                     {
                         final String value = flagsElement.getText().orElse( "" ).trim();
                         final String value = flagsElement.getText().orElse( "" ).trim();
-                        JavaHelper.readEnumFromString( PwmSettingFlag.class, value ).ifPresent( returnObj::add );
+                        EnumUtil.readEnumFromString( PwmSettingFlag.class, value ).ifPresent( returnObj::add );
                     } )
                     } )
             );
             );
             return Collections.unmodifiableSet( returnObj );
             return Collections.unmodifiableSet( returnObj );
@@ -139,10 +140,10 @@ class PwmSettingMetaData
             {
             {
                 permissionElement.getChildren( PwmSettingXml.XML_ELEMENT_LDAP ).forEach( ldapElement ->
                 permissionElement.getChildren( PwmSettingXml.XML_ELEMENT_LDAP ).forEach( ldapElement ->
                 {
                 {
-                    final Optional<LDAPPermissionInfo.Actor> actor = JavaHelper.readEnumFromString(
+                    final Optional<LDAPPermissionInfo.Actor> actor = EnumUtil.readEnumFromString(
                             LDAPPermissionInfo.Actor.class,
                             LDAPPermissionInfo.Actor.class,
                             permissionElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACTOR ).orElse( "" ) );
                             permissionElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACTOR ).orElse( "" ) );
-                    final Optional<LDAPPermissionInfo.Access> type = JavaHelper.readEnumFromString(
+                    final Optional<LDAPPermissionInfo.Access> type = EnumUtil.readEnumFromString(
                             LDAPPermissionInfo.Access.class,
                             LDAPPermissionInfo.Access.class,
                             permissionElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACCESS ).orElse( "" ) );
                             permissionElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACCESS ).orElse( "" ) );
 
 
@@ -191,7 +192,7 @@ class PwmSettingMetaData
                         final String keyAttribute = propertyElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_KEY )
                         final String keyAttribute = propertyElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_KEY )
                                 .orElseThrow( () -> new IllegalStateException( "property element is missing 'key' attribute for value " + pwmSetting.getKey() ) );
                                 .orElseThrow( () -> new IllegalStateException( "property element is missing 'key' attribute for value " + pwmSetting.getKey() ) );
 
 
-                        final PwmSettingProperty property = JavaHelper.readEnumFromString( PwmSettingProperty.class, keyAttribute )
+                        final PwmSettingProperty property = EnumUtil.readEnumFromString( PwmSettingProperty.class, keyAttribute )
                                 .orElseThrow( () -> new IllegalStateException( "property element has unknown 'key' attribute for value " + pwmSetting.getKey() ) );
                                 .orElseThrow( () -> new IllegalStateException( "property element has unknown 'key' attribute for value " + pwmSetting.getKey() ) );
 
 
                         propertyElement.getText().ifPresent( value -> newProps.put( property, value ) );
                         propertyElement.getText().ifPresent( value -> newProps.put( property, value ) );

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

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

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

@@ -21,6 +21,7 @@
 package password.pwm.config;
 package password.pwm.config;
 
 
 import org.jrivard.xmlchai.XmlElement;
 import org.jrivard.xmlchai.XmlElement;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 
 
 import java.util.EnumMap;
 import java.util.EnumMap;
@@ -82,7 +83,7 @@ public enum PwmSettingTemplate
 
 
     public static Set<PwmSettingTemplate> valuesForType( final Type type )
     public static Set<PwmSettingTemplate> valuesForType( final Type type )
     {
     {
-        return JavaHelper.readEnumsFromPredicate( PwmSettingTemplate.class, t -> t.getType() == type );
+        return EnumUtil.readEnumsFromPredicate( PwmSettingTemplate.class, t -> t.getType() == type );
     }
     }
 
 
     public enum Type
     public enum Type

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

@@ -22,6 +22,7 @@ package password.pwm.config;
 
 
 import lombok.Value;
 import lombok.Value;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.EnumUtil;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.util.List;
 import java.util.List;
@@ -41,7 +42,7 @@ public class PwmSettingTemplateSet implements Serializable
                 .map( PwmSettingTemplate::getType )
                 .map( PwmSettingTemplate::getType )
                 .collect( Collectors.toSet() );
                 .collect( Collectors.toSet() );
 
 
-        workingSet.addAll( CollectionUtil.enumStream( PwmSettingTemplate.Type.class )
+        workingSet.addAll( EnumUtil.enumStream( PwmSettingTemplate.Type.class )
                 .filter( type -> !seenTypes.contains( type ) )
                 .filter( type -> !seenTypes.contains( type ) )
                 .map( PwmSettingTemplate.Type::getDefaultValue )
                 .map( PwmSettingTemplate.Type::getDefaultValue )
                 .collect( Collectors.toUnmodifiableSet( ) ) );
                 .collect( Collectors.toUnmodifiableSet( ) ) );
@@ -70,7 +71,7 @@ public class PwmSettingTemplateSet implements Serializable
      */
      */
     public static List<PwmSettingTemplateSet> allValues()
     public static List<PwmSettingTemplateSet> allValues()
     {
     {
-        return CollectionUtil.enumStream( PwmSettingTemplate.class )
+        return EnumUtil.enumStream( PwmSettingTemplate.class )
                 .map( pwmSettingTemplate -> new PwmSettingTemplateSet( Set.of( pwmSettingTemplate ) ) )
                 .map( pwmSettingTemplate -> new PwmSettingTemplateSet( Set.of( pwmSettingTemplate ) ) )
                 .collect( Collectors.toUnmodifiableList() );
                 .collect( Collectors.toUnmodifiableList() );
     }
     }

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

@@ -25,7 +25,7 @@ import org.jrivard.xmlchai.XmlChai;
 import org.jrivard.xmlchai.XmlDocument;
 import org.jrivard.xmlchai.XmlDocument;
 import org.jrivard.xmlchai.XmlElement;
 import org.jrivard.xmlchai.XmlElement;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.LazySoftReference;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
@@ -36,6 +36,9 @@ import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.List;
 import java.util.Set;
 import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicInteger;
 
 
 public class PwmSettingXml
 public class PwmSettingXml
@@ -69,7 +72,9 @@ public class PwmSettingXml
 
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSettingXml.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSettingXml.class );
 
 
-    private static final LazySoftReference<XmlDocument> XML_DOC_CACHE = new LazySoftReference<>( PwmSettingXml::readXml );
+    private static final LazySupplier<XmlDocument> XML_DOC_CACHE = LazySupplier.synchronizedSupplier(
+            LazySupplier.create( PwmSettingXml::readXml ) );
+
     private static final AtomicInteger LOAD_COUNTER = new AtomicInteger( 0 );
     private static final AtomicInteger LOAD_COUNTER = new AtomicInteger( 0 );
 
 
     private static XmlDocument readXml( )
     private static XmlDocument readXml( )
@@ -80,6 +85,10 @@ public class PwmSettingXml
             final XmlDocument newDoc = XmlChai.getFactory().parse( inputStream, AccessMode.IMMUTABLE );
             final XmlDocument newDoc = XmlChai.getFactory().parse( inputStream, AccessMode.IMMUTABLE );
             final TimeDuration parseDuration = TimeDuration.fromCurrent( startTime );
             final TimeDuration parseDuration = TimeDuration.fromCurrent( startTime );
             LOGGER.trace( () -> "parsed PwmSettingXml in " + parseDuration.asCompactString() + ", loads=" + LOAD_COUNTER.getAndIncrement() );
             LOGGER.trace( () -> "parsed PwmSettingXml in " + parseDuration.asCompactString() + ", loads=" + LOAD_COUNTER.getAndIncrement() );
+
+            final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
+            scheduledExecutorService.schedule( XML_DOC_CACHE::clear, 30, TimeUnit.SECONDS );
+
             return newDoc;
             return newDoc;
         }
         }
         catch ( final IOException e )
         catch ( final IOException e )

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

@@ -24,7 +24,7 @@ import password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.bean.PrivateKeyCertificate;
-import password.pwm.bean.SessionLabel;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.profile.Profile;
 import password.pwm.config.profile.Profile;
 import password.pwm.config.profile.ProfileDefinition;
 import password.pwm.config.profile.ProfileDefinition;
@@ -48,8 +48,9 @@ import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.CollectorUtil;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.PwmHashAlgorithm;
 
 
@@ -58,7 +59,6 @@ import java.security.MessageDigest;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Collections;
-import java.util.EnumMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
@@ -73,13 +73,13 @@ public class StoredSettingReader implements SettingReader
     private static final PwmLogger LOGGER = PwmLogger.forClass( StoredSettingReader.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( StoredSettingReader.class );
 
 
     private final StoredConfiguration storedConfiguration;
     private final StoredConfiguration storedConfiguration;
-    private final String profileID;
+    private final ProfileID profileID;
     private final DomainID domainID;
     private final DomainID domainID;
 
 
     private final Map<ProfileDefinition, Map> profileCache;
     private final Map<ProfileDefinition, Map> profileCache;
     private final String valueHash;
     private final String valueHash;
 
 
-    public StoredSettingReader( final StoredConfiguration storedConfiguration, final String profileID, final DomainID domainID )
+    public StoredSettingReader( final StoredConfiguration storedConfiguration, final ProfileID profileID, final DomainID domainID )
     {
     {
         this.storedConfiguration = Objects.requireNonNull( storedConfiguration );
         this.storedConfiguration = Objects.requireNonNull( storedConfiguration );
         this.profileID = profileID;
         this.profileID = profileID;
@@ -192,7 +192,7 @@ public class StoredSettingReader implements SettingReader
         final String input = readSettingAsString( setting );
         final String input = readSettingAsString( setting );
 
 
         return Arrays.stream( input.split( "-" ) )
         return Arrays.stream( input.split( "-" ) )
-                .map( s ->  JavaHelper.readEnumFromString( DataStorageMethod.class, s ) )
+                .map( s ->  EnumUtil.readEnumFromString( DataStorageMethod.class, s ) )
                 .flatMap( Optional::stream )
                 .flatMap( Optional::stream )
                 .collect( Collectors.toUnmodifiableList() );
                 .collect( Collectors.toUnmodifiableList() );
     }
     }
@@ -223,7 +223,7 @@ public class StoredSettingReader implements SettingReader
     }
     }
 
 
 
 
-    public <T extends Profile> Map<String, T> getProfileMap( final ProfileDefinition profileDefinition )
+    public <T extends Profile> Map<ProfileID, T> getProfileMap( final ProfileDefinition profileDefinition )
     {
     {
         if ( profileID != null )
         if ( profileID != null )
         {
         {
@@ -240,17 +240,15 @@ public class StoredSettingReader implements SettingReader
                 final DomainID domainID
                 final DomainID domainID
         )
         )
         {
         {
-            final Map<ProfileDefinition, Map<String, Profile>> returnMap = new EnumMap<>( ProfileDefinition.class );
-            returnMap.putAll( CollectionUtil.enumStream( ProfileDefinition.class )
+            return EnumUtil.enumStream( ProfileDefinition.class )
                     .filter( profileDefinition -> domainID.inScope( profileDefinition.getCategory().getScope() ) )
                     .filter( profileDefinition -> domainID.inScope( profileDefinition.getCategory().getScope() ) )
-                    .collect( CollectionUtil.collectorToLinkedMap(
+                    .collect( CollectorUtil.toUnmodifiableLinkedMap(
                             profileDefinition -> profileDefinition,
                             profileDefinition -> profileDefinition,
                             profileDefinition -> profileMap( profileDefinition, storedConfiguration, domainID )
                             profileDefinition -> profileMap( profileDefinition, storedConfiguration, domainID )
-                    ) ) );
-            return Collections.unmodifiableMap( returnMap );
+                    ) );
         }
         }
 
 
-        private static <T extends Profile> Map<String, T> profileMap(
+        private static <T extends Profile> Map<ProfileID, T> profileMap(
                 final ProfileDefinition profileDefinition,
                 final ProfileDefinition profileDefinition,
                 final StoredConfiguration storedConfiguration,
                 final StoredConfiguration storedConfiguration,
                 final DomainID domainID
                 final DomainID domainID
@@ -262,7 +260,7 @@ public class StoredSettingReader implements SettingReader
             }
             }
 
 
             return ProfileUtility.profileIDsForCategory( storedConfiguration, domainID, profileDefinition.getCategory() ).stream()
             return ProfileUtility.profileIDsForCategory( storedConfiguration, domainID, profileDefinition.getCategory() ).stream()
-                    .collect( CollectionUtil.collectorToLinkedMap(
+                    .collect( CollectorUtil.toUnmodifiableLinkedMap(
                             Function.identity(),
                             Function.identity(),
                             profileID -> newProfileForID( profileDefinition, storedConfiguration, domainID, profileID )
                             profileID -> newProfileForID( profileDefinition, storedConfiguration, domainID, profileID )
                     ) );
                     ) );
@@ -272,7 +270,7 @@ public class StoredSettingReader implements SettingReader
                 final ProfileDefinition profileDefinition,
                 final ProfileDefinition profileDefinition,
                 final StoredConfiguration storedConfiguration,
                 final StoredConfiguration storedConfiguration,
                 final DomainID domainID,
                 final DomainID domainID,
-                final String profileID
+                final ProfileID profileID
         )
         )
         {
         {
             Objects.requireNonNull( profileDefinition );
             Objects.requireNonNull( profileDefinition );
@@ -321,10 +319,10 @@ public class StoredSettingReader implements SettingReader
 
 
         if ( setting.getFlags().contains( PwmSettingFlag.Deprecated ) )
         if ( setting.getFlags().contains( PwmSettingFlag.Deprecated ) )
         {
         {
-            LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "attempt to read deprecated config setting: " + setting.toMenuLocationDebug( profileID, null ) );
+            LOGGER.warn( () -> "attempt to read deprecated config setting: " + setting.toMenuLocationDebug( profileID, null ) );
         }
         }
 
 
-        if ( StringUtil.isEmpty( profileID ) )
+        if ( profileID == null )
         {
         {
             if ( setting.getCategory().hasProfiles() )
             if ( setting.getCategory().hasProfiles() )
             {
             {

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

@@ -20,7 +20,7 @@
 
 
 package password.pwm.config.option;
 package password.pwm.config.option;
 
 
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.ws.server.RestAuthenticationType;
 import password.pwm.ws.server.RestAuthenticationType;
 
 
 import java.util.Arrays;
 import java.util.Arrays;
@@ -59,6 +59,6 @@ public enum WebServiceUsage
 
 
     public static Set<WebServiceUsage> forType( final RestAuthenticationType type )
     public static Set<WebServiceUsage> forType( final RestAuthenticationType type )
     {
     {
-        return JavaHelper.readEnumsFromPredicate( WebServiceUsage.class, webServiceUsage -> webServiceUsage.getTypes().contains( type ) );
+        return EnumUtil.readEnumsFromPredicate( WebServiceUsage.class, webServiceUsage -> webServiceUsage.getTypes().contains( type ) );
     }
     }
 }
 }

+ 11 - 9
server/src/main/java/password/pwm/config/profile/AbstractProfile.java

@@ -21,6 +21,7 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredSettingReader;
 import password.pwm.config.StoredSettingReader;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.option.IdentityVerificationMethod;
@@ -30,19 +31,20 @@ import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.EnumUtil;
 
 
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
 
 
 public abstract class AbstractProfile implements Profile
 public abstract class AbstractProfile implements Profile
 {
 {
-    private final String identifier;
+    private final ProfileID profileID;
     private final StoredConfiguration storedConfiguration;
     private final StoredConfiguration storedConfiguration;
     private final StoredSettingReader settingReader;
     private final StoredSettingReader settingReader;
 
 
@@ -53,23 +55,23 @@ public abstract class AbstractProfile implements Profile
         ATTRIBUTE,
         ATTRIBUTE,
     }
     }
 
 
-    AbstractProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedConfiguration )
+    AbstractProfile( final DomainID domainID, final ProfileID profileID, final StoredConfiguration storedConfiguration )
     {
     {
-        this.identifier = identifier;
+        this.profileID = Objects.requireNonNull( profileID );
         this.storedConfiguration = storedConfiguration;
         this.storedConfiguration = storedConfiguration;
-        this.settingReader = new StoredSettingReader( storedConfiguration, identifier, domainID );
+        this.settingReader = new StoredSettingReader( storedConfiguration, profileID, domainID );
     }
     }
 
 
     @Override
     @Override
-    public String getIdentifier( )
+    public ProfileID getId( )
     {
     {
-        return identifier;
+        return profileID;
     }
     }
 
 
     @Override
     @Override
     public String getDisplayName( final Locale locale )
     public String getDisplayName( final Locale locale )
     {
     {
-        return getIdentifier();
+        return getId().stringValue();
     }
     }
 
 
     public List<UserPermission> readSettingAsUserPermission( final PwmSetting setting )
     public List<UserPermission> readSettingAsUserPermission( final PwmSetting setting )
@@ -174,6 +176,6 @@ public abstract class AbstractProfile implements Profile
     public GuidMode readGuidMode()
     public GuidMode readGuidMode()
     {
     {
         final String guidAttributeName = readSettingAsString( PwmSetting.LDAP_GUID_ATTRIBUTE );
         final String guidAttributeName = readSettingAsString( PwmSetting.LDAP_GUID_ATTRIBUTE );
-        return JavaHelper.readEnumFromString( GuidMode.class, guidAttributeName ).orElse( GuidMode.VENDORGUID );
+        return EnumUtil.readEnumFromString( GuidMode.class, guidAttributeName ).orElse( GuidMode.VENDORGUID );
     }
     }
 }
 }

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

@@ -21,13 +21,14 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
 
 
 public class AccountInformationProfile extends AbstractProfile implements Profile
 public class AccountInformationProfile extends AbstractProfile implements Profile
 {
 {
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.AccountInformation;
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.AccountInformation;
 
 
-    protected AccountInformationProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedConfiguration )
+    protected AccountInformationProfile( final DomainID domainID, final ProfileID identifier, final StoredConfiguration storedConfiguration )
     {
     {
         super( domainID, identifier, storedConfiguration );
         super( domainID, identifier, storedConfiguration );
     }
     }
@@ -41,7 +42,7 @@ public class AccountInformationProfile extends AbstractProfile implements Profil
     public static class AccountInformationProfileFactory implements Profile.ProfileFactory
     public static class AccountInformationProfileFactory implements Profile.ProfileFactory
     {
     {
         @Override
         @Override
-        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final String identifier )
+        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final ProfileID identifier )
         {
         {
             return new AccountInformationProfile( domainID, identifier, storedConfiguration );
             return new AccountInformationProfile( domainID, identifier, storedConfiguration );
         }
         }

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

@@ -21,13 +21,14 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
 
 
 public class ActivateUserProfile extends AbstractProfile implements Profile
 public class ActivateUserProfile extends AbstractProfile implements Profile
 {
 {
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.ActivateUser;
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.ActivateUser;
 
 
-    protected ActivateUserProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedValueMap )
+    protected ActivateUserProfile( final DomainID domainID, final ProfileID identifier, final StoredConfiguration storedValueMap )
     {
     {
         super( domainID, identifier, storedValueMap );
         super( domainID, identifier, storedValueMap );
     }
     }
@@ -41,7 +42,7 @@ public class ActivateUserProfile extends AbstractProfile implements Profile
     public static class UserActivationProfileFactory implements ProfileFactory
     public static class UserActivationProfileFactory implements ProfileFactory
     {
     {
         @Override
         @Override
-        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final String identifier )
+        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final ProfileID identifier )
         {
         {
             return new ActivateUserProfile( domainID, identifier, storedConfiguration );
             return new ActivateUserProfile( domainID, identifier, storedConfiguration );
         }
         }

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

@@ -27,6 +27,7 @@ import com.novell.ldapchai.cr.ChallengeSet;
 import com.novell.ldapchai.exception.ChaiValidationException;
 import com.novell.ldapchai.exception.ChaiValidationException;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredSettingReader;
 import password.pwm.config.StoredSettingReader;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
@@ -48,7 +49,7 @@ public class ChallengeProfile implements Profile, Serializable
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( ChallengeProfile.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( ChallengeProfile.class );
 
 
-    private final String profileID;
+    private final ProfileID profileID;
     private final Locale locale;
     private final Locale locale;
     private final ChallengeSet challengeSet;
     private final ChallengeSet challengeSet;
     private final ChallengeSet helpdeskChallengeSet;
     private final ChallengeSet helpdeskChallengeSet;
@@ -57,7 +58,7 @@ public class ChallengeProfile implements Profile, Serializable
     private final List<UserPermission> userPermissions;
     private final List<UserPermission> userPermissions;
 
 
     private ChallengeProfile(
     private ChallengeProfile(
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale,
             final Locale locale,
             final ChallengeSet challengeSet,
             final ChallengeSet challengeSet,
             final ChallengeSet helpdeskChallengeSet,
             final ChallengeSet helpdeskChallengeSet,
@@ -77,7 +78,7 @@ public class ChallengeProfile implements Profile, Serializable
 
 
     public static ChallengeProfile readChallengeProfileFromConfig(
     public static ChallengeProfile readChallengeProfileFromConfig(
             final DomainID domainID,
             final DomainID domainID,
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale,
             final Locale locale,
             final StoredConfiguration storedConfiguration
             final StoredConfiguration storedConfiguration
     )
     )
@@ -127,7 +128,7 @@ public class ChallengeProfile implements Profile, Serializable
     }
     }
 
 
     public static ChallengeProfile createChallengeProfile(
     public static ChallengeProfile createChallengeProfile(
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale,
             final Locale locale,
             final ChallengeSet challengeSet,
             final ChallengeSet challengeSet,
             final ChallengeSet helpdeskChallengeSet,
             final ChallengeSet helpdeskChallengeSet,
@@ -139,7 +140,7 @@ public class ChallengeProfile implements Profile, Serializable
     }
     }
 
 
     @Override
     @Override
-    public String getIdentifier( )
+    public ProfileID getId( )
     {
     {
         return profileID;
         return profileID;
     }
     }
@@ -147,7 +148,7 @@ public class ChallengeProfile implements Profile, Serializable
     @Override
     @Override
     public String getDisplayName( final Locale locale )
     public String getDisplayName( final Locale locale )
     {
     {
-        return getIdentifier();
+        return getId().stringValue();
     }
     }
 
 
     public Locale getLocale( )
     public Locale getLocale( )

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

@@ -21,13 +21,14 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
 
 
 public class ChangePasswordProfile extends AbstractProfile implements Profile
 public class ChangePasswordProfile extends AbstractProfile implements Profile
 {
 {
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.ChangePassword;
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.ChangePassword;
 
 
-    protected ChangePasswordProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedConfiguration )
+    protected ChangePasswordProfile( final DomainID domainID, final ProfileID identifier, final StoredConfiguration storedConfiguration )
     {
     {
         super( domainID, identifier, storedConfiguration );
         super( domainID, identifier, storedConfiguration );
     }
     }
@@ -41,7 +42,7 @@ public class ChangePasswordProfile extends AbstractProfile implements Profile
     public static class ChangePasswordProfileFactory implements Profile.ProfileFactory
     public static class ChangePasswordProfileFactory implements Profile.ProfileFactory
     {
     {
         @Override
         @Override
-        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final String identifier )
+        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final ProfileID identifier )
         {
         {
             return new ChangePasswordProfile( domainID, identifier, storedConfiguration );
             return new ChangePasswordProfile( domainID, identifier, storedConfiguration );
         }
         }

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

@@ -21,13 +21,14 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
 
 
 public class DeleteAccountProfile extends AbstractProfile implements Profile
 public class DeleteAccountProfile extends AbstractProfile implements Profile
 {
 {
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.DeleteAccount;
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.DeleteAccount;
 
 
-    protected DeleteAccountProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedConfiguration )
+    protected DeleteAccountProfile( final DomainID domainID, final ProfileID identifier, final StoredConfiguration storedConfiguration )
     {
     {
         super( domainID, identifier, storedConfiguration );
         super( domainID, identifier, storedConfiguration );
     }
     }
@@ -41,7 +42,7 @@ public class DeleteAccountProfile extends AbstractProfile implements Profile
     public static class DeleteAccountProfileFactory implements ProfileFactory
     public static class DeleteAccountProfileFactory implements ProfileFactory
     {
     {
         @Override
         @Override
-        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final String identifier )
+        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final ProfileID identifier )
         {
         {
             return new DeleteAccountProfile( domainID, identifier, storedConfiguration );
             return new DeleteAccountProfile( domainID, identifier, storedConfiguration );
         }
         }

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

@@ -21,16 +21,15 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
 
 
-import java.util.Locale;
-
 public class EmailServerProfile extends AbstractProfile
 public class EmailServerProfile extends AbstractProfile
 {
 {
 
 
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.EmailServers;
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.EmailServers;
 
 
-    protected EmailServerProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedConfiguration )
+    protected EmailServerProfile( final DomainID domainID, final ProfileID identifier, final StoredConfiguration storedConfiguration )
     {
     {
         super( domainID, identifier, storedConfiguration );
         super( domainID, identifier, storedConfiguration );
     }
     }
@@ -41,16 +40,10 @@ public class EmailServerProfile extends AbstractProfile
         return PROFILE_TYPE;
         return PROFILE_TYPE;
     }
     }
 
 
-    @Override
-    public String getDisplayName( final Locale locale )
-    {
-        return this.getIdentifier();
-    }
-
     public static class EmailServerProfileFactory implements ProfileFactory
     public static class EmailServerProfileFactory implements ProfileFactory
     {
     {
         @Override
         @Override
-        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final String identifier )
+        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final ProfileID identifier )
         {
         {
             return new EmailServerProfile( domainID, identifier, storedConfiguration );
             return new EmailServerProfile( domainID, identifier, storedConfiguration );
         }
         }

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

@@ -21,6 +21,7 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
@@ -36,7 +37,7 @@ public class ForgottenPasswordProfile extends AbstractProfile
     private Set<IdentityVerificationMethod> requiredRecoveryVerificationMethods;
     private Set<IdentityVerificationMethod> requiredRecoveryVerificationMethods;
     private Set<IdentityVerificationMethod> optionalRecoveryVerificationMethods;
     private Set<IdentityVerificationMethod> optionalRecoveryVerificationMethods;
 
 
-    public ForgottenPasswordProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedConfiguration )
+    public ForgottenPasswordProfile( final DomainID domainID, final ProfileID identifier, final StoredConfiguration storedConfiguration )
     {
     {
         super( domainID, identifier, storedConfiguration );
         super( domainID, identifier, storedConfiguration );
     }
     }
@@ -80,7 +81,7 @@ public class ForgottenPasswordProfile extends AbstractProfile
     public static class ForgottenPasswordProfileFactory implements ProfileFactory
     public static class ForgottenPasswordProfileFactory implements ProfileFactory
     {
     {
         @Override
         @Override
-        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final String identifier )
+        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final ProfileID identifier )
         {
         {
             return new ForgottenPasswordProfile( domainID, identifier, storedConfiguration );
             return new ForgottenPasswordProfile( domainID, identifier, storedConfiguration );
         }
         }

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

@@ -21,6 +21,7 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
@@ -35,7 +36,7 @@ public class HelpdeskProfile extends AbstractProfile implements Profile
 {
 {
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.Helpdesk;
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.Helpdesk;
 
 
-    protected HelpdeskProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedConfiguration )
+    protected HelpdeskProfile( final DomainID domainID, final ProfileID identifier, final StoredConfiguration storedConfiguration )
     {
     {
         super( domainID, identifier, storedConfiguration );
         super( domainID, identifier, storedConfiguration );
     }
     }
@@ -62,7 +63,7 @@ public class HelpdeskProfile extends AbstractProfile implements Profile
     public static class HelpdeskProfileFactory implements ProfileFactory
     public static class HelpdeskProfileFactory implements ProfileFactory
     {
     {
         @Override
         @Override
-        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final String identifier )
+        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final ProfileID identifier )
         {
         {
             return new HelpdeskProfile( domainID, identifier, storedConfiguration );
             return new HelpdeskProfile( domainID, identifier, storedConfiguration );
         }
         }

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

@@ -27,6 +27,7 @@ import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.AppProperty;
 import password.pwm.AppProperty;
 import password.pwm.PwmDomain;
 import password.pwm.PwmDomain;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
@@ -43,7 +44,6 @@ import password.pwm.util.logging.PwmLogger;
 
 
 import java.time.Instant;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
@@ -56,7 +56,14 @@ public class LdapProfile extends AbstractProfile implements Profile
 
 
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.LdapProfile;
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.LdapProfile;
 
 
-    protected LdapProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedValueMap )
+    private List<String> rootContextSupplier;
+    private Map<String, String> selectableContexts;
+
+    private Optional<UserIdentity> cachedTestUser;
+    private UserIdentity cachedProxyUser;
+
+
+    protected LdapProfile( final DomainID domainID, final ProfileID identifier, final StoredConfiguration storedValueMap )
     {
     {
         super( domainID, identifier, storedValueMap );
         super( domainID, identifier, storedValueMap );
     }
     }
@@ -67,17 +74,22 @@ public class LdapProfile extends AbstractProfile implements Profile
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        final List<String> rawValues = readSettingAsStringArray( PwmSetting.LDAP_LOGIN_CONTEXTS );
-        final Map<String, String> configuredValues = StringUtil.convertStringListToNameValuePair( rawValues, ":::" );
-        final Map<String, String> canonicalValues = new LinkedHashMap<>( configuredValues.size() );
-        for ( final Map.Entry<String, String> entry : configuredValues.entrySet() )
+        if ( selectableContexts == null )
         {
         {
-            final String dn = entry.getKey();
-            final String label = entry.getValue();
-            final String canonicalDN = readCanonicalDN( sessionLabel, pwmDomain, dn );
-            canonicalValues.put( canonicalDN, label );
+            final List<String> rawValues = readSettingAsStringArray( PwmSetting.LDAP_LOGIN_CONTEXTS );
+            final Map<String, String> configuredValues = StringUtil.convertStringListToNameValuePair( rawValues, ":::" );
+            final Map<String, String> canonicalValues = new LinkedHashMap<>( configuredValues.size() );
+            for ( final Map.Entry<String, String> entry : configuredValues.entrySet() )
+            {
+                final String dn = entry.getKey();
+                final String label = entry.getValue();
+                final String canonicalDN = readCanonicalDN( sessionLabel, pwmDomain, dn );
+                canonicalValues.put( canonicalDN, label );
+            }
+            selectableContexts = Map.copyOf( canonicalValues );
         }
         }
-        return Collections.unmodifiableMap( canonicalValues );
+
+        return selectableContexts;
     }
     }
 
 
     public List<String> getRootContexts(
     public List<String> getRootContexts(
@@ -86,14 +98,19 @@ public class LdapProfile extends AbstractProfile implements Profile
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        final List<String> rawValues = readSettingAsStringArray( PwmSetting.LDAP_CONTEXTLESS_ROOT );
-        final List<String> canonicalValues = new ArrayList<>( rawValues.size() );
-        for ( final String dn : rawValues )
+        if ( rootContextSupplier == null )
         {
         {
-            final String canonicalDN = readCanonicalDN( sessionLabel, pwmDomain, dn );
-            canonicalValues.add( canonicalDN );
+            final List<String> rawValues = readSettingAsStringArray( PwmSetting.LDAP_CONTEXTLESS_ROOT );
+            final List<String> canonicalValues = new ArrayList<>( rawValues.size() );
+            for ( final String dn : rawValues )
+            {
+                final String canonicalDN = readCanonicalDN( sessionLabel, pwmDomain, dn );
+                canonicalValues.add( canonicalDN );
+            }
+            rootContextSupplier = List.copyOf( canonicalValues );
         }
         }
-        return Collections.unmodifiableList( canonicalValues );
+
+        return rootContextSupplier;
     }
     }
 
 
     public List<String> getLdapUrls(
     public List<String> getLdapUrls(
@@ -106,7 +123,7 @@ public class LdapProfile extends AbstractProfile implements Profile
     public String getDisplayName( final Locale locale )
     public String getDisplayName( final Locale locale )
     {
     {
         final String displayName = readSettingAsLocalizedString( PwmSetting.LDAP_PROFILE_DISPLAY_NAME, locale );
         final String displayName = readSettingAsLocalizedString( PwmSetting.LDAP_PROFILE_DISPLAY_NAME, locale );
-        return StringUtil.isTrimEmpty( displayName ) ? getIdentifier() : displayName;
+        return StringUtil.isTrimEmpty( displayName ) ? getId().stringValue() : displayName;
     }
     }
 
 
     public String getUsernameAttribute( )
     public String getUsernameAttribute( )
@@ -119,13 +136,13 @@ public class LdapProfile extends AbstractProfile implements Profile
     public ChaiProvider getProxyChaiProvider( final SessionLabel sessionLabel, final PwmDomain pwmDomain ) throws PwmUnrecoverableException
     public ChaiProvider getProxyChaiProvider( final SessionLabel sessionLabel, final PwmDomain pwmDomain ) throws PwmUnrecoverableException
     {
     {
         verifyIsEnabled();
         verifyIsEnabled();
-        return pwmDomain.getProxyChaiProvider( sessionLabel, this.getIdentifier() );
+        return pwmDomain.getProxyChaiProvider( sessionLabel, this.getId() );
     }
     }
 
 
     @Override
     @Override
     public ProfileDefinition profileType( )
     public ProfileDefinition profileType( )
     {
     {
-        throw new UnsupportedOperationException();
+        return PROFILE_TYPE;
     }
     }
 
 
     @Override
     @Override
@@ -154,7 +171,7 @@ public class LdapProfile extends AbstractProfile implements Profile
         final boolean enableCanonicalCache = Boolean.parseBoolean( pwmDomain.getConfig().readAppProperty( AppProperty.LDAP_CACHE_CANONICAL_ENABLE ) );
         final boolean enableCanonicalCache = Boolean.parseBoolean( pwmDomain.getConfig().readAppProperty( AppProperty.LDAP_CACHE_CANONICAL_ENABLE ) );
 
 
         String canonicalValue = null;
         String canonicalValue = null;
-        final CacheKey cacheKey = CacheKey.newKey( LdapProfile.class, null, "canonicalDN-" + this.getIdentifier() + "-" + dnValue );
+        final CacheKey cacheKey = CacheKey.newKey( LdapProfile.class, null, "canonicalDN-" + this.getId() + "-" + dnValue );
         if ( enableCanonicalCache )
         if ( enableCanonicalCache )
         {
         {
             final String cachedDN = pwmDomain.getCacheService().get( cacheKey, String.class );
             final String cachedDN = pwmDomain.getCacheService().get( cacheKey, String.class );
@@ -198,17 +215,27 @@ public class LdapProfile extends AbstractProfile implements Profile
     public Optional<UserIdentity> getTestUser( final SessionLabel sessionLabel, final PwmDomain pwmDomain )
     public Optional<UserIdentity> getTestUser( final SessionLabel sessionLabel, final PwmDomain pwmDomain )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        return readUserIdentity( sessionLabel, pwmDomain, PwmSetting.LDAP_TEST_USER_DN );
+        if ( cachedTestUser == null )
+        {
+            cachedTestUser = readUserIdentity( sessionLabel, pwmDomain, PwmSetting.LDAP_TEST_USER_DN );
+        }
+
+        return cachedTestUser;
     }
     }
 
 
     public UserIdentity getProxyUser( final SessionLabel sessionLabel, final PwmDomain pwmDomain )
     public UserIdentity getProxyUser( final SessionLabel sessionLabel, final PwmDomain pwmDomain )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        return readUserIdentity( sessionLabel, pwmDomain, PwmSetting.LDAP_PROXY_USER_DN )
-                .orElseThrow( () ->
-                        new PwmUnrecoverableException( new ErrorInformation(
-                                PwmError.CONFIG_FORMAT_ERROR,
-                                "ldap proxy user is not defined" ) ) );
+        if ( cachedProxyUser == null )
+        {
+            cachedProxyUser = readUserIdentity( sessionLabel, pwmDomain, PwmSetting.LDAP_PROXY_USER_DN )
+                    .orElseThrow( () ->
+                            new PwmUnrecoverableException( new ErrorInformation(
+                                    PwmError.CONFIG_FORMAT_ERROR,
+                                    "ldap proxy user is not defined" ) ) );
+        }
+
+        return cachedProxyUser;
     }
     }
 
 
     private Optional<UserIdentity> readUserIdentity(
     private Optional<UserIdentity> readUserIdentity(
@@ -222,7 +249,7 @@ public class LdapProfile extends AbstractProfile implements Profile
 
 
         if ( StringUtil.notEmpty( testUserDN ) )
         if ( StringUtil.notEmpty( testUserDN ) )
         {
         {
-            return Optional.of( UserIdentity.create( testUserDN, this.getIdentifier(), pwmDomain.getDomainID() ).canonicalized( sessionLabel, pwmDomain.getPwmApplication() ) );
+            return Optional.of( UserIdentity.create( testUserDN, this.getId(), pwmDomain.getDomainID() ).canonicalized( sessionLabel, pwmDomain.getPwmApplication() ) );
         }
         }
 
 
         return Optional.empty();
         return Optional.empty();
@@ -231,7 +258,7 @@ public class LdapProfile extends AbstractProfile implements Profile
     public static class LdapProfileFactory implements ProfileFactory
     public static class LdapProfileFactory implements ProfileFactory
     {
     {
         @Override
         @Override
-        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final String identifier )
+        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final ProfileID identifier )
         {
         {
             return new LdapProfile( domainID, identifier, storedConfiguration );
             return new LdapProfile( domainID, identifier, storedConfiguration );
         }
         }
@@ -242,7 +269,7 @@ public class LdapProfile extends AbstractProfile implements Profile
     {
     {
         if ( !isEnabled() )
         if ( !isEnabled() )
         {
         {
-            final String msg = "ldap profile '" + getIdentifier() + "' is not enabled";
+            final String msg = "ldap profile '" + getId() + "' is not enabled";
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, msg ) );
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, msg ) );
         }
         }
     }
     }
@@ -255,6 +282,29 @@ public class LdapProfile extends AbstractProfile implements Profile
     @Override
     @Override
     public String toString()
     public String toString()
     {
     {
-        return "LDAPProfile:" + this.getIdentifier();
+        return "LDAPProfile:" + this.getId();
+    }
+
+    public void testIfDnIsContainedByRootContext( final SessionLabel sessionLabel, final PwmDomain pwmDomain, final String testDN )
+            throws PwmUnrecoverableException
+    {
+        if ( StringUtil.isEmpty( testDN ) )
+        {
+            return;
+        }
+
+        final List<String> rootContexts = getRootContexts( sessionLabel, pwmDomain );
+
+        final Optional<String> matchedDn = rootContexts.stream()
+                .filter( testDN::endsWith )
+                .findFirst();
+
+        if ( matchedDn.isPresent() )
+        {
+            return;
+        }
+
+        final String msg = "specified search context '" + testDN + "' is not contained by a configured root context";
+        throw new PwmUnrecoverableException( PwmError.CONFIG_FORMAT_ERROR, msg );
     }
     }
 }
 }

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

@@ -27,6 +27,7 @@ import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.PwmDomain;
 import password.pwm.PwmDomain;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.DomainConfig;
@@ -44,6 +45,7 @@ import java.time.Instant;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 
 
 public class NewUserProfile extends AbstractProfile implements Profile
 public class NewUserProfile extends AbstractProfile implements Profile
 {
 {
@@ -54,7 +56,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
     private Instant newUserPasswordPolicyCacheTime;
     private Instant newUserPasswordPolicyCacheTime;
     private final Map<Locale, PwmPasswordPolicy> newUserPasswordPolicyCache = new HashMap<>();
     private final Map<Locale, PwmPasswordPolicy> newUserPasswordPolicyCache = new HashMap<>();
 
 
-    protected NewUserProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedConfiguration )
+    protected NewUserProfile( final DomainID domainID, final ProfileID identifier, final StoredConfiguration storedConfiguration )
     {
     {
         super( domainID, identifier, storedConfiguration );
         super( domainID, identifier, storedConfiguration );
     }
     }
@@ -69,7 +71,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
     public String getDisplayName( final Locale locale )
     public String getDisplayName( final Locale locale )
     {
     {
         final String value = this.readSettingAsLocalizedString( PwmSetting.NEWUSER_PROFILE_DISPLAY_NAME, locale );
         final String value = this.readSettingAsLocalizedString( PwmSetting.NEWUSER_PROFILE_DISPLAY_NAME, locale );
-        return value != null && !value.isEmpty() ? value : this.getIdentifier();
+        return value != null && !value.isEmpty() ? value : this.getId().stringValue();
     }
     }
 
 
     public PwmPasswordPolicy getNewUserPasswordPolicy( final PwmRequestContext pwmRequestContext )
     public PwmPasswordPolicy getNewUserPasswordPolicy( final PwmRequestContext pwmRequestContext )
@@ -101,7 +103,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
         if ( StringUtil.isEmpty( configuredNewUserPasswordDN ) )
         if ( StringUtil.isEmpty( configuredNewUserPasswordDN ) )
         {
         {
             final String errorMsg = "the setting "
             final String errorMsg = "the setting "
-                    + PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( this.getIdentifier(), PwmConstants.DEFAULT_LOCALE )
+                    + PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( this.getId(), PwmConstants.DEFAULT_LOCALE )
                     + " must have a value";
                     + " must have a value";
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INVALID_CONFIG, errorMsg ) );
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INVALID_CONFIG, errorMsg ) );
         }
         }
@@ -114,9 +116,9 @@ public class NewUserProfile extends AbstractProfile implements Profile
                 if ( StringUtil.isEmpty( lookupDN ) )
                 if ( StringUtil.isEmpty( lookupDN ) )
                 {
                 {
                     final String errorMsg = "setting "
                     final String errorMsg = "setting "
-                            + PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE )
+                            + PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getId(), PwmConstants.DEFAULT_LOCALE )
                             + " must be configured since setting "
                             + " must be configured since setting "
-                            + PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( this.getIdentifier(), PwmConstants.DEFAULT_LOCALE )
+                            + PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( this.getId(), PwmConstants.DEFAULT_LOCALE )
                             + " is set to " + TEST_USER_CONFIG_VALUE;
                             + " is set to " + TEST_USER_CONFIG_VALUE;
                     throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INVALID_CONFIG, errorMsg ) );
                     throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INVALID_CONFIG, errorMsg ) );
                 }
                 }
@@ -139,9 +141,9 @@ public class NewUserProfile extends AbstractProfile implements Profile
             {
             {
                 try
                 try
                 {
                 {
-                    final ChaiProvider chaiProvider = pwmDomain.getProxyChaiProvider( sessionLabel, ldapProfile.getIdentifier() );
+                    final ChaiProvider chaiProvider = pwmDomain.getProxyChaiProvider( sessionLabel, ldapProfile.getId() );
                     final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser( lookupDN );
                     final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser( lookupDN );
-                    final UserIdentity userIdentity = UserIdentity.create( lookupDN, ldapProfile.getIdentifier(), pwmDomain.getDomainID() );
+                    final UserIdentity userIdentity = UserIdentity.create( lookupDN, ldapProfile.getId(), pwmDomain.getDomainID() );
                     thePolicy = PasswordUtility.readPasswordPolicyForUser( pwmDomain, null, userIdentity, chaiUser );
                     thePolicy = PasswordUtility.readPasswordPolicyForUser( pwmDomain, null, userIdentity, chaiUser );
                 }
                 }
                 catch ( final ChaiUnavailableException e )
                 catch ( final ChaiUnavailableException e )
@@ -179,7 +181,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
     public static class NewUserProfileFactory implements ProfileFactory
     public static class NewUserProfileFactory implements ProfileFactory
     {
     {
         @Override
         @Override
-        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final String identifier )
+        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final ProfileID identifier )
         {
         {
             return new NewUserProfile( domainID, identifier, storedConfiguration );
             return new NewUserProfile( domainID, identifier, storedConfiguration );
         }
         }
@@ -188,16 +190,17 @@ public class NewUserProfile extends AbstractProfile implements Profile
     public LdapProfile getLdapProfile( final DomainConfig domainConfig )
     public LdapProfile getLdapProfile( final DomainConfig domainConfig )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        final String configuredProfile = readSettingAsString( PwmSetting.NEWUSER_LDAP_PROFILE );
-        if ( StringUtil.notEmpty( configuredProfile ) )
+        final Optional<ProfileID> configuredProfile = domainConfig
+                .profileForStringId( ProfileDefinition.NewUser, readSettingAsString( PwmSetting.NEWUSER_LDAP_PROFILE ) );
+        if ( configuredProfile.isPresent() )
         {
         {
-            final LdapProfile ldapProfile = domainConfig.getLdapProfiles().get( configuredProfile );
+            final LdapProfile ldapProfile = domainConfig.getLdapProfiles().get( configuredProfile.get() );
             if ( ldapProfile == null )
             if ( ldapProfile == null )
             {
             {
                 throw new PwmUnrecoverableException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                 throw new PwmUnrecoverableException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                         {
                         {
                                 "configured ldap profile for new user profile is invalid.  check setting "
                                 "configured ldap profile for new user profile is invalid.  check setting "
-                                        + PwmSetting.NEWUSER_LDAP_PROFILE.toMenuLocationDebug( this.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
+                                        + PwmSetting.NEWUSER_LDAP_PROFILE.toMenuLocationDebug( this.getId(), PwmConstants.DEFAULT_LOCALE ),
                         }
                         }
                 ) );
                 ) );
             }
             }

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

@@ -21,6 +21,7 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.bean.DomainID;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
 
 
 public class PeopleSearchProfile extends AbstractProfile
 public class PeopleSearchProfile extends AbstractProfile
@@ -28,7 +29,7 @@ public class PeopleSearchProfile extends AbstractProfile
 
 
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.PeopleSearch;
     private static final ProfileDefinition PROFILE_TYPE = ProfileDefinition.PeopleSearch;
 
 
-    protected PeopleSearchProfile( final DomainID domainID, final String identifier, final StoredConfiguration storedConfiguration )
+    protected PeopleSearchProfile( final DomainID domainID, final ProfileID identifier, final StoredConfiguration storedConfiguration )
     {
     {
         super( domainID, identifier, storedConfiguration );
         super( domainID, identifier, storedConfiguration );
     }
     }
@@ -42,7 +43,7 @@ public class PeopleSearchProfile extends AbstractProfile
     public static class PeopleSearchProfileFactory implements ProfileFactory
     public static class PeopleSearchProfileFactory implements ProfileFactory
     {
     {
         @Override
         @Override
-        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final String identifier )
+        public Profile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final DomainID domainID, final ProfileID identifier )
         {
         {
             return new PeopleSearchProfile( domainID, identifier, storedConfiguration );
             return new PeopleSearchProfile( domainID, identifier, storedConfiguration );
         }
         }

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