Browse Source

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 năm trước cách đây
mục cha
commit
53485c17dd
100 tập tin đã thay đổi với 2508 bổ sung1247 xóa
  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
 - 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
 ### Changed
 - Issue #573 - PWM 5081 at the end of user activation ( no profile assigned )

+ 100 - 44
README.md

@@ -1,6 +1,6 @@
 # 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/).
 
@@ -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 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.
+* [Downloads](https://github.com/pwm-project/pwm/releases)
 
 # Features
 * 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
-  * 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
-* 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
-* 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
 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
 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:
 * 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
 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:

+ 3 - 2
build/checkstyle-import.xml

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

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

@@ -49,7 +49,7 @@
                 "karma-spec-reporter": "0.0.32",
                 "karma-webpack": "5.0.0",
                 "lodash": ">=4.17.21",
-                "moment": "^2.29.3",
+                "moment": "^2.29.4",
                 "ngtemplate-loader": "2.0.1",
                 "node-sass": "^7.0.0",
                 "postcss-loader": "2.1.1",
@@ -7061,9 +7061,9 @@
             }
         },
         "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,
             "engines": {
                 "node": "*"
@@ -10956,9 +10956,9 @@
             "dev": true
         },
         "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,
             "dependencies": {
                 "commander": "^2.20.0",
@@ -19298,9 +19298,9 @@
             }
         },
         "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
         },
         "move-concurrently": {
@@ -22466,9 +22466,9 @@
             }
         },
         "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,
             "requires": {
                 "commander": "^2.20.0",

+ 1 - 1
client/angular/package.json

@@ -57,7 +57,7 @@
         "karma-spec-reporter": "0.0.32",
         "karma-webpack": "5.0.0",
         "lodash": ">=4.17.21",
-        "moment": "^2.29.3",
+        "moment": "^2.29.4",
         "ngtemplate-loader": "2.0.1",
         "node-sass": "^7.0.0",
         "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_MatchCondition": "Match Condition",
   "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_PleaseWait": "Loading...",
   "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'])
     .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) => {
                 this.verificationStatus = STATUS_FAILED;
-            })
+            });
     }
 
     onTokenDestinationChanged() {

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

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

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

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

+ 6 - 4
client/pom.xml

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

+ 7 - 10
data-service/pom.xml

@@ -9,8 +9,8 @@
 
     <modelVersion>4.0.0</modelVersion>
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-data-service</artifactId>
-
     <packaging>war</packaging>
 
     <name>PWM Password Self Service: Data Service WAR</name>
@@ -73,7 +73,7 @@
             </plugin>
             <plugin>
                 <artifactId>maven-resources-plugin</artifactId>
-                <version>3.2.0</version>
+                <version>3.3.0</version>
                 <executions>
                     <execution>
                         <id>copy-resources</id>
@@ -82,6 +82,7 @@
                             <goal>copy-resources</goal>
                         </goals>
                         <configuration>
+                            <propertiesEncoding>ISO-8859-1</propertiesEncoding>
                             <outputDirectory>${project.build.outputDirectory}/src</outputDirectory>
                             <resources>
                                 <resource><directory>src/main/java</directory></resource>
@@ -99,7 +100,7 @@
             <plugin>
                 <groupId>com.google.cloud.tools</groupId>
                 <artifactId>jib-maven-plugin</artifactId>
-                <version>3.2.1</version>
+                <version>3.3.0</version>
                 <executions>
                     <execution>
                         <id>make-docker-image</id>
@@ -127,6 +128,7 @@
                                 <jvmFlags>
                                     <jvmFlag>-server</jvmFlag>
                                     <jvmFlag>-Xmx256m</jvmFlag>
+                                    <jvmFlag>-XX:+UseStringDeduplication</jvmFlag>
                                 </jvmFlags>
                                 <environment>
                                     <DATA_SERVICE_PROPS>/config/data-service.properties</DATA_SERVICE_PROPS>
@@ -184,11 +186,6 @@
         </dependency>
         <!-- / container dependencies -->
 
-        <dependency>
-            <groupId>commons-net</groupId>
-            <artifactId>commons-net</artifactId>
-            <version>3.8.0</version>
-        </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-csv</artifactId>
@@ -202,12 +199,12 @@
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
-            <version>2.0.0-alpha7</version>
+            <version>2.0.2</version>
         </dependency>
         <dependency>
             <groupId>ch.qos.logback</groupId>
             <artifactId>logback-classic</artifactId>
-            <version>1.3.0-alpha16</version>
+            <version>1.4.1</version>
         </dependency>
     </dependencies>
 </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();
         sce.getServletContext().setAttribute( CONTEXT_ATTR, this );
-        LOGGER.info( "open for bidness" );
+        LOGGER.info( () -> "open for bidness" );
     }
 
     @Override
@@ -45,7 +45,7 @@ public class ContextManager implements ServletContextListener
     {
         app.close();
         app = null;
-        LOGGER.info( "cya!" );
+        LOGGER.info( () -> "cya!" );
     }
 
     public PwmReceiverApp getApp( )

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

@@ -20,6 +20,8 @@
 
 package password.pwm.receiver;
 
+import java.util.function.Supplier;
+
 public class Logger
 {
     private final org.slf4j.Logger logger;
@@ -34,13 +36,13 @@ public class Logger
         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;
 
 import password.pwm.bean.pub.PublishVersionBean;
-import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.ws.server.RestResultBean;
 
 import javax.servlet.annotation.WebServlet;
@@ -38,19 +37,17 @@ import java.util.Collections;
 )
 public class PublishVersionServlet extends HttpServlet
 {
-    private static final Logger LOGGER = Logger.createLogger( PublishVersionServlet.class );
-    private static final AtomicLoopIntIncrementer REQ_COUNTER = new AtomicLoopIntIncrementer();
-
-
     @Override
     protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
             throws IOException
     {
-        final int requestId = REQ_COUNTER.next();
-        LOGGER.debug( "http request #" + requestId + " for version" );
 
         final ContextManager contextManager = ContextManager.getContextManager( req.getServletContext() );
         final PwmReceiverApp app = contextManager.getApp();
+
+        app.getStatisticCounterBundle().increment( PwmReceiverApp.CounterStatsKey.VersionCheckRequests );
+        app.getStatisticEpsBundle().markEvent( PwmReceiverApp.EpsStatKey.VersionCheckRequests );
+
         final PublishVersionBean publishVersionBean = new PublishVersionBean(
                 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;
 
 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 java.io.IOException;
+import java.time.Instant;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -38,6 +41,24 @@ public class PwmReceiverApp
 
     private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
     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( )
     {
@@ -106,4 +127,18 @@ public class PwmReceiverApp
         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;
 
 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.TimeDuration;
 
@@ -87,7 +87,7 @@ public class Settings
         try ( Reader reader = new InputStreamReader( Files.newInputStream( path ), StandardCharsets.UTF_8 ) )
         {
             properties.load( reader );
-            final Map<Setting, String> returnMap = CollectionUtil.enumStream( Setting.class )
+            final Map<Setting, String> returnMap = EnumUtil.enumStream( Setting.class )
                     .collect( Collectors.toUnmodifiableMap(
                             setting -> setting,
                             setting -> properties.getProperty( setting.name(), setting.getDefaultValue() )
@@ -128,7 +128,7 @@ public class Settings
         }
         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;
         }
     }

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

@@ -67,12 +67,12 @@ public class Storage
         final EnvironmentConfig environmentConfig = new EnvironmentConfig();
         environment = Environments.newInstance( storagePath.getAbsolutePath(), environmentConfig );
 
-        LOGGER.info( "environment open" );
+        LOGGER.info( () -> "environment open" );
 
         environment.executeInTransaction( txn -> store
                 = 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 )

+ 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.config.PwmSetting;
 import password.pwm.svc.stats.Statistic;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 
 import java.time.Duration;
@@ -45,6 +46,7 @@ public class SummaryBean
     private Map<String, Integer> settingCount;
     private Map<String, Integer> statCount;
     private Map<String, Integer> osCount;
+    private Map<String, Integer> deploymentCount;
     private Map<String, Integer> dbCount;
     private Map<String, Integer> javaCount;
     private Map<String, Integer> appVersionCount;
@@ -60,6 +62,7 @@ public class SummaryBean
         final Map<String, Integer> appServerCount = new TreeMap<>();
         final Map<String, Integer> settingCount = 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> dbCount = new TreeMap<>();
         final Map<String, Integer> javaCount = new TreeMap<>();
@@ -86,6 +89,7 @@ public class SummaryBean
                         .installAge( TimeDuration.fromCurrent( bean.getInstallTime() ).asDuration() )
                         .updateAge( TimeDuration.fromCurrent( bean.getTimestamp() ).asDuration() )
                         .ldapVendor( ldapVendor )
+
                         .osName( bean.getAbout().get( PwmAboutProperty.java_osName.name() ) )
                         .osVersion( bean.getAbout().get( PwmAboutProperty.java_osVersion.name() ) )
                         .servletName( bean.getAbout().get( PwmAboutProperty.java_appServerInfo.name() ) )
@@ -106,6 +110,8 @@ public class SummaryBean
 
                 incrementCounterMap( javaCount, siteSummary.getJavaVm() );
 
+                incrementCounterMap( deploymentCount, bean.getAbout().get( PwmAboutProperty.app_deployment_type.name() ) );
+
                 incrementCounterMap( appVersionCount, siteSummary.getVersion() );
 
                 for ( final String settingKey : bean.getConfiguredSettings() )
@@ -138,6 +144,7 @@ public class SummaryBean
                 .appServerCount( appServerCount )
                 .osCount( osCount )
                 .dbCount( dbCount )
+                .deploymentCount( deploymentCount )
                 .javaCount( javaCount )
                 .appVersionCount( appVersionCount )
                 .build();
@@ -151,6 +158,11 @@ public class SummaryBean
 
     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 ) )
         {
             map.put( key, map.get( key ) + count );
@@ -198,6 +210,7 @@ public class SummaryBean
         private String osName;
         private String osVersion;
         private String servletName;
+        private String deploymentType;
         private String dbVendor;
         private String javaVm;
         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.http.ServletUtility;
 import password.pwm.i18n.Message;
-import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.ws.server.RestResultBean;
 
@@ -46,8 +45,6 @@ import java.io.IOException;
 public class TelemetryRestReceiver extends HttpServlet
 {
     private static final Logger LOGGER = Logger.createLogger( TelemetryViewerServlet.class );
-    private static final AtomicLoopIntIncrementer REQ_COUNTER = new AtomicLoopIntIncrementer();
-
 
     @Override
     protected void doPost( final HttpServletRequest req, final HttpServletResponse resp )
@@ -55,17 +52,19 @@ public class TelemetryRestReceiver extends HttpServlet
     {
         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 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 );
 
             final RestResultBean restResultBean = RestResultBean.forSuccessMessage( null, null, null, Message.Success_Unknown );
             ReceiverUtil.outputJsonResponse( req, resp, restResultBean );
-            LOGGER.debug( "http rest request #" + requestId + " received from " + telemetryPublishBean.getSiteDescription() );
+            LOGGER.debug( () -> "http telemetry rest data received from " + telemetryPublishBean.getSiteDescription() );
         }
         catch ( final PwmUnrecoverableException e )
         {

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

@@ -20,7 +20,6 @@
 
 package password.pwm.receiver;
 
-import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.StringUtil;
 
 import javax.servlet.ServletException;
@@ -40,31 +39,27 @@ import java.time.temporal.ChronoUnit;
 )
 public class TelemetryViewerServlet extends HttpServlet
 {
-    private static final Logger LOGGER = Logger.createLogger( TelemetryViewerServlet.class );
     private static final String PARAM_DAYS = "days";
-    private static final AtomicLoopIntIncrementer REQ_COUNTER = new AtomicLoopIntIncrementer();
 
     public static final String SUMMARY_ATTR = "SummaryBean";
 
-
     @Override
     protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
             throws ServletException, IOException
     {
-        final int requestId = REQ_COUNTER.next();
-        LOGGER.debug( "http request #" + requestId + " for viewer" );
         final String daysString = req.getParameter( PARAM_DAYS );
         final int days = StringUtil.isEmpty( daysString ) ? 30 : Integer.parseInt( daysString );
         final ContextManager contextManager = ContextManager.getContextManager( req.getServletContext() );
+
         final PwmReceiverApp app = contextManager.getApp();
+        app.getStatisticCounterBundle().increment( PwmReceiverApp.CounterStatsKey.TelemetryViewRequests );
+        app.getStatisticEpsBundle().markEvent( PwmReceiverApp.EpsStatKey.TelemetryViewRequests );
 
         {
             final String errorState = app.getStatus().getErrorState();
             if ( StringUtil.notEmpty( errorState ) )
             {
                 resp.sendError( 500, errorState );
-                final String htmlBody = "<html>Error: " + errorState + "</html>";
-                resp.getWriter().print( htmlBody );
                 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.ContextManager" %>
 <%@ 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>
 <%@ page contentType="text/html" %>
 <% SummaryBean summaryBean = (SummaryBean)request.getAttribute(TelemetryViewerServlet.SUMMARY_ATTR); %>
 <% PwmReceiverApp app = ContextManager.getContextManager(request.getServletContext()).getApp(); %>
+<% PwmNumberFormat format = PwmNumberFormat.forLocale( request.getLocale() ); %>
 <html>
 <head>
     <title>Telemetry Data</title>
@@ -40,8 +44,11 @@
 </head>
 <body>
 <div>
+    <h2>Server Info</h2>
     Current Time: <%=StringUtil.toIsoDate( Instant.now() )%>
     <br/>
+    Up Time: <%=StringUtil.toIsoDuration(Duration.between(app.getStartupTime(), Instant.now()))%>
+    <br/>
     <% if (app.getSettings().isFtpEnabled()) {%>
     <% Instant lastIngest = app.getStatus().getLastFtpIngest(); %>
     Last FTP Ingestion: <%= lastIngest == null ? "n/a" : lastIngest.toString()%>
@@ -55,15 +62,28 @@
     <br/>
     Servers Shown: <%= summaryBean.getServerCount() %>
     <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/>
+    <h1>PWM Telemetry Data</h1>
 
+    <%--
     <form method="get">
         <label>Servers that have sent data in last number of days
             <input type="number" name="days" id="days" value="30" max="3650" min="1">
         </label>
         <button type="submit">Update</button>
     </form>
-
+    --%>
     <h2>Versions</h2>
     <table class="sortable">
         <tr>
@@ -116,6 +136,19 @@
         </tr>
         <% } %>
     </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>
     <table class="sortable">
         <tr>
@@ -151,7 +184,7 @@
         <% for (final String setting: summaryBean.getSettingCount().keySet()) { %>
         <tr>
             <td><%=setting%></td>
-            <td><%=summaryBean.getSettingCount().get(setting)%></td>
+            <td><%=format.format(summaryBean.getSettingCount().get(setting).longValue())%></td>
         </tr>
         <% } %>
     </table>
@@ -164,7 +197,7 @@
         <% for (final String statistic: summaryBean.getStatCount().keySet()) { %>
         <tr>
             <td><%=statistic%></td>
-            <td><%=summaryBean.getStatCount().get(statistic)%></td>
+            <td><%=format.format(summaryBean.getStatCount().get(statistic).longValue())%></td>
         </tr>
         <% } %>
     </table>

+ 4 - 4
docker/pom.xml

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

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

@@ -1,4 +1,5 @@
 -server
 -Xmx1g
 -Xms1g
+-XX:+UseStringDeduplication
 -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>
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-lib-data</artifactId>
     <packaging>jar</packaging>
 
@@ -40,7 +41,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.3.0</version>
                 <configuration>
                     <archive>
                         <manifestEntries>
@@ -70,7 +71,7 @@
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>
-            <version>2.9.0</version>
+            <version>2.9.1</version>
         </dependency>
     </dependencies>
 </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;
 
-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.Collections;
@@ -34,12 +34,12 @@ public class VersionNumberTest
     {
         {
             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" );
-            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 );
 
-        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>
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-lib-util</artifactId>
     <packaging>jar</packaging>
 
@@ -40,7 +41,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.3.0</version>
                 <configuration>
                     <archive>
                         <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;
 
-import password.pwm.util.java.MovingAverage;
-import password.pwm.util.java.TimeDuration;
+import password.pwm.util.java.PwmNumberFormat;
 
 import java.io.Serializable;
 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.ReentrantLock;
 
 public class EventRateMeter implements Serializable
 {
-    private final TimeDuration maxDuration;
+    private final long maxDuration;
     private final Lock lock = new ReentrantLock();
 
     private volatile MovingAverage movingAverage;
     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();
     }
 
@@ -51,7 +49,7 @@ public class EventRateMeter implements Serializable
         lock.lock();
         try
         {
-            movingAverage = new MovingAverage( maxDuration.asMillis() );
+            movingAverage = new MovingAverage( Duration.ofMillis( maxDuration ) );
             remainder = 0;
         }
         finally
@@ -60,6 +58,11 @@ public class EventRateMeter implements Serializable
         }
     }
 
+    public void markEvent()
+    {
+        markEvents( 1 );
+    }
+
     public void markEvents( final int eventCount )
     {
         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();
         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.
  */
 
-package password.pwm.util.java;
+package password.pwm.util;
 
 import java.io.Serializable;
 import java.text.NumberFormat;
@@ -59,20 +59,12 @@ public class MovingAverage implements Serializable
     private volatile long lastMillis;
     private volatile double average;
 
-
     /**
      * 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 )
     {
         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.
  */
 
-package password.pwm.util.java;
+package password.pwm.util;
 
 import java.math.BigDecimal;
 import java.math.MathContext;

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

@@ -20,7 +20,6 @@
 
 package password.pwm.util;
 
-import password.pwm.util.java.Percent;
 import password.pwm.util.java.TimeDuration;
 
 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 password.pwm.util.java.TimeDuration;
 
-import java.util.Objects;
+import java.time.Duration;
 
 public class TransactionSizeCalculator
 {
@@ -42,32 +42,29 @@ public class TransactionSizeCalculator
     public void reset( )
     {
         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( )
     {
-        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();
     }
 
-    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 );
 
         int newTransactionSize;
-        if ( duration.isShorterThan( settings.getDurationGoal() ) )
+        if ( duration < ( settings.getDurationGoal() ) )
         {
             if ( difference > closeThreshold )
             {
@@ -78,7 +75,7 @@ public class TransactionSizeCalculator
                 newTransactionSize = transactionSize + 1;
             }
         }
-        else if ( duration.isLongerThan( settings.getDurationGoal() ) )
+        else if ( duration > ( settings.getDurationGoal() ) )
         {
             if ( difference > ( 10 * durationGoalMs ) )
             {
@@ -117,7 +114,7 @@ public class TransactionSizeCalculator
     public static class Settings
     {
         @Builder.Default
-        private TimeDuration durationGoal = TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS );
+        private long durationGoal = 100;
 
         @Builder.Default
         private int maxTransactions = 5003;
@@ -142,12 +139,7 @@ public class TransactionSizeCalculator
                 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" );
             }

+ 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.BigInteger;
 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.ReentrantReadWriteLock;
 
 public class AverageTracker
 {
     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();
 
     public AverageTracker( final int maxSamples )
     {
-        this.maxSamples = maxSamples;
+        this.maxSamples = maxSamples - 1;
+        this.samples = new AtomicLongArray( maxSamples );
     }
 
     public void addSample( final long input )
@@ -45,11 +48,9 @@ public class AverageTracker
         lock.writeLock().lock();
         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
         {
@@ -62,23 +63,55 @@ public class AverageTracker
         lock.readLock().lock();
         try
         {
-            if ( samples.isEmpty() )
+            if ( top.get() == 0 )
             {
                 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
         {
             lock.readLock().unlock();
         }
+
     }
 
     public long avgAsLong( )
     {
         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.EnumMap;
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -37,23 +38,28 @@ import java.util.Set;
 import java.util.Spliterator;
 import java.util.Spliterators;
 import java.util.function.Function;
-import java.util.stream.Collector;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
-public class CollectionUtil
+public final class CollectionUtil
 {
+    private CollectionUtil()
+    {
+    }
+
     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 )
     {
         if ( input == null )
         {
-            return Collections.emptyList();
+            return List.of();
         }
 
         return input.stream()
@@ -65,7 +71,7 @@ public class CollectionUtil
     {
         if ( input == null )
         {
-            return Collections.emptySet();
+            return Set.of();
         }
 
         return input.stream()
@@ -77,42 +83,45 @@ public class CollectionUtil
     {
         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 )
     {
-        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 )
     {
-        if ( inputs == null )
+        if ( CollectionUtil.isEmpty( inputs ) )
         {
             return Collections.emptySet();
         }
 
         final Set<E> set = inputs.stream()
-                .map( input -> JavaHelper.readEnumFromString( enumClass, input ) )
+                .filter( Objects::nonNull )
+                .map( input -> EnumUtil.readEnumFromString( enumClass, input ) )
                 .flatMap( Optional::stream )
                 .collect( Collectors.toSet() );
 
@@ -131,10 +140,16 @@ public class CollectionUtil
             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() ),
-                        Map.Entry::getValue ) ) );
+                        Map.Entry::getValue ) );
     }
 
     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 );
     }
 
-    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 )
                 .collect( Collectors.toUnmodifiableList() );
     }
@@ -176,51 +193,56 @@ public class CollectionUtil
      * {@link Collections#unmodifiableMap(Map)}.
      */
     @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.Instant;
-import java.time.temporal.ChronoUnit;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicReference;
 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
  * 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 BooleanSupplier predicate;
@@ -53,7 +52,6 @@ public class ConditionalTaskExecutor
         {
             if ( predicate.getAsBoolean() )
             {
-
                 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.predicate = Objects.requireNonNull( predicate );
@@ -77,7 +75,8 @@ public class ConditionalTaskExecutor
     public static ConditionalTaskExecutor forPeriodicTask(
             final Runnable task,
             final Duration timeDuration,
-            final Duration firstExecutionDelay )
+            final Duration firstExecutionDelay
+    )
     {
         return new ConditionalTaskExecutor( task, new TimeDurationPredicate( timeDuration, firstExecutionDelay ) );
     }
@@ -101,7 +100,7 @@ public class ConditionalTaskExecutor
 
         private void setNextTimeFromNow( final Duration duration )
         {
-            nextExecuteTimestamp.set( Instant.now().plus( duration.toMillis(), ChronoUnit.MILLIS ) );
+            nextExecuteTimestamp.set( Instant.now().plus( duration ) );
         }
 
         @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.Optional;
 import java.util.Properties;
-import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.LongAccumulator;
 import java.util.function.Predicate;
-import java.util.stream.Collectors;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
 
-public class JavaHelper
+public final class JavaHelper
 {
     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 )
     {
-        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 )
@@ -182,24 +121,6 @@ public class JavaHelper
         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 )
             throws IOException
     {
@@ -310,6 +231,7 @@ public class JavaHelper
 
     /**
      * Copy of {@link ThreadInfo#toString()} but with the MAX_FRAMES changed from 8 to 256.
+     *
      * @param threadInfo thread information
      * @return a stacktrace string with newline formatting
      */
@@ -520,7 +442,7 @@ public class JavaHelper
     public static byte[] gunzip( final byte[] bytes )
             throws IOException
     {
-        try (  GZIPInputStream inputGzipStream = new GZIPInputStream( new ByteArrayInputStream( bytes ) ) )
+        try ( GZIPInputStream inputGzipStream = new GZIPInputStream( new ByteArrayInputStream( bytes ) ) )
         {
             return inputGzipStream.readAllBytes();
         }
@@ -547,6 +469,7 @@ public class JavaHelper
 
     /**
      * Append multiple byte array values into a single array.
+     *
      * @param byteArrays two or more byte arrays.
      * @return A new array with the contents of all byteArrays appended
      */
@@ -605,4 +528,10 @@ public class JavaHelper
         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;
 
 /**
- * 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.
  */
-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.
  */
 
-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;
 
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
 import java.text.NumberFormat;
 import java.util.Locale;
 
@@ -42,4 +46,12 @@ public class PwmNumberFormat
         final NumberFormat numberFormat = NumberFormat.getInstance( locale );
         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;
 
+import password.pwm.util.MovingAverage;
+
 import java.time.Duration;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.EnumMap;
 import java.util.Map;
 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 )
     {
         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 )
@@ -70,11 +69,10 @@ public class StatisticAverageBundle<K extends Enum<K>>
 
     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()

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

@@ -20,10 +20,10 @@
 
 package password.pwm.util.java;
 
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.EnumMap;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.atomic.LongAccumulator;
 import java.util.stream.Collectors;
 
@@ -34,9 +34,9 @@ public class StatisticCounterBundle<K extends Enum<K>>
 
     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 )
@@ -55,12 +55,12 @@ public class StatisticCounterBundle<K extends Enum<K>>
         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.StandardCharsets;
 import java.text.NumberFormat;
+import java.time.Duration;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
@@ -51,8 +52,12 @@ import java.util.function.Function;
 import java.util.function.Predicate;
 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 Base64.Decoder B64_MIME_DECODER = Base64.getMimeDecoder();
@@ -247,6 +252,11 @@ public abstract class StringUtil
         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
     {
         GZIP,
@@ -319,7 +329,7 @@ public abstract class StringUtil
         }
 
         final byte[] decodedBytes;
-        if ( JavaHelper.enumArrayContainsValue( options, Base64Options.URL_SAFE ) )
+        if ( EnumUtil.enumArrayContainsValue( options, Base64Options.URL_SAFE ) )
         {
             decodedBytes = B64_URL_DECODER.decode( input.toString() );
         }
@@ -328,7 +338,7 @@ public abstract class StringUtil
             decodedBytes = B64_MIME_DECODER.decode( input.toString() );
         }
 
-        if ( JavaHelper.enumArrayContainsValue( options, Base64Options.GZIP ) )
+        if ( EnumUtil.enumArrayContainsValue( options, Base64Options.GZIP ) )
         {
             return JavaHelper.gunzip( decodedBytes );
         }
@@ -341,7 +351,7 @@ public abstract class StringUtil
     public static String base64Encode( final byte[] input, final StringUtil.Base64Options... options )
     {
         final byte[] compressedBytes;
-        if ( JavaHelper.enumArrayContainsValue( options, Base64Options.GZIP ) )
+        if ( EnumUtil.enumArrayContainsValue( options, Base64Options.GZIP ) )
         {
             try
             {
@@ -357,7 +367,7 @@ public abstract class StringUtil
             compressedBytes = input;
         }
 
-        if ( JavaHelper.enumArrayContainsValue( options, Base64Options.URL_SAFE ) )
+        if ( EnumUtil.enumArrayContainsValue( options, Base64Options.URL_SAFE ) )
         {
             return B64_URL_ENCODER.encodeToString( compressedBytes );
         }
@@ -389,20 +399,13 @@ public abstract class StringUtil
             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 )
@@ -768,24 +771,28 @@ public abstract class StringUtil
      */
     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 )
                 || "yes".equalsIgnoreCase( string )
-                || "y".equalsIgnoreCase( string )
-        );
+                || "y".equalsIgnoreCase( string );
     }
 
     public static List<String> tokenizeString(
             final String inputString,
-            final String seperator
+            final String separator
     )
     {
-        if ( inputString == null || inputString.length() < 1 )
+        if ( StringUtil.isEmpty( inputString ) )
         {
             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 );
     }
 }

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

@@ -20,8 +20,8 @@
 
 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
 {
@@ -33,16 +33,16 @@ public class AtomicLoopIntIncrementerTest
         for ( int i = 0; i < 5; i++ )
         {
             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++ )
         {
             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;
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
 
 public class AverageTrackerTest
 {
@@ -34,7 +34,7 @@ public class AverageTrackerTest
         averageTracker.addSample( 7 );
         averageTracker.addSample( 8 );
         averageTracker.addSample( 9 );
-        Assert.assertEquals( 7, averageTracker.avgAsLong() );
+        Assertions.assertEquals( 7, averageTracker.avgAsLong() );
     }
 
     @Test
@@ -48,7 +48,7 @@ public class AverageTrackerTest
         averageTracker.addSample( 9 );
         averageTracker.addSample( 10 );
         averageTracker.addSample( 15 );
-        Assert.assertEquals( 9, averageTracker.avgAsLong() );
+        Assertions.assertEquals( 9, averageTracker.avgAsLong() );
     }
 
     @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_804L  );
         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;
 
 
-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.ByteArrayOutputStream;
@@ -64,7 +64,7 @@ public class CopyingInputStreamTest
 
 
 
-    @Before
+    @BeforeEach
     public void setUp() throws Exception
     {
         final InputStream input = new ByteArrayInputStream( "abc".getBytes( ASCII ) );
@@ -75,70 +75,70 @@ public class CopyingInputStreamTest
     @Test
     public void testReadNothing() throws Exception
     {
-        Assert.assertEquals( "", new String( output.bytes(), ASCII ) );
+        Assertions.assertEquals( "", new String( output.bytes(), ASCII ) );
     }
 
     @Test
     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
     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
     public void testReadToArray() throws Exception
     {
         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
     public void testReadToArrayWithOffset() throws Exception
     {
         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
     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
     public void testMarkReset() throws Exception
     {
-        Assert.assertEquals( 'a', copyingStream.read() );
+        Assertions.assertEquals( 'a', copyingStream.read() );
         copyingStream.mark( 1 );
-        Assert.assertEquals( 'b', copyingStream.read() );
+        Assertions.assertEquals( 'b', copyingStream.read() );
         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;
 
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
 
 public class JavaHelperTest
 {
@@ -39,7 +39,7 @@ public class JavaHelperTest
                 };
 
         final byte[] output = JavaHelper.concatByteArrays( byteArray1, byteArray2 );
-        Assert.assertArrayEquals( new byte[]
+        Assertions.assertArrayEquals( new byte[]
                 {
                         0, 122, 5, 6, 121, 19,
                 },
@@ -66,7 +66,7 @@ public class JavaHelperTest
                 };
 
         final byte[] output = JavaHelper.concatByteArrays( byteArray1, byteArray2, byteArray3, byteArray4 );
-        Assert.assertArrayEquals( new byte[]
+        Assertions.assertArrayEquals( new byte[]
                 {
                         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;
 
 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;
 
@@ -33,7 +33,7 @@ public class StringUtilTest
     {
         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.";
-        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 expected = "thisisateststringsecondline";
-        Assert.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
+        Assertions.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
     }
 
     @Test
@@ -50,7 +50,7 @@ public class StringUtilTest
     {
         final String input = " this is a \n test \t string\r\nsecond line ";
         final String expected = "thisisateststringsecondline";
-        Assert.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
+        Assertions.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
     }
 
     @Test
@@ -58,7 +58,7 @@ public class StringUtilTest
     {
         final String input = "nochangetest";
         final String expected = "nochangetest";
-        Assert.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
+        Assertions.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
     }
 
     @Test
@@ -92,7 +92,7 @@ public class StringUtilTest
                 + "sLd5kinMLYBq8I4g4Xmk/gNHE+r1hspZcX30BJZr01lYPf7TMSVcGDiEo+afgv2MW5gxTs14nhr9hctJqvIni5ly/D6q1UEL2tU2ob8cbkdJf17ZSHwD2f2L"
                 + "SaCYJkJA69aSEaRkCldUxPUd1gJea6zuxICaEnL6VpPX/78whQYwvwt/Tv9XBZ0k7YXDK/umdaisLRbvfXknsuvCnQsH6qqF0wGjIChBWUMo0oHjqvbsezt3"
                 + "tkBigAVBRQHvFwY+3sAzm2fTYS5yh+Rp/BIAV0AecPUeybQ=";
-        Assert.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
+        Assertions.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
     }
 
     @Test
@@ -163,7 +163,7 @@ public class StringUtilTest
                 + "g";
         final String expected = "H4sIAAAAAAAAAKR7A5Rly5ZtVlbatm3btm3bNipt27Ztm5W2baPS/97X/bvve79H9e3+Z4w9ttaaJ2LuWDNW7IgtJ/kdCAIADAwMQNViXcL1eWngGAAAIOcb"
                 + "AADSH3tpYSV+anEZEVppfhlxEWFFJRppEe/YTYlBOrig6+/uIVw/sDUi8JBprZYhUSn8d6qkEupMWKUt4rXbba+HabTf+yr8HHmmJzNRqnwK4BpQ7/g";
-        Assert.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
+        Assertions.assertEquals( expected, StringUtil.stripAllWhitespace( input ) );
     }
 
     @Test
@@ -173,9 +173,9 @@ public class StringUtilTest
 
         final String original = RandomStringUtils.random( 1024 * 1024, true, true );
         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 );
-        Assert.assertEquals( original, stripped );
+        Assertions.assertEquals( original, stripped );
     }
 
     @Test
@@ -184,14 +184,14 @@ public class StringUtilTest
     {
         final String input = "0�\u0000\u0000\u0000\u0007\u0002\u0001\u0001\u0002\u0002�\u007F";
         final String expected = "0�?????????�?";
-        Assert.assertEquals( expected, StringUtil.cleanNonPrintableCharacters( input ) );
+        Assertions.assertEquals( expected, StringUtil.cleanNonPrintableCharacters( input ) );
     }
 
     @Test
     public void urlPathEncodeTest()
     {
         final String input = "dsad(dsadaasds)dsdasdad";
-        Assert.assertEquals( "dsad%28dsadaasds%29dsdasdad", StringUtil.urlPathEncode( input ) );
+        Assertions.assertEquals( "dsad%28dsadaasds%29dsdasdad", StringUtil.urlPathEncode( input ) );
     }
 
     @Test
@@ -217,7 +217,7 @@ public class StringUtilTest
                 + "IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKB"
                 + "IFAUCQKB";
 
-        Assert.assertEquals( expectedValue, b32value );
+        Assertions.assertEquals( expectedValue, b32value );
     }
 
     private static byte[] makeB64inputByteArray()
@@ -259,55 +259,75 @@ public class StringUtilTest
     public void base64TestEncode() throws Exception
     {
         final String b64string = StringUtil.base64Encode( makeB64inputByteArray() );
-        Assert.assertEquals( B64_TEST, b64string );
+        Assertions.assertEquals( B64_TEST, b64string );
     }
 
     @Test
     public void base64TestDecode() throws Exception
     {
         final byte[] b64array = StringUtil.base64Decode( B64_TEST );
-        Assert.assertArrayEquals( makeB64inputByteArray(), b64array );
+        Assertions.assertArrayEquals( makeB64inputByteArray(), b64array );
     }
 
     @Test
     public void base64TestEncodeUrlSafe() throws Exception
     {
         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
     public void base64TestDecodeUrlSafe() throws Exception
     {
         final byte[] b64array = StringUtil.base64Decode( B64_TEST_URL_SAFE, StringUtil.Base64Options.URL_SAFE );
-        Assert.assertArrayEquals( makeB64inputByteArray(), b64array );
+        Assertions.assertArrayEquals( makeB64inputByteArray(), b64array );
     }
 
     @Test
     public void base64TestEncodeGzipAndUrlSafe() throws Exception
     {
         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
     public void base64TestDecodeGzipAndUrlSafe() throws Exception
     {
         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
     public void base64TestEncodeGzip() throws Exception
     {
         final String b64string = StringUtil.base64Encode( makeB64inputByteArray(), StringUtil.Base64Options.GZIP );
-        Assert.assertEquals( B64_TEST_GZIP, b64string );
+        Assertions.assertEquals( B64_TEST_GZIP, b64string );
     }
 
     @Test
     public void base64TestDecodeGzip() throws Exception
     {
         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>
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>pwm-onejar</artifactId>
-
     <packaging>jar</packaging>
 
     <name>PWM Password Self Service: Executable Server JAR</name>
 
     <properties>
-        <tomcat.version>9.0.64</tomcat.version>
+        <tomcat.version>9.0.65</tomcat.version>
     </properties>
 
     <build>
@@ -25,7 +25,7 @@
                 <!-- prevent normal jar from being built -->
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.3.0</version>
                 <executions>
                     <execution>
                         <id>default-jar</id>
@@ -40,7 +40,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-assembly-plugin</artifactId>
-                <version>3.3.0</version>
+                <version>3.4.2</version>
                 <configuration>
                     <appendAssemblyId>false</appendAssemblyId>
                     <descriptors>

+ 20 - 15
pom.xml

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

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

@@ -9,6 +9,7 @@
 
     <modelVersion>4.0.0</modelVersion>
 
+    <url>https://github.com/pwm-project/pwm</url>
     <artifactId>rest-test-service</artifactId>
     <packaging>war</packaging>
 
@@ -76,16 +77,6 @@
         <!-- / container 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>
             <groupId>org.pwm-project</groupId>
             <artifactId>pwm-lib-util</artifactId>

+ 27 - 20
server/pom.xml

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

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

@@ -38,7 +38,9 @@ public enum AppAttribute
     CONFIG_LOGIN_HISTORY( "config.loginHistory" ),
     LOCALDB_LOGGER_STORAGE_FORMAT( "localdb.logger.storage.format" ),
     TELEMETRY_LAST_PUBLISH_TIMESTAMP( "telemetry.lastPublish.timestamp" ),
-    VERSION_CHECK_CACHE( "versionCheckInfoCache" ),;
+    VERSION_CHECK_CACHE( "versionCheckInfoCache" ),
+    REPORT_COUNTER( "report.counter" ),;
+
 
     private final String key;
 

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

@@ -20,7 +20,7 @@
 
 package password.pwm;
 
-import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.EnumUtil;
 
 import java.util.Objects;
 import java.util.Optional;
@@ -80,6 +80,7 @@ public enum AppProperty
     CONFIG_EDITOR_BLOCK_OLD_IE                      ( "configEditor.blockOldIE" ),
     CONFIG_EDITOR_USER_PERMISSION_MATCH_LIMIT       ( "configEditor.userPermission.matchResultsLimit" ),
     CONFIG_EDITOR_IDLE_TIMEOUT                      ( "configEditor.idleTimeoutSeconds" ),
+    CONFIG_EDITOR_SETTING_FUNCTION_TIMEOUT_MS       ( "configEditor.settingFunction.timeoutMs" ),
     CONFIG_GUIDE_IDLE_TIMEOUT                       ( "configGuide.idleTimeoutSeconds" ),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGBYTES             ( "configManager.zipDebug.maxLogBytes" ),
     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_THREAD_MAX                 ( "ldap.search.parallel.threadMax" ),
     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_FILE_MAX_SIZE                           ( "logging.file.maxSize" ),
     LOGGING_FILE_MAX_ROLLOVER                       ( "logging.file.maxRollover" ),
@@ -285,6 +289,8 @@ public enum AppProperty
     OTP_ENCRYPTION_ALG                              ( "otp.encryptionAlg" ),
     PASSWORD_RANDOMGEN_MAX_ATTEMPTS                 ( "password.randomGenerator.maxAttempts" ),
     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" ),
 
     /* 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;
     }
 
+    public boolean isDefaultValue( final String value )
+    {
+        return Objects.equals( defaultValue, value );
+    }
+
     public String getDescription( )
     {
         return readAppPropertiesBundle( this.getKey() + DESCRIPTION_SUFFIX );
@@ -439,8 +450,6 @@ public enum AppProperty
 
     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;
 
-import lombok.Value;
 import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Display;
 import password.pwm.ldap.LdapDomainService;
 import password.pwm.svc.db.DatabaseService;
 import password.pwm.util.i18n.LocaleHelper;
-import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.CollectorUtil;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
@@ -37,10 +37,8 @@ import java.lang.management.ManagementFactory;
 import java.nio.charset.Charset;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
-import java.util.Collections;
 import java.util.Map;
 import java.util.Optional;
-import java.util.TreeMap;
 import java.util.function.Function;
 
 public enum PwmAboutProperty
@@ -58,7 +56,6 @@ public enum PwmAboutProperty
     app_applicationPath( null, pwmApplication -> pwmApplication.getPwmEnvironment().getApplicationPath().getAbsolutePath() ),
     app_environmentFlags( null, pwmApplication -> StringUtil.collectionToString( pwmApplication.getPwmEnvironment().getFlags() ) ),
     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_sharedHistoryOldestTime( null, pwmApplication -> format( pwmApplication.getSharedHistoryManager().getOldestEntryTime() ) ),
     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 );
 
-    @Value
-    private static class Pair<K, V>
-    {
-        private final K key;
-        private final V value;
-    }
-
     public static Map<PwmAboutProperty, String> makeInfoBean(
             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() )
-                .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;
     }
 
-    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()
     {
         try

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

@@ -22,6 +22,7 @@ package password.pwm;
 
 import lombok.Value;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.config.AppConfig;
 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.StatisticsClient;
 import password.pwm.svc.stats.StatisticsService;
-import password.pwm.svc.wordlist.SeedlistService;
 import password.pwm.svc.wordlist.SharedHistoryService;
 import password.pwm.svc.wordlist.WordlistService;
 import password.pwm.util.MBeanUtility;
@@ -113,9 +113,7 @@ public class PwmApplication
             throws PwmUnrecoverableException
     {
         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(
                 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
                 + " closed for bidness, cya!", TimeDuration.fromCurrent( startTime ) );
+
+        if ( !pwmEnvironment.isInternalRuntimeInstance() )
+        {
+            PwmLogManager.disableLogging();
+        }
     }
 
     public String getInstanceID( )
@@ -451,7 +454,6 @@ public class PwmApplication
 
         if ( localDB == null || localDB.status() != LocalDB.Status.OPEN )
         {
-            LOGGER.debug( () -> "error retrieving key '" + appAttribute.getKey() + "', localDB unavailable: " );
             return Optional.empty();
         }
 
@@ -475,7 +477,7 @@ public class PwmApplication
         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
         {
@@ -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() );
     }
@@ -519,14 +521,14 @@ public class PwmApplication
     @Value
     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 );
         }
 
-        public Map<DomainID, Map<String, ErrorInformation>> getRecords()
+        public Map<DomainID, Map<ProfileID, ErrorInformation>> getRecords()
         {
             // required because json deserialization can still set records == null
             return records == null ? Collections.emptyMap() : records;
@@ -534,9 +536,9 @@ public class PwmApplication
 
         StoredErrorRecords addDomainErrorMap(
                 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 ) );
             return new StoredErrorRecords( newRecords );
         }
@@ -566,7 +568,6 @@ public class PwmApplication
 
         if ( localDB == null || localDB.status() != LocalDB.Status.OPEN )
         {
-            LOGGER.error( () -> "error writing key '" + appAttribute.getKey() + "', localDB unavailable: " );
             return;
         }
 
@@ -589,7 +590,7 @@ public class PwmApplication
         }
         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
             {
                 localDB.remove( LocalDB.DB.PWM_META, appAttribute.getKey() );
@@ -683,11 +684,6 @@ public class PwmApplication
         return ( WordlistService ) pwmServiceManager.getService( PwmServiceEnum.WordlistService );
     }
 
-    public SeedlistService getSeedlistManager( )
-    {
-        return ( SeedlistService ) pwmServiceManager.getService( PwmServiceEnum.SeedlistService );
-    }
-
     public ReportService getReportService( )
     {
         return ( ReportService ) pwmServiceManager.getService( PwmServiceEnum.ReportService );

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

@@ -21,7 +21,6 @@
 package password.pwm;
 
 import password.pwm.bean.DomainID;
-import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationUtil;
@@ -31,6 +30,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.cli.commands.ExportHttpsTomcatConfigCommand;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.CollectorUtil;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.StringUtil;
 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.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogManager;
+import password.pwm.util.logging.PwmLogSettings;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.HttpsServerCertificateManager;
 import password.pwm.util.secure.PwmRandom;
@@ -100,49 +101,30 @@ class PwmApplicationUtil
     {
         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(
@@ -310,7 +292,7 @@ class PwmApplicationUtil
         LOGGER.trace( pwmApplication.getSessionLabel(), () -> "--begin current configuration output for domainID '" + domainID + "'--" );
         debugStrings.entrySet().stream()
                 .map( valueFormatter )
-                .map( s -> ( Supplier<CharSequence> ) () -> s )
+                .map( s -> ( Supplier<String> ) () -> s )
                 .forEach( s -> LOGGER.trace( pwmApplication.getSessionLabel(), s ) );
 
         final long itemCount = debugStrings.size();
@@ -321,7 +303,7 @@ class PwmApplicationUtil
     static void outputNonDefaultPropertiesToLog( final PwmApplication pwmApplication )
     {
         final Map<String, String> data = pwmApplication.getConfig().readAllNonDefaultAppProperties().entrySet().stream()
-                .collect( CollectionUtil.collectorToLinkedMap(
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
                         entry -> "AppProperty: " + entry.getKey().getKey(),
                         Map.Entry::getValue ) );
 
@@ -331,7 +313,7 @@ class PwmApplicationUtil
     static void outputApplicationInfoToLog( final PwmApplication pwmApplication )
     {
         final Map<String, String> data = PwmAboutProperty.makeInfoBean( pwmApplication ).entrySet().stream()
-                .collect( CollectionUtil.collectorToLinkedMap(
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
                         entry -> "AboutProperty: " + entry.getKey().getLabel(),
                         Map.Entry::getValue ) );
 
@@ -350,7 +332,7 @@ class PwmApplicationUtil
         {
             final String separator = " -> ";
             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 ) );
         }
         else
@@ -363,7 +345,7 @@ class PwmApplicationUtil
 
     private static boolean checkIfOutputDumpingEnabled( final PwmApplication pwmApplication )
     {
-        return LOGGER.isEnabled( PwmLogLevel.TRACE )
+        return LOGGER.isInterestingLevel( PwmLogLevel.TRACE )
                 && !pwmApplication.getPwmEnvironment().isInternalRuntimeInstance()
                 && 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" );
 
     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";
 
@@ -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_DOMAIN = "domain";
     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" );
 

+ 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.provider.ChaiProvider;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 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.state.SessionStateService;
 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.PwmServiceEnum;
 import password.pwm.svc.PwmServiceManager;
@@ -86,9 +87,7 @@ public class PwmDomain
         this.pwmApplication = Objects.requireNonNull( pwmApplication );
         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 ) );
     }
@@ -159,7 +158,7 @@ public class PwmDomain
         return pwmApplication.determineIfDetailErrorMsgShown();
     }
 
-    public LdapDomainService getLdapConnectionService( )
+    public LdapDomainService getLdapService( )
     {
         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
     {
-        return getLdapConnectionService().getProxyChaiProvider( sessionLabel, profileId );
+        return getLdapService().getProxyChaiProvider( sessionLabel, profileId );
     }
 
     public List<PwmService> getPwmServices( )
@@ -199,9 +198,9 @@ public class PwmDomain
         return pwmServiceManager.getRunningServices();
     }
 
-    public UserSearchEngine getUserSearchEngine()
+    public UserSearchService getUserSearchEngine()
     {
-        return ( UserSearchEngine ) pwmServiceManager.getService( PwmServiceEnum.UserSearchEngine );
+        return ( UserSearchService ) pwmServiceManager.getService( PwmServiceEnum.UserSearchEngine );
     }
 
     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.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.util.java.CollectionUtil;
 import password.pwm.util.java.TimeDuration;
@@ -31,11 +32,9 @@ import password.pwm.util.logging.PwmLogger;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
@@ -136,7 +135,7 @@ class PwmDomainUtil
                         + "' configuration modification detected as: " + modifyCategory ) ) );
 
         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() );
 
 
@@ -196,49 +195,90 @@ class PwmDomainUtil
 
     enum DomainModifyCategory
     {
-        obsolete,
+        removed,
         unchanged,
         modified,
         created,
     }
 
-    private static Map<DomainModifyCategory, Set<DomainID>> categorizeDomainModifications(
+    public static Map<DomainModifyCategory, Set<DomainID>> categorizeDomainModifications(
             final AppConfig newConfig,
             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() );
             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 SessionLabel SESSION_LABEL = SessionLabel.SYSTEM_LABEL;
+
     @lombok.Builder.Default
     private PwmApplicationMode applicationMode = PwmApplicationMode.ERROR;
 
@@ -72,7 +74,7 @@ public class PwmEnvironment
     @Singular
     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
     {
@@ -179,7 +181,7 @@ public class PwmEnvironment
         }
         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() )
         {
@@ -234,7 +236,7 @@ public class PwmEnvironment
         }
 
         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() )
         {
             final String errorMsg = "The file " + infoFile.getAbsolutePath() + " exists, and an applicationPath was not explicitly specified."
@@ -321,7 +323,7 @@ public class PwmEnvironment
                 }
                 else
                 {
-                    LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "unknown " + EnvironmentParameter.applicationFlags + " value: " + input );
+                    LOGGER.warn( SESSION_LABEL, () -> "unknown " + EnvironmentParameter.applicationFlags + " value: " + input );
                 }
             }
             return returnFlags;
@@ -341,7 +343,7 @@ public class PwmEnvironment
             }
             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() );
             }
 
@@ -358,14 +360,14 @@ public class PwmEnvironment
                     }
                     else
                     {
-                        LOGGER.warn( SessionLabel.SYSTEM_LABEL, () -> "unknown " + EnvironmentParameter.applicationParamFile + " value: " + input );
+                        LOGGER.warn( SESSION_LABEL, () -> "unknown " + EnvironmentParameter.applicationParamFile + " value: " + input );
                     }
                 }
                 return Collections.unmodifiableMap( returnParams );
             }
             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();
@@ -376,7 +378,7 @@ public class PwmEnvironment
     {
         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;
         }
 

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

@@ -20,23 +20,27 @@
 
 package password.pwm.bean;
 
-import password.pwm.config.PwmSetting;
+import password.pwm.PwmConstants;
 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.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 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.
     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 )
     {
-        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 )
@@ -73,7 +72,7 @@ public class DomainID implements Comparable<DomainID>, Serializable
                 return !this.isSystem();
 
             default:
-                MiscUtil.unhandledSwitchStatement( scope );
+                PwmUtil.unhandledSwitchStatement( scope );
         }
 
         return false;
@@ -97,7 +96,7 @@ public class DomainID implements Comparable<DomainID>, Serializable
     @Override
     public int hashCode()
     {
-        return Objects.hash( domainID );
+        return Objects.hashCode( domainID );
     }
 
     @Override
@@ -124,6 +123,32 @@ public class DomainID implements Comparable<DomainID>, Serializable
 
     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 password.pwm.user.UserInfoBean;
-import password.pwm.util.java.MovingAverage;
+import password.pwm.util.MovingAverage;
 import password.pwm.util.java.TimeDuration;
 
 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;
 
+import lombok.AccessLevel;
 import lombok.Builder;
 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.user.UserInfo;
+import password.pwm.util.java.AtomicLoopLongIncrementer;
 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.util.Objects;
 
 @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
 {
-    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 requestID;
-    private final String userID;
     private final String username;
     private final String sourceAddress;
     private final String sourceHostname;
     private final String profile;
     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()
-                .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() )
                 .domain( domainID.stringValue() )
                 .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 String sessionID = getSessionID();
@@ -71,15 +202,20 @@ public class SessionLabel implements Serializable
         {
             sb.append( sessionID );
         }
+
         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 )
             {
@@ -97,4 +233,13 @@ public class SessionLabel implements Serializable
         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;
 
 import com.novell.ldapchai.ChaiUser;
-import com.novell.ldapchai.exception.ChaiException;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.jetbrains.annotations.NotNull;
 import password.pwm.PwmApplication;
+import password.pwm.PwmDomain;
 import password.pwm.config.AppConfig;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 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.json.JsonFactory;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
 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 long serialVersionUID = 1L;
 
-    private static final String CRYPO_HEADER = "ui_C-";
     private static final String DELIM_SEPARATOR = "|";
 
     private static final Comparator<UserIdentity> COMPARATOR = Comparator.comparing(
@@ -58,16 +53,16 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
             Comparator.nullsLast( Comparator.naturalOrder() ) )
             .thenComparing(
                     UserIdentity::getLdapProfileID,
-                    Comparator.nullsLast( Comparator.naturalOrder() ) )
+                    ProfileID.comparator()
+            )
             .thenComparing(
                     UserIdentity::getDomainID,
-                    Comparator.nullsLast( Comparator.naturalOrder() ) );
+                    DomainID.comparator() );
 
-    private transient String obfuscatedValue;
     private transient boolean canonical;
 
     private final String userDN;
-    private final String ldapProfile;
+    private final ProfileID ldapProfile;
     private final DomainID domainID;
 
     public enum Flag
@@ -75,14 +70,14 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         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.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 );
     }
 
-    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.canonical = canonical;
@@ -90,12 +85,12 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
 
     public static UserIdentity create(
             final String userDN,
-            final String ldapProfile,
+            final ProfileID ldapProfile,
             final DomainID domainID,
             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 );
     }
 
@@ -109,7 +104,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         return domainID;
     }
 
-    public String getLdapProfileID( )
+    public ProfileID getLdapProfileID( )
     {
         return ldapProfile;
     }
@@ -130,41 +125,6 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         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( )
     {
         return JsonFactory.get().serialize( this );
@@ -174,30 +134,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
     {
         return "[" + this.getDomainID() + "]"
                 + " " + 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 )
@@ -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" ) );
         }
-        final String profileID = st.nextToken();
+        final ProfileID profileID = ProfileID.create( st.nextToken() );
         final String userDN = st.nextToken();
         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 )
             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 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.bean.DomainID;
 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.DataStorageMethod;
 import password.pwm.config.profile.EmailServerProfile;
@@ -41,30 +41,26 @@ import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.CollectorUtil;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
-import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmSecurityKey;
 
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.EnumMap;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
-import java.util.TreeMap;
+import java.util.SortedMap;
 import java.util.TreeSet;
 import java.util.function.Function;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
 
 public class AppConfig implements SettingReader
 {
@@ -79,13 +75,13 @@ public class AppConfig implements SettingReader
     private final Map<AppProperty, String> appPropertyOverrides;
     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()
     {
         try
         {
-            return new AppConfig( StoredConfigurationFactory.newConfig() );
+            return forStoredConfig( StoredConfigurationFactory.newConfig() );
         }
         catch ( final PwmUnrecoverableException e )
         {
@@ -98,7 +94,7 @@ public class AppConfig implements SettingReader
         return DEFAULT_CONFIG.get();
     }
 
-    public AppConfig( final StoredConfiguration storedConfiguration )
+    private AppConfig( final StoredConfiguration storedConfiguration )
     {
         this.storedConfiguration = storedConfiguration;
         this.settingReader = new StoredSettingReader( storedConfiguration, null, DomainID.systemId() );
@@ -108,14 +104,17 @@ public class AppConfig implements SettingReader
 
         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 ) -> 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()
@@ -186,12 +185,11 @@ public class AppConfig implements SettingReader
 
     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()
@@ -222,11 +220,6 @@ public class AppConfig implements SettingReader
         return settingReader.readSettingAsStringArray( pwmSetting );
     }
 
-    public PwmLogLevel getEventLogLocalDBLevel()
-    {
-        return readSettingAsEnum( PwmSetting.EVENTS_LOCALDB_LOG_LEVEL, PwmLogLevel.class );
-    }
-
     public boolean isDevDebugMode()
     {
         return Boolean.parseBoolean( readAppProperty( AppProperty.LOGGING_DEV_OUTPUT ) );
@@ -301,7 +294,7 @@ public class AppConfig implements SettingReader
         return settingReader.readGenericStorageLocations( setting );
     }
 
-    public Map<String, EmailServerProfile> getEmailServerProfiles( )
+    public Map<ProfileID, EmailServerProfile> getEmailServerProfiles( )
     {
         return settingReader.getProfileMap( ProfileDefinition.EmailServers );
     }
@@ -335,8 +328,9 @@ public class AppConfig implements 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 );
         for ( final Map.Entry<String, String> stringEntry : stringMap.entrySet() )
@@ -344,16 +338,14 @@ public class AppConfig implements SettingReader
             AppProperty.forKey( stringEntry.getKey() )
                     .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()
@@ -361,12 +353,13 @@ public class AppConfig implements SettingReader
         final String gatewayUrl = readSettingAsString( PwmSetting.SMS_GATEWAY_URL );
         final String gatewayUser = readSettingAsString( PwmSetting.SMS_GATEWAY_USER );
         final PasswordData gatewayPass = readSettingAsPassword( PwmSetting.SMS_GATEWAY_PASSWORD );
-        if ( gatewayUrl == null || gatewayUrl.length() < 1 )
+
+        if ( StringUtil.isEmpty( gatewayUrl ) )
         {
             return false;
         }
 
-        if ( gatewayUser != null && gatewayUser.length() > 0 && ( gatewayPass == null ) )
+        if ( !StringUtil.isEmpty( gatewayUser ) && gatewayPass == null )
         {
             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 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 ) );
             }
             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 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

+ 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.EmailItemBean;
 import password.pwm.bean.PrivateKeyCertificate;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.option.TokenStorageMethod;
 import password.pwm.config.profile.ActivateUserProfile;
@@ -53,8 +54,8 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
 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 java.security.cert.X509Certificate;
@@ -74,15 +75,13 @@ import java.util.stream.Collectors;
  */
 public class DomainConfig implements SettingReader
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( DomainConfig.class );
-
     private final StoredConfiguration storedConfiguration;
     private final AppConfig appConfig;
     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 PwmSecurityKey domainSecurityKey;
 
@@ -93,22 +92,22 @@ public class DomainConfig implements SettingReader
         this.domainID = Objects.requireNonNull( domainID );
         this.settingReader = new StoredSettingReader( storedConfiguration, null, domainID );
 
-        this.cachedPasswordPolicy = Collections.unmodifiableMap( getPasswordProfileIDs().stream()
+        this.cachedPasswordPolicy = getPasswordProfileIDs().stream()
                 .map( profile -> PwmPasswordPolicy.createPwmPasswordPolicy( this, profile ) )
-                .collect( Collectors.toMap(
-                        PwmPasswordPolicy::getIdentifier,
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
+                        PwmPasswordPolicy::getId,
                         Function.identity()
-                ) ) );
+                ) );
 
-        this.cachedChallengeProfiles = Collections.unmodifiableMap( getChallengeProfileIDs().stream()
-                .collect( Collectors.toMap(
+        this.cachedChallengeProfiles = getChallengeProfileIDs().stream()
+                .collect( Collectors.toUnmodifiableMap(
                         Function.identity(),
-                        profileId -> Collections.unmodifiableMap( appConfig.getKnownLocales().stream()
-                                .collect( Collectors.toMap(
+                        profileId -> appConfig.getKnownLocales().stream()
+                                .collect( CollectorUtil.toUnmodifiableLinkedMap(
                                         Function.identity(),
                                         locale -> ChallengeProfile.readChallengeProfileFromConfig( domainID, profileId, locale, storedConfiguration )
-                                ) ) )
-                ) ) );
+                                ) )
+                ) );
 
         this.ldapProfiles = makeLdapProfileMap( this );
         this.domainSecurityKey = makeDomainSecurityKey( appConfig, settingReader.getValueHash() );
@@ -135,7 +134,7 @@ public class DomainConfig implements SettingReader
         return settingReader.readSettingAsUserPermission( setting );
     }
 
-    public Map<String, LdapProfile> getLdapProfiles( )
+    public Map<ProfileID, LdapProfile> getLdapProfiles( )
     {
         return ldapProfiles;
     }
@@ -192,12 +191,12 @@ public class DomainConfig implements SettingReader
         return settingReader.readLocalizedBundle( className, keyName );
     }
 
-    public List<String> getChallengeProfileIDs( )
+    public List<ProfileID> getChallengeProfileIDs( )
     {
         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 );
 
@@ -214,12 +213,12 @@ public class DomainConfig implements SettingReader
         return settingReader.readSettingAsLong( setting );
     }
 
-    public PwmPasswordPolicy getPasswordPolicy( final String profile )
+    public PwmPasswordPolicy getPasswordPolicy( final ProfileID profile )
     {
         return cachedPasswordPolicy.get( profile );
     }
 
-    public List<String> getPasswordProfileIDs( )
+    public List<ProfileID> getPasswordProfileIDs( )
     {
         return StoredConfigurationUtil.profilesForSetting( this.getDomainID(), PwmSetting.PASSWORD_PROFILE_LIST, storedConfiguration );
     }
@@ -271,7 +270,7 @@ public class DomainConfig implements SettingReader
 
     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( )
@@ -290,52 +289,52 @@ public class DomainConfig implements SettingReader
     }
 
     /* generic profile stuff */
-    public Map<String, NewUserProfile> getNewUserProfiles( )
+    public Map<ProfileID, NewUserProfile> getNewUserProfiles( )
     {
         return this.getProfileMap( ProfileDefinition.NewUser );
     }
 
-    public Map<String, ActivateUserProfile> getUserActivationProfiles( )
+    public Map<ProfileID, ActivateUserProfile> getUserActivationProfiles( )
     {
         return this.getProfileMap( ProfileDefinition.ActivateUser );
     }
 
-    public Map<String, HelpdeskProfile> getHelpdeskProfiles( )
+    public Map<ProfileID, HelpdeskProfile> getHelpdeskProfiles( )
     {
         return this.getProfileMap( ProfileDefinition.Helpdesk );
     }
 
-    public Map<String, PeopleSearchProfile> getPeopleSearchProfiles( )
+    public Map<ProfileID, PeopleSearchProfile> getPeopleSearchProfiles( )
     {
         return this.getProfileMap( ProfileDefinition.PeopleSearch );
     }
 
-    public Map<String, SetupOtpProfile> getSetupOTPProfiles( )
+    public Map<ProfileID, SetupOtpProfile> getSetupOTPProfiles( )
     {
         return this.getProfileMap( ProfileDefinition.SetupOTPProfile );
     }
 
-    public Map<String, SetupResponsesProfile> getSetupResponseProfiles( )
+    public Map<ProfileID, SetupResponsesProfile> getSetupResponseProfiles( )
     {
         return this.getProfileMap( ProfileDefinition.SetupResponsesProfile );
     }
 
-    public Map<String, UpdateProfileProfile> getUpdateAttributesProfile( )
+    public Map<ProfileID, UpdateProfileProfile> getUpdateAttributesProfile( )
     {
         return this.getProfileMap( ProfileDefinition.UpdateAttributes );
     }
 
-    public Map<String, ChangePasswordProfile> getChangePasswordProfile( )
+    public Map<ProfileID, ChangePasswordProfile> getChangePasswordProfile( )
     {
         return this.getProfileMap( ProfileDefinition.ChangePassword );
     }
 
-    public Map<String, ForgottenPasswordProfile> getForgottenPasswordProfiles( )
+    public Map<ProfileID, ForgottenPasswordProfile> getForgottenPasswordProfiles( )
     {
         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 );
     }
@@ -347,11 +346,14 @@ public class DomainConfig implements SettingReader
 
     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();
     }
@@ -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()
                 .filter( entry -> entry.getValue().isEnabled() )
-                .collect( CollectionUtil.collectorToLinkedMap(
+                .collect( CollectorUtil.toUnmodifiableLinkedMap(
                         Map.Entry::getKey,
-                        Map.Entry::getValue ) ) );
+                        Map.Entry::getValue ) );
     }
 
     private static PwmSecurityKey makeDomainSecurityKey(
@@ -430,4 +432,24 @@ public class DomainConfig implements SettingReader
     {
         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 password.pwm.PwmConstants;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.ValueFactory;
@@ -71,7 +72,7 @@ public enum PwmSetting
     DOMAIN_SYSTEM_ADMIN(
             "domain.system.adminDomain", PwmSettingSyntax.STRING, PwmSettingCategory.DOMAINS ),
     DOMAIN_DOMAIN_PATHS(
-            "domain.system.domainPaths", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.DOMAINS ),
+            "domain.system.domainPathsEnabled", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.DOMAINS ),
 
     // application settings
     APP_PROPERTY_OVERRIDES(
@@ -480,8 +481,6 @@ public enum PwmSetting
             "wordlistCaseSensitive", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.WORDLISTS ),
     PASSWORD_WORDLIST_WORDSIZE(
             "password.wordlist.wordSize", PwmSettingSyntax.NUMERIC, PwmSettingCategory.WORDLISTS ),
-    SEEDLIST_FILENAME(
-            "pwm.seedlist.location", PwmSettingSyntax.STRING, PwmSettingCategory.WORDLISTS ),
 
 
     // password policy profile settings
@@ -704,8 +703,6 @@ public enum PwmSetting
             "events.pwmDB.maxAge", PwmSettingSyntax.DURATION, PwmSettingCategory.LOGGING ),
     EVENTS_ALERT_DAILY_SUMMARY(
             "events.alert.dailySummary.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.LOGGING ),
-    EVENTS_JAVA_LOG4JCONFIG_FILE(
-            "events.java.log4jconfigFile", PwmSettingSyntax.STRING, PwmSettingCategory.LOGGING ),
 
     PASSWORD_STRENGTH_METER_TYPE(
             "password.strengthMeter.type", PwmSettingSyntax.SELECT, PwmSettingCategory.LOGGING ),
@@ -1277,6 +1274,16 @@ public enum PwmSetting
 
 
     // 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
     IP_PERMITTED_RANGE(
             "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 final transient Supplier<String> defaultMenuLocation = new LazySupplier<>(
+    private final transient Supplier<String> defaultMenuLocation = LazySupplier.create(
             () -> 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 ) );
 
-    private final transient Supplier<String> defaultLocaleDescription = new LazySupplier<>(
+    private final transient Supplier<String> defaultLocaleDescription = LazySupplier.create(
             () -> readDescription( this, PwmConstants.DEFAULT_LOCALE ) );
 
 
@@ -1473,11 +1480,11 @@ public enum PwmSetting
     }
 
     public String toMenuLocationDebug(
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale
     )
     {
-        if ( PwmConstants.DEFAULT_LOCALE.equals( locale ) && StringUtil.isEmpty( profileID ) )
+        if ( PwmConstants.DEFAULT_LOCALE.equals( locale ) && profileID == null )
         {
             return defaultMenuLocation.get();
         }
@@ -1544,7 +1551,7 @@ public enum PwmSetting
         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 );
         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 password.pwm.PwmConstants;
+import password.pwm.bean.ProfileID;
 import password.pwm.i18n.Config;
 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.LazySupplier;
 import password.pwm.util.macro.MacroRequest;
@@ -211,21 +213,21 @@ public enum PwmSettingCategory
     private static final Comparator<PwmSettingCategory> MENU_LOCATION_COMPARATOR = Comparator.comparing(
             ( 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 )
             .collect( Collectors.toList() ) ) );
 
     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 )
     {
@@ -313,7 +315,7 @@ public enum PwmSettingCategory
     }
 
     public String toMenuLocationDebug(
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale
     )
     {
@@ -322,7 +324,7 @@ public enum PwmSettingCategory
 
     private static String toMenuLocationDebugImpl(
             final PwmSettingCategory category,
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale
     )
     {
@@ -455,7 +457,7 @@ public enum PwmSettingCategory
         private static PwmSettingScope readScope( final PwmSettingCategory category )
         {
             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" ) );
         }
 
@@ -509,19 +511,17 @@ public enum PwmSettingCategory
 
         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 )
-                    .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 )
         {
-            final Set<PwmSetting> settings = EnumSet.allOf( PwmSetting.class )
+            return EnumSet.allOf( PwmSetting.class )
                     .stream()
                     .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.Value;
 import org.jrivard.xmlchai.XmlElement;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.macro.MacroRequest;
 
@@ -104,7 +105,7 @@ class PwmSettingMetaData
                     flagElement.getChildren( PwmSettingXml.XML_ELEMENT_FLAG ).forEach( flagsElement ->
                     {
                         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 );
@@ -139,10 +140,10 @@ class PwmSettingMetaData
             {
                 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,
                             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,
                             permissionElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_PERMISSION_ACCESS ).orElse( "" ) );
 
@@ -191,7 +192,7 @@ class PwmSettingMetaData
                         final String keyAttribute = propertyElement.getAttribute( PwmSettingXml.XML_ATTRIBUTE_KEY )
                                 .orElseThrow( () -> new IllegalStateException( "property element is missing 'key' attribute for value " + pwmSetting.getKey() ) );
 
-                        final PwmSettingProperty property = JavaHelper.readEnumFromString( PwmSettingProperty.class, keyAttribute )
+                        final PwmSettingProperty property = EnumUtil.readEnumFromString( PwmSettingProperty.class, keyAttribute )
                                 .orElseThrow( () -> new IllegalStateException( "property element has unknown 'key' attribute for value " + pwmSetting.getKey() ) );
 
                         propertyElement.getText().ifPresent( value -> newProps.put( property, value ) );

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

@@ -20,7 +20,7 @@
 
 package password.pwm.config;
 
-import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.EnumUtil;
 
 import java.util.Arrays;
 import java.util.LinkedHashMap;
@@ -42,7 +42,7 @@ public class PwmSettingStats
 
         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() )
                 .count() );
 

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

@@ -21,6 +21,7 @@
 package password.pwm.config;
 
 import org.jrivard.xmlchai.XmlElement;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.JavaHelper;
 
 import java.util.EnumMap;
@@ -82,7 +83,7 @@ public enum PwmSettingTemplate
 
     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

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

@@ -22,6 +22,7 @@ package password.pwm.config;
 
 import lombok.Value;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.EnumUtil;
 
 import java.io.Serializable;
 import java.util.List;
@@ -41,7 +42,7 @@ public class PwmSettingTemplateSet implements Serializable
                 .map( PwmSettingTemplate::getType )
                 .collect( Collectors.toSet() );
 
-        workingSet.addAll( CollectionUtil.enumStream( PwmSettingTemplate.Type.class )
+        workingSet.addAll( EnumUtil.enumStream( PwmSettingTemplate.Type.class )
                 .filter( type -> !seenTypes.contains( type ) )
                 .map( PwmSettingTemplate.Type::getDefaultValue )
                 .collect( Collectors.toUnmodifiableSet( ) ) );
@@ -70,7 +71,7 @@ public class PwmSettingTemplateSet implements Serializable
      */
     public static List<PwmSettingTemplateSet> allValues()
     {
-        return CollectionUtil.enumStream( PwmSettingTemplate.class )
+        return EnumUtil.enumStream( PwmSettingTemplate.class )
                 .map( pwmSettingTemplate -> new PwmSettingTemplateSet( Set.of( pwmSettingTemplate ) ) )
                 .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.XmlElement;
 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.logging.PwmLogger;
 
@@ -36,6 +36,9 @@ import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
 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;
 
 public class PwmSettingXml
@@ -69,7 +72,9 @@ public class PwmSettingXml
 
     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 XmlDocument readXml( )
@@ -80,6 +85,10 @@ public class PwmSettingXml
             final XmlDocument newDoc = XmlChai.getFactory().parse( inputStream, AccessMode.IMMUTABLE );
             final TimeDuration parseDuration = TimeDuration.fromCurrent( startTime );
             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;
         }
         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.EmailItemBean;
 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.profile.Profile;
 import password.pwm.config.profile.ProfileDefinition;
@@ -48,8 +48,9 @@ import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.CollectorUtil;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmHashAlgorithm;
 
@@ -58,7 +59,6 @@ import java.security.MessageDigest;
 import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.EnumMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -73,13 +73,13 @@ public class StoredSettingReader implements SettingReader
     private static final PwmLogger LOGGER = PwmLogger.forClass( StoredSettingReader.class );
 
     private final StoredConfiguration storedConfiguration;
-    private final String profileID;
+    private final ProfileID profileID;
     private final DomainID domainID;
 
     private final Map<ProfileDefinition, Map> profileCache;
     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.profileID = profileID;
@@ -192,7 +192,7 @@ public class StoredSettingReader implements SettingReader
         final String input = readSettingAsString( setting );
 
         return Arrays.stream( input.split( "-" ) )
-                .map( s ->  JavaHelper.readEnumFromString( DataStorageMethod.class, s ) )
+                .map( s ->  EnumUtil.readEnumFromString( DataStorageMethod.class, s ) )
                 .flatMap( Optional::stream )
                 .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 )
         {
@@ -240,17 +240,15 @@ public class StoredSettingReader implements SettingReader
                 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() ) )
-                    .collect( CollectionUtil.collectorToLinkedMap(
+                    .collect( CollectorUtil.toUnmodifiableLinkedMap(
                             profileDefinition -> profileDefinition,
                             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 StoredConfiguration storedConfiguration,
                 final DomainID domainID
@@ -262,7 +260,7 @@ public class StoredSettingReader implements SettingReader
             }
 
             return ProfileUtility.profileIDsForCategory( storedConfiguration, domainID, profileDefinition.getCategory() ).stream()
-                    .collect( CollectionUtil.collectorToLinkedMap(
+                    .collect( CollectorUtil.toUnmodifiableLinkedMap(
                             Function.identity(),
                             profileID -> newProfileForID( profileDefinition, storedConfiguration, domainID, profileID )
                     ) );
@@ -272,7 +270,7 @@ public class StoredSettingReader implements SettingReader
                 final ProfileDefinition profileDefinition,
                 final StoredConfiguration storedConfiguration,
                 final DomainID domainID,
-                final String profileID
+                final ProfileID profileID
         )
         {
             Objects.requireNonNull( profileDefinition );
@@ -321,10 +319,10 @@ public class StoredSettingReader implements SettingReader
 
         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() )
             {

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

@@ -20,7 +20,7 @@
 
 package password.pwm.config.option;
 
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.EnumUtil;
 import password.pwm.ws.server.RestAuthenticationType;
 
 import java.util.Arrays;
@@ -59,6 +59,6 @@ public enum WebServiceUsage
 
     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;
 
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredSettingReader;
 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.UserPermission;
 import password.pwm.util.PasswordData;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.EnumUtil;
 
 import java.security.cert.X509Certificate;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 
 public abstract class AbstractProfile implements Profile
 {
-    private final String identifier;
+    private final ProfileID profileID;
     private final StoredConfiguration storedConfiguration;
     private final StoredSettingReader settingReader;
 
@@ -53,23 +55,23 @@ public abstract class AbstractProfile implements Profile
         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.settingReader = new StoredSettingReader( storedConfiguration, identifier, domainID );
+        this.settingReader = new StoredSettingReader( storedConfiguration, profileID, domainID );
     }
 
     @Override
-    public String getIdentifier( )
+    public ProfileID getId( )
     {
-        return identifier;
+        return profileID;
     }
 
     @Override
     public String getDisplayName( final Locale locale )
     {
-        return getIdentifier();
+        return getId().stringValue();
     }
 
     public List<UserPermission> readSettingAsUserPermission( final PwmSetting setting )
@@ -174,6 +176,6 @@ public abstract class AbstractProfile implements Profile
     public GuidMode readGuidMode()
     {
         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;
 
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 
 public class AccountInformationProfile extends AbstractProfile implements Profile
 {
     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 );
     }
@@ -41,7 +42,7 @@ public class AccountInformationProfile extends AbstractProfile implements Profil
     public static class AccountInformationProfileFactory implements Profile.ProfileFactory
     {
         @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 );
         }

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

@@ -21,13 +21,14 @@
 package password.pwm.config.profile;
 
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 
 public class ActivateUserProfile extends AbstractProfile implements Profile
 {
     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 );
     }
@@ -41,7 +42,7 @@ public class ActivateUserProfile extends AbstractProfile implements Profile
     public static class UserActivationProfileFactory implements ProfileFactory
     {
         @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 );
         }

+ 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 password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredSettingReader;
 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 final String profileID;
+    private final ProfileID profileID;
     private final Locale locale;
     private final ChallengeSet challengeSet;
     private final ChallengeSet helpdeskChallengeSet;
@@ -57,7 +58,7 @@ public class ChallengeProfile implements Profile, Serializable
     private final List<UserPermission> userPermissions;
 
     private ChallengeProfile(
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale,
             final ChallengeSet challengeSet,
             final ChallengeSet helpdeskChallengeSet,
@@ -77,7 +78,7 @@ public class ChallengeProfile implements Profile, Serializable
 
     public static ChallengeProfile readChallengeProfileFromConfig(
             final DomainID domainID,
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale,
             final StoredConfiguration storedConfiguration
     )
@@ -127,7 +128,7 @@ public class ChallengeProfile implements Profile, Serializable
     }
 
     public static ChallengeProfile createChallengeProfile(
-            final String profileID,
+            final ProfileID profileID,
             final Locale locale,
             final ChallengeSet challengeSet,
             final ChallengeSet helpdeskChallengeSet,
@@ -139,7 +140,7 @@ public class ChallengeProfile implements Profile, Serializable
     }
 
     @Override
-    public String getIdentifier( )
+    public ProfileID getId( )
     {
         return profileID;
     }
@@ -147,7 +148,7 @@ public class ChallengeProfile implements Profile, Serializable
     @Override
     public String getDisplayName( final Locale locale )
     {
-        return getIdentifier();
+        return getId().stringValue();
     }
 
     public Locale getLocale( )

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

@@ -21,13 +21,14 @@
 package password.pwm.config.profile;
 
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 
 public class ChangePasswordProfile extends AbstractProfile implements Profile
 {
     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 );
     }
@@ -41,7 +42,7 @@ public class ChangePasswordProfile extends AbstractProfile implements Profile
     public static class ChangePasswordProfileFactory implements Profile.ProfileFactory
     {
         @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 );
         }

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

@@ -21,13 +21,14 @@
 package password.pwm.config.profile;
 
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 
 public class DeleteAccountProfile extends AbstractProfile implements Profile
 {
     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 );
     }
@@ -41,7 +42,7 @@ public class DeleteAccountProfile extends AbstractProfile implements Profile
     public static class DeleteAccountProfileFactory implements ProfileFactory
     {
         @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 );
         }

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

@@ -21,16 +21,15 @@
 package password.pwm.config.profile;
 
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 
-import java.util.Locale;
-
 public class EmailServerProfile extends AbstractProfile
 {
 
     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 );
     }
@@ -41,16 +40,10 @@ public class EmailServerProfile extends AbstractProfile
         return PROFILE_TYPE;
     }
 
-    @Override
-    public String getDisplayName( final Locale locale )
-    {
-        return this.getIdentifier();
-    }
-
     public static class EmailServerProfileFactory implements ProfileFactory
     {
         @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 );
         }

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

@@ -21,6 +21,7 @@
 package password.pwm.config.profile;
 
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.stored.StoredConfiguration;
@@ -36,7 +37,7 @@ public class ForgottenPasswordProfile extends AbstractProfile
     private Set<IdentityVerificationMethod> requiredRecoveryVerificationMethods;
     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 );
     }
@@ -80,7 +81,7 @@ public class ForgottenPasswordProfile extends AbstractProfile
     public static class ForgottenPasswordProfileFactory implements ProfileFactory
     {
         @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 );
         }

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

@@ -21,6 +21,7 @@
 package password.pwm.config.profile;
 
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.IdentityVerificationMethod;
 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;
 
-    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 );
     }
@@ -62,7 +63,7 @@ public class HelpdeskProfile extends AbstractProfile implements Profile
     public static class HelpdeskProfileFactory implements ProfileFactory
     {
         @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 );
         }

+ 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.PwmDomain;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
@@ -43,7 +44,6 @@ import password.pwm.util.logging.PwmLogger;
 
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -56,7 +56,14 @@ public class LdapProfile extends AbstractProfile implements Profile
 
     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 );
     }
@@ -67,17 +74,22 @@ public class LdapProfile extends AbstractProfile implements Profile
     )
             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(
@@ -86,14 +98,19 @@ public class LdapProfile extends AbstractProfile implements Profile
     )
             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(
@@ -106,7 +123,7 @@ public class LdapProfile extends AbstractProfile implements Profile
     public String getDisplayName( final Locale locale )
     {
         final String displayName = readSettingAsLocalizedString( PwmSetting.LDAP_PROFILE_DISPLAY_NAME, locale );
-        return StringUtil.isTrimEmpty( displayName ) ? getIdentifier() : displayName;
+        return StringUtil.isTrimEmpty( displayName ) ? getId().stringValue() : displayName;
     }
 
     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
     {
         verifyIsEnabled();
-        return pwmDomain.getProxyChaiProvider( sessionLabel, this.getIdentifier() );
+        return pwmDomain.getProxyChaiProvider( sessionLabel, this.getId() );
     }
 
     @Override
     public ProfileDefinition profileType( )
     {
-        throw new UnsupportedOperationException();
+        return PROFILE_TYPE;
     }
 
     @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 ) );
 
         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 )
         {
             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 )
             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 )
             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(
@@ -222,7 +249,7 @@ public class LdapProfile extends AbstractProfile implements Profile
 
         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();
@@ -231,7 +258,7 @@ public class LdapProfile extends AbstractProfile implements Profile
     public static class LdapProfileFactory implements ProfileFactory
     {
         @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 );
         }
@@ -242,7 +269,7 @@ public class LdapProfile extends AbstractProfile implements Profile
     {
         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 ) );
         }
     }
@@ -255,6 +282,29 @@ public class LdapProfile extends AbstractProfile implements Profile
     @Override
     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.PwmDomain;
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.DomainConfig;
@@ -44,6 +45,7 @@ import java.time.Instant;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 
 public class NewUserProfile extends AbstractProfile implements Profile
 {
@@ -54,7 +56,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
     private Instant newUserPasswordPolicyCacheTime;
     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 );
     }
@@ -69,7 +71,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
     public String getDisplayName( final Locale 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 )
@@ -101,7 +103,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
         if ( StringUtil.isEmpty( configuredNewUserPasswordDN ) )
         {
             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";
             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 ) )
                 {
                     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 "
-                            + 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;
                     throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INVALID_CONFIG, errorMsg ) );
                 }
@@ -139,9 +141,9 @@ public class NewUserProfile extends AbstractProfile implements Profile
             {
                 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 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 );
                 }
                 catch ( final ChaiUnavailableException e )
@@ -179,7 +181,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
     public static class NewUserProfileFactory implements ProfileFactory
     {
         @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 );
         }
@@ -188,16 +190,17 @@ public class NewUserProfile extends AbstractProfile implements Profile
     public LdapProfile getLdapProfile( final DomainConfig domainConfig )
             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 )
             {
                 throw new PwmUnrecoverableException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                         {
                                 "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;
 
 import password.pwm.bean.DomainID;
+import password.pwm.bean.ProfileID;
 import password.pwm.config.stored.StoredConfiguration;
 
 public class PeopleSearchProfile extends AbstractProfile
@@ -28,7 +29,7 @@ public class PeopleSearchProfile extends AbstractProfile
 
     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 );
     }
@@ -42,7 +43,7 @@ public class PeopleSearchProfile extends AbstractProfile
     public static class PeopleSearchProfileFactory implements ProfileFactory
     {
         @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 );
         }

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác