Browse Source

Merge branch 'master' into patch-12

atuc2 6 years ago
parent
commit
cbb7c9f756
98 changed files with 1562 additions and 1081 deletions
  1. 1 0
      build/checkstyle-import.xml
  2. 9 0
      client/pom.xml
  3. 1 1
      data-service/pom.xml
  4. 2 5
      data-service/src/main/java/password/pwm/receiver/SummaryBean.java
  5. 1 1
      docker/pom.xml
  6. 1 1
      onejar/pom.xml
  7. 2 0
      onejar/src/main/java/password/pwm/onejar/TomcatOnejarRunner.java
  8. 2 2
      pom.xml
  9. 1 1
      server/pom.xml
  10. 3 1
      server/src/main/java/password/pwm/AppProperty.java
  11. 5 0
      server/src/main/java/password/pwm/bean/LocalSessionStateBean.java
  12. 1 1
      server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  13. 5 5
      server/src/main/java/password/pwm/config/value/FileValue.java
  14. 1 1
      server/src/main/java/password/pwm/health/HealthMessage.java
  15. 104 20
      server/src/main/java/password/pwm/health/HealthMonitor.java
  16. 35 40
      server/src/main/java/password/pwm/http/ContextManager.java
  17. 25 1
      server/src/main/java/password/pwm/http/HttpEventManager.java
  18. 1 1
      server/src/main/java/password/pwm/http/PwmRequest.java
  19. 1 1
      server/src/main/java/password/pwm/http/auth/CASFilterAuthenticationProvider.java
  20. 7 2
      server/src/main/java/password/pwm/http/bean/ImmutableByteArray.java
  21. 1 0
      server/src/main/java/password/pwm/http/bean/SetupResponsesBean.java
  22. 4 0
      server/src/main/java/password/pwm/http/filter/SessionFilter.java
  23. 9 4
      server/src/main/java/password/pwm/http/servlet/SetupResponsesServlet.java
  24. 2 2
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  25. 1 1
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  26. 2 1
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideForm.java
  27. 4 23
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java
  28. 2 2
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerWordlistServlet.java
  29. 206 138
      server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  30. 1 1
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  31. 1 1
      server/src/main/java/password/pwm/http/servlet/oauth/OAuthState.java
  32. 1 1
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchService.java
  33. 1 1
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java
  34. 2 2
      server/src/main/java/password/pwm/http/servlet/resource/CacheEntry.java
  35. 2 2
      server/src/main/java/password/pwm/http/servlet/resource/MemoryFileResource.java
  36. 1 1
      server/src/main/java/password/pwm/http/servlet/resource/ResourceFileServlet.java
  37. 2 2
      server/src/main/java/password/pwm/http/servlet/resource/ResourceServletConfiguration.java
  38. 64 52
      server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java
  39. 28 8
      server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileUtil.java
  40. 7 2
      server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java
  41. 3 2
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  42. 28 9
      server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java
  43. 3 3
      server/src/main/java/password/pwm/svc/node/LDAPNodeDataService.java
  44. 2 2
      server/src/main/java/password/pwm/svc/node/NodeService.java
  45. 5 0
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java
  46. 1 1
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java
  47. 3 3
      server/src/main/java/password/pwm/svc/report/ReportService.java
  48. 4 0
      server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java
  49. 74 0
      server/src/main/java/password/pwm/svc/stats/AvgStatistic.java
  50. 79 0
      server/src/main/java/password/pwm/svc/stats/DailyKey.java
  51. 49 0
      server/src/main/java/password/pwm/svc/stats/EpsKey.java
  52. 10 21
      server/src/main/java/password/pwm/svc/stats/EpsStatistic.java
  53. 27 0
      server/src/main/java/password/pwm/svc/stats/StatKey.java
  54. 80 117
      server/src/main/java/password/pwm/svc/stats/Statistic.java
  55. 30 0
      server/src/main/java/password/pwm/svc/stats/StatisticType.java
  56. 63 106
      server/src/main/java/password/pwm/svc/stats/StatisticsBundle.java
  57. 41 177
      server/src/main/java/password/pwm/svc/stats/StatisticsManager.java
  58. 1 1
      server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java
  59. 0 2
      server/src/main/java/password/pwm/svc/wordlist/AbstractWordlist.java
  60. 2 2
      server/src/main/java/password/pwm/svc/wordlist/WordlistSource.java
  61. 2 2
      server/src/main/java/password/pwm/svc/wordlist/WordlistZipReader.java
  62. 7 0
      server/src/main/java/password/pwm/util/CaptchaUtility.java
  63. 1 2
      server/src/main/java/password/pwm/util/EventRateMeter.java
  64. 13 10
      server/src/main/java/password/pwm/util/PwmPasswordRuleValidator.java
  65. 1 1
      server/src/main/java/password/pwm/util/PwmScheduler.java
  66. 4 2
      server/src/main/java/password/pwm/util/RandomPasswordGenerator.java
  67. 1 1
      server/src/main/java/password/pwm/util/db/DBConfiguration.java
  68. 6 0
      server/src/main/java/password/pwm/util/java/AtomicLoopIntIncrementer.java
  69. 60 0
      server/src/main/java/password/pwm/util/java/AtomicLoopLongIncrementer.java
  70. 43 0
      server/src/main/java/password/pwm/util/java/DebugOutputBuilder.java
  71. 109 82
      server/src/main/java/password/pwm/util/java/FileSystemUtility.java
  72. 12 0
      server/src/main/java/password/pwm/util/java/JavaHelper.java
  73. 1 2
      server/src/main/java/password/pwm/util/localdb/LocalDBFactory.java
  74. 132 103
      server/src/main/java/password/pwm/util/localdb/LocalDBUtility.java
  75. 3 3
      server/src/main/java/password/pwm/util/localdb/WorkQueueProcessor.java
  76. 4 2
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  77. 1 1
      server/src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java
  78. 12 25
      server/src/main/java/password/pwm/util/secure/ChecksumInputStream.java
  79. 10 23
      server/src/main/java/password/pwm/util/secure/ChecksumOutputStream.java
  80. 1 1
      server/src/main/java/password/pwm/util/secure/X509Utils.java
  81. 1 1
      server/src/main/java/password/pwm/ws/server/RestServlet.java
  82. 19 9
      server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java
  83. 4 2
      server/src/main/resources/password/pwm/AppProperty.properties
  84. 3 0
      server/src/main/resources/password/pwm/i18n/Admin.properties
  85. 1 0
      server/src/main/resources/password/pwm/i18n/Display.properties
  86. 1 1
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  87. 18 7
      server/src/test/java/password/pwm/i18n/AdminPropertyKeysTest.java
  88. 1 1
      server/src/test/java/password/pwm/util/localdb/LocalDBLoggerExtendedTest.java
  89. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/activateuser.jsp
  90. 22 9
      webapp/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp
  91. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/forgottenpassword-search.jsp
  92. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/forgottenusername-search.jsp
  93. 1 1
      webapp/src/main/webapp/WEB-INF/jsp/fragment/form.jsp
  94. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/login.jsp
  95. 0 1
      webapp/src/main/webapp/WEB-INF/jsp/newuser.jsp
  96. 10 7
      webapp/src/main/webapp/public/resources/js/main.js
  97. 1 1
      webapp/src/main/webapp/public/resources/js/responses.js
  98. 3 3
      webapp/src/main/webapp/public/resources/js/uilibrary.js

+ 1 - 0
build/checkstyle-import.xml

@@ -38,6 +38,7 @@
     <allow pkg="java.security"/>
     <allow pkg="java.security"/>
 
 
     <allow pkg="org.apache.catalina"/>
     <allow pkg="org.apache.catalina"/>
+    <allow pkg="org.apache.coyote"/>
 
 
     <!-- chai -->
     <!-- chai -->
     <allow pkg="com.novell.ldapchai"/>
     <allow pkg="com.novell.ldapchai"/>

+ 9 - 0
client/pom.xml

@@ -18,6 +18,15 @@
         <project.root.basedir>${project.basedir}/..</project.root.basedir>
         <project.root.basedir>${project.basedir}/..</project.root.basedir>
     </properties>
     </properties>
 
 
+    <profiles>
+        <profile>
+            <id>skip-frontend</id>
+            <properties>
+                <skip.npm>true</skip.npm>
+            </properties>
+        </profile>
+    </profiles>
+
     <build>
     <build>
         <plugins>
         <plugins>
             <plugin>
             <plugin>

+ 1 - 1
data-service/pom.xml

@@ -140,7 +140,7 @@
         <dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
             <artifactId>commons-lang3</artifactId>
-            <version>3.8.1</version>
+            <version>3.9</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>com.sun.mail</groupId>
             <groupId>com.sun.mail</groupId>

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

@@ -125,11 +125,8 @@ public class SummaryBean
                     final Statistic statistic = Statistic.forKey( statKey );
                     final Statistic statistic = Statistic.forKey( statKey );
                     if ( statistic != null )
                     if ( statistic != null )
                     {
                     {
-                        if ( statistic.getType() == Statistic.Type.INCREMENTER )
-                        {
-                            final int count = Integer.parseInt( bean.getStatistics().get( statKey ) );
-                            incrementCounterMap( statCount, statistic.getLabel( null ), count );
-                        }
+                        final int count = Integer.parseInt( bean.getStatistics().get( statKey ) );
+                        incrementCounterMap( statCount, statistic.getLabel( null ), count );
                     }
                     }
                 }
                 }
             }
             }

+ 1 - 1
docker/pom.xml

@@ -34,7 +34,7 @@
             <plugin>
             <plugin>
                 <groupId>com.google.cloud.tools</groupId>
                 <groupId>com.google.cloud.tools</groupId>
                 <artifactId>jib-maven-plugin</artifactId>
                 <artifactId>jib-maven-plugin</artifactId>
-                <version>1.0.2</version>
+                <version>1.1.0</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <id>make-docker-image</id>
                         <id>make-docker-image</id>

+ 1 - 1
onejar/pom.xml

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

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

@@ -25,6 +25,7 @@ package password.pwm.onejar;
 import org.apache.catalina.connector.Connector;
 import org.apache.catalina.connector.Connector;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.util.ServerInfo;
 import org.apache.catalina.util.ServerInfo;
+import org.apache.coyote.http2.Http2Protocol;
 
 
 import javax.servlet.ServletException;
 import javax.servlet.ServletException;
 import java.io.BufferedReader;
 import java.io.BufferedReader;
@@ -159,6 +160,7 @@ public class TomcatOnejarRunner
         }
         }
         connector.setSecure( true );
         connector.setSecure( true );
         connector.setScheme( "https" );
         connector.setScheme( "https" );
+        connector.addUpgradeProtocol( new Http2Protocol() );
         connector.setAttribute( "SSLEnabled", "true" );
         connector.setAttribute( "SSLEnabled", "true" );
         connector.setAttribute( "keystoreFile", onejarConfig.getKeystoreFile().getAbsolutePath() );
         connector.setAttribute( "keystoreFile", onejarConfig.getKeystoreFile().getAbsolutePath() );
         connector.setAttribute( "keystorePass", onejarConfig.getKeystorePass() );
         connector.setAttribute( "keystorePass", onejarConfig.getKeystorePass() );

+ 2 - 2
pom.xml

@@ -247,7 +247,7 @@
                     <dependency>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
                         <artifactId>spotbugs</artifactId>
-                        <version>3.1.12</version>
+                        <version>4.0.0-beta1</version>
                     </dependency>
                     </dependency>
                 </dependencies>
                 </dependencies>
                 <configuration>
                 <configuration>
@@ -291,7 +291,7 @@
         <dependency>
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>3.1.12</version>
+            <version>4.0.0-beta1</version>
             <scope>provided</scope>
             <scope>provided</scope>
         </dependency>
         </dependency>
     </dependencies>
     </dependencies>

+ 1 - 1
server/pom.xml

@@ -249,7 +249,7 @@
         <dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
             <artifactId>commons-lang3</artifactId>
-            <version>3.8.1</version>
+            <version>3.9</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>commons-validator</groupId>
             <groupId>commons-validator</groupId>

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

@@ -78,7 +78,7 @@ public enum AppProperty
     CONFIG_EDITOR_QUERY_FILTER_TEST_LIMIT           ( "configEditor.queryFilter.testLimit" ),
     CONFIG_EDITOR_QUERY_FILTER_TEST_LIMIT           ( "configEditor.queryFilter.testLimit" ),
     CONFIG_EDITOR_IDLE_TIMEOUT                      ( "configEditor.idleTimeoutSeconds" ),
     CONFIG_EDITOR_IDLE_TIMEOUT                      ( "configEditor.idleTimeoutSeconds" ),
     CONFIG_GUIDE_IDLE_TIMEOUT                       ( "configGuide.idleTimeoutSeconds" ),
     CONFIG_GUIDE_IDLE_TIMEOUT                       ( "configGuide.idleTimeoutSeconds" ),
-    CONFIG_MANAGER_ZIPDEBUG_MAXLOGLINES             ( "configManager.zipDebug.maxLogLines" ),
+    CONFIG_MANAGER_ZIPDEBUG_MAXLOGBYTES             ( "configManager.zipDebug.maxLogBytes" ),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS           ( "configManager.zipDebug.maxLogSeconds" ),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS           ( "configManager.zipDebug.maxLogSeconds" ),
     CLUSTER_DB_ENABLE                               ( "cluster.db.enable" ),
     CLUSTER_DB_ENABLE                               ( "cluster.db.enable" ),
     CLUSTER_DB_HEARTBEAT_SECONDS                    ( "cluster.db.heartbeatSeconds" ),
     CLUSTER_DB_HEARTBEAT_SECONDS                    ( "cluster.db.heartbeatSeconds" ),
@@ -194,6 +194,8 @@ public enum AppProperty
     HEALTHCHECK_MIN_CHECK_INTERVAL                  ( "healthCheck.minimumCheckIntervalSeconds" ),
     HEALTHCHECK_MIN_CHECK_INTERVAL                  ( "healthCheck.minimumCheckIntervalSeconds" ),
     HEALTHCHECK_MAX_RECORD_AGE                      ( "healthCheck.maximumRecordAgeSeconds" ),
     HEALTHCHECK_MAX_RECORD_AGE                      ( "healthCheck.maximumRecordAgeSeconds" ),
     HEALTHCHECK_MAX_FORCE_WAIT                      ( "healthCheck.maximumForceCheckWaitSeconds" ),
     HEALTHCHECK_MAX_FORCE_WAIT                      ( "healthCheck.maximumForceCheckWaitSeconds" ),
+    HEALTH_SUPPORT_BUNDLE_WRITE_INTERVAL_SECONDS    ( "health.supportBundle.file.writeIntervalSeconds" ),
+    HEALTH_SUPPORT_BUNDLE_FILE_WRITE_COUNT          ( "health.supportBundle.file.writeRetentionCount" ),
     HEALTH_CERTIFICATE_WARN_SECONDS                 ( "health.certificate.warnSeconds" ),
     HEALTH_CERTIFICATE_WARN_SECONDS                 ( "health.certificate.warnSeconds" ),
     HEALTH_LDAP_CAUTION_DURATION_MS                 ( "health.ldap.cautionDurationMS" ),
     HEALTH_LDAP_CAUTION_DURATION_MS                 ( "health.ldap.cautionDurationMS" ),
     HEALTH_LDAP_PROXY_WARN_PW_EXPIRE_SECONDS        ( "health.ldap.proxy.pwExpireWarnSeconds" ),
     HEALTH_LDAP_PROXY_WARN_PW_EXPIRE_SECONDS        ( "health.ldap.proxy.pwExpireWarnSeconds" ),

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

@@ -24,6 +24,8 @@ package password.pwm.bean;
 
 
 import lombok.Data;
 import lombok.Data;
 import password.pwm.ldap.UserInfoBean;
 import password.pwm.ldap.UserInfoBean;
+import password.pwm.util.EventRateMeter;
+import password.pwm.util.java.TimeDuration;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.time.Instant;
 import java.time.Instant;
@@ -64,8 +66,11 @@ public class LocalSessionStateBean implements Serializable
 
 
     private boolean passwordModified;
     private boolean passwordModified;
     private boolean privateUrlAccessed;
     private boolean privateUrlAccessed;
+    private boolean captchaBypassedViaParameter;
 
 
     private final AtomicInteger intruderAttempts = new AtomicInteger( 0 );
     private final AtomicInteger intruderAttempts = new AtomicInteger( 0 );
+    private final AtomicInteger requestCount = new AtomicInteger( 0 );
+    private final EventRateMeter.MovingAverage avgRequestDuration = new EventRateMeter.MovingAverage( TimeDuration.DAY );
     private boolean oauthInProgress;
     private boolean oauthInProgress;
 
 
     private boolean sessionIdRecycleNeeded;
     private boolean sessionIdRecycleNeeded;

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

@@ -44,8 +44,8 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.i18n.PwmLocaleBundle;
-import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
+import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;

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

@@ -89,30 +89,30 @@ public class FileValue extends AbstractValue implements StoredValue
                 throws IOException
                 throws IOException
         {
         {
             final byte[] convertedBytes = StringUtil.base64Decode( input );
             final byte[] convertedBytes = StringUtil.base64Decode( input );
-            return new FileContent( new ImmutableByteArray( convertedBytes ) );
+            return new FileContent( ImmutableByteArray.of( convertedBytes ) );
         }
         }
 
 
         public String toEncodedString( )
         public String toEncodedString( )
                 throws IOException
                 throws IOException
         {
         {
-            return StringUtil.base64Encode( contents.getBytes(), StringUtil.Base64Options.GZIP );
+            return StringUtil.base64Encode( contents.copyOf(), StringUtil.Base64Options.GZIP );
         }
         }
 
 
         public String md5sum( )
         public String md5sum( )
                 throws PwmUnrecoverableException
                 throws PwmUnrecoverableException
         {
         {
-            return SecureEngine.hash( new ByteArrayInputStream( contents.getBytes() ), PwmHashAlgorithm.MD5 );
+            return SecureEngine.hash( new ByteArrayInputStream( contents.copyOf() ), PwmHashAlgorithm.MD5 );
         }
         }
 
 
         public String sha1sum( )
         public String sha1sum( )
                 throws PwmUnrecoverableException
                 throws PwmUnrecoverableException
         {
         {
-            return SecureEngine.hash( new ByteArrayInputStream( contents.getBytes() ), PwmHashAlgorithm.SHA1 );
+            return SecureEngine.hash( new ByteArrayInputStream( contents.copyOf() ), PwmHashAlgorithm.SHA1 );
         }
         }
 
 
         public int size( )
         public int size( )
         {
         {
-            return contents.getBytes().length;
+            return contents.copyOf().length;
         }
         }
     }
     }
 
 

+ 1 - 1
server/src/main/java/password/pwm/health/HealthMessage.java

@@ -44,7 +44,7 @@ public enum HealthMessage
     LDAP_TestUserReadPwError( HealthStatus.WARN, HealthTopic.LDAP ),
     LDAP_TestUserReadPwError( HealthStatus.WARN, HealthTopic.LDAP ),
     LDAP_TestUserOK( HealthStatus.GOOD, HealthTopic.LDAP ),
     LDAP_TestUserOK( HealthStatus.GOOD, HealthTopic.LDAP ),
     Email_SendFailure( HealthStatus.WARN, HealthTopic.Email ),
     Email_SendFailure( HealthStatus.WARN, HealthTopic.Email ),
-    PwNotify_Failure( HealthStatus.WARN, HealthTopic.Email ),
+    PwNotify_Failure( HealthStatus.WARN, HealthTopic.Application ),
     MissingResource( HealthStatus.DEBUG, HealthTopic.Integrity ),
     MissingResource( HealthStatus.DEBUG, HealthTopic.Integrity ),
     BrokenMethod( HealthStatus.DEBUG, HealthTopic.Integrity ),
     BrokenMethod( HealthStatus.DEBUG, HealthTopic.Integrity ),
     Appliance_PendingUpdates( HealthStatus.CAUTION, HealthTopic.Appliance ),
     Appliance_PendingUpdates( HealthStatus.CAUTION, HealthTopic.Appliance ),

+ 104 - 20
server/src/main/java/password/pwm/health/HealthMonitor.java

@@ -25,13 +25,22 @@ package password.pwm.health;
 import lombok.Value;
 import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
+import password.pwm.bean.SessionLabel;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.servlet.configmanager.DebugItemGenerator;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmService;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.PwmScheduler;
+import password.pwm.util.java.FileSystemUtility;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.Serializable;
 import java.io.Serializable;
+import java.nio.file.Files;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collection;
@@ -44,6 +53,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.ZipOutputStream;
 
 
 public class HealthMonitor implements PwmService
 public class HealthMonitor implements PwmService
 {
 {
@@ -64,6 +74,7 @@ public class HealthMonitor implements PwmService
     }
     }
 
 
     private ExecutorService executorService;
     private ExecutorService executorService;
+    private ExecutorService supportZipWriterService;
     private HealthMonitorSettings settings;
     private HealthMonitorSettings settings;
 
 
     private Map<HealthMonitorFlag, Serializable> healthProperties = new ConcurrentHashMap<>();
     private Map<HealthMonitorFlag, Serializable> healthProperties = new ConcurrentHashMap<>();
@@ -82,6 +93,26 @@ public class HealthMonitor implements PwmService
     {
     {
     }
     }
 
 
+    public void init( final PwmApplication pwmApplication ) throws PwmException
+    {
+        status = STATUS.OPENING;
+        this.pwmApplication = pwmApplication;
+        settings = HealthMonitorSettings.fromConfiguration( pwmApplication.getConfig() );
+
+        if ( !Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTHCHECK_ENABLED ) ) )
+        {
+            LOGGER.debug( () -> "health monitor will remain inactive due to AppProperty " + AppProperty.HEALTHCHECK_ENABLED.getKey() );
+            status = STATUS.CLOSED;
+            return;
+        }
+
+        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
+        supportZipWriterService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
+        scheduleNextZipOutput();
+
+        status = STATUS.OPEN;
+    }
+
     public Instant getLastHealthCheckTime( )
     public Instant getLastHealthCheckTime( )
     {
     {
         if ( status != STATUS.OPEN )
         if ( status != STATUS.OPEN )
@@ -122,24 +153,6 @@ public class HealthMonitor implements PwmService
         return status;
         return status;
     }
     }
 
 
-    public void init( final PwmApplication pwmApplication ) throws PwmException
-    {
-        status = STATUS.OPENING;
-        this.pwmApplication = pwmApplication;
-        settings = HealthMonitorSettings.fromConfiguration( pwmApplication.getConfig() );
-
-        if ( !Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTHCHECK_ENABLED ) ) )
-        {
-            LOGGER.debug( () -> "health monitor will remain inactive due to AppProperty " + AppProperty.HEALTHCHECK_ENABLED.getKey() );
-            status = STATUS.CLOSED;
-            return;
-        }
-
-        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
-
-        status = STATUS.OPEN;
-    }
-
     public Set<HealthRecord> getHealthRecords( )
     public Set<HealthRecord> getHealthRecords( )
     {
     {
         if ( status != STATUS.OPEN )
         if ( status != STATUS.OPEN )
@@ -151,12 +164,12 @@ public class HealthMonitor implements PwmService
         {
         {
             final Instant startTime = Instant.now();
             final Instant startTime = Instant.now();
             LOGGER.trace( () ->  "begin force immediate check" );
             LOGGER.trace( () ->  "begin force immediate check" );
-            final Future future = pwmApplication.getPwmScheduler().scheduleFutureJob( new ImmediateJob(), executorService, TimeDuration.ZERO );
+            final Future future = pwmApplication.getPwmScheduler().scheduleJob( new ImmediateJob(), executorService, TimeDuration.ZERO );
             settings.getMaximumForceCheckWait().pause( future::isDone );
             settings.getMaximumForceCheckWait().pause( future::isDone );
             LOGGER.trace( () ->  "exit force immediate check, done=" + future.isDone() + ", " + TimeDuration.compactFromCurrent( startTime ) );
             LOGGER.trace( () ->  "exit force immediate check, done=" + future.isDone() + ", " + TimeDuration.compactFromCurrent( startTime ) );
         }
         }
 
 
-        pwmApplication.getPwmScheduler().scheduleFutureJob( new UpdateJob(), executorService, settings.getNominalCheckInterval() );
+        pwmApplication.getPwmScheduler().scheduleJob( new UpdateJob(), executorService, settings.getNominalCheckInterval() );
 
 
         {
         {
             final HealthData localHealthData = this.healthData;
             final HealthData localHealthData = this.healthData;
@@ -175,6 +188,10 @@ public class HealthMonitor implements PwmService
         {
         {
             executorService.shutdown();
             executorService.shutdown();
         }
         }
+        if ( supportZipWriterService != null )
+        {
+            supportZipWriterService.shutdown();
+        }
         healthData = emptyHealthData();
         healthData = emptyHealthData();
         status = STATUS.CLOSED;
         status = STATUS.CLOSED;
     }
     }
@@ -299,4 +316,71 @@ public class HealthMonitor implements PwmService
             return TimeDuration.fromCurrent( this.getTimeStamp() ).isLongerThan( settings.getMaximumRecordAge() );
             return TimeDuration.fromCurrent( this.getTimeStamp() ).isLongerThan( settings.getMaximumRecordAge() );
         }
         }
     }
     }
+
+    private void scheduleNextZipOutput()
+    {
+        final int intervalSeconds = JavaHelper.silentParseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_SUPPORT_BUNDLE_WRITE_INTERVAL_SECONDS ), 0 );
+        if ( intervalSeconds > 0 )
+        {
+            final TimeDuration intervalDuration = TimeDuration.of( intervalSeconds, TimeDuration.Unit.SECONDS );
+            pwmApplication.getPwmScheduler().scheduleJob( new SupportZipFileWriter( pwmApplication ), supportZipWriterService, intervalDuration );
+        }
+    }
+
+    private class SupportZipFileWriter implements Runnable
+    {
+        private final PwmApplication pwmApplication;
+
+        SupportZipFileWriter( final PwmApplication pwmApplication )
+        {
+            this.pwmApplication = pwmApplication;
+        }
+
+        @Override
+        public void run()
+        {
+            try
+            {
+                writeSupportZipToAppPath();
+            }
+            catch ( Exception e )
+            {
+                LOGGER.debug( SessionLabel.HEALTH_SESSION_LABEL, () -> "error writing support zip to file system: " + e.getMessage() );
+            }
+
+            scheduleNextZipOutput();
+        }
+
+        private void writeSupportZipToAppPath()
+                throws IOException, PwmUnrecoverableException
+        {
+            final File appPath = pwmApplication.getPwmEnvironment().getApplicationPath();
+            if ( !appPath.exists() )
+            {
+                return;
+            }
+
+            final int rotationCount = JavaHelper.silentParseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HEALTH_SUPPORT_BUNDLE_FILE_WRITE_COUNT ), 10 );
+            final DebugItemGenerator debugItemGenerator = new DebugItemGenerator( pwmApplication, SessionLabel.HEALTH_SESSION_LABEL );
+
+            final File supportPath = new File( appPath.getPath() + File.separator + "support" );
+
+            FileSystemUtility.mkdirs( supportPath );
+
+            final File supportFile = new File ( supportPath.getPath() + File.separator + debugItemGenerator.getFilename() );
+
+            FileSystemUtility.rotateBackups( supportFile, rotationCount );
+
+            final File newSupportFile = new File ( supportFile.getPath() + ".new" );
+            Files.deleteIfExists( newSupportFile.toPath() );
+
+            try ( ZipOutputStream zipOutputStream = new ZipOutputStream( new FileOutputStream( newSupportFile ) ) )
+            {
+                LOGGER.trace( SessionLabel.HEALTH_SESSION_LABEL, () -> "beginning periodic support bundle filesystem output" );
+                debugItemGenerator.outputZipDebugFile( zipOutputStream );
+            }
+
+            Files.move( newSupportFile.toPath(), supportFile.toPath() );
+        }
+    }
 }
 }

+ 35 - 40
server/src/main/java/password/pwm/http/ContextManager.java

@@ -59,9 +59,8 @@ import java.util.Map;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
 
 
 public class ContextManager implements Serializable
 public class ContextManager implements Serializable
 {
 {
@@ -76,8 +75,8 @@ public class ContextManager implements Serializable
     private ErrorInformation startupErrorInformation;
     private ErrorInformation startupErrorInformation;
 
 
     private final AtomicInteger restartCount = new AtomicInteger( 0 );
     private final AtomicInteger restartCount = new AtomicInteger( 0 );
-    private TimeDuration readApplicationLockMaxWait = TimeDuration.SECONDS_30;
-    private final Lock restartLock = new ReentrantLock();
+    private TimeDuration readApplicationLockMaxWait = TimeDuration.of( 10, TimeDuration.Unit.SECONDS );
+    private final AtomicBoolean restartInProgressFlag = new AtomicBoolean();
 
 
     private String contextPath;
     private String contextPath;
     private File applicationPath;
     private File applicationPath;
@@ -143,42 +142,32 @@ public class ContextManager implements Serializable
     public PwmApplication getPwmApplication( )
     public PwmApplication getPwmApplication( )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        PwmApplication localApplication = this.pwmApplication;
+        final Instant startTime = Instant.now();
+        PwmApplication localApplication = pwmApplication;
 
 
-        if ( localApplication == null )
+        while (
+                ( restartInProgressFlag.get() || pwmApplication == null )
+                        &&  TimeDuration.fromCurrent( startTime ).isShorterThan( readApplicationLockMaxWait )
+        )
         {
         {
-            try
-            {
-                final Instant startTime = Instant.now();
-                final boolean hasLock = restartLock.tryLock( readApplicationLockMaxWait.asMillis(), TimeUnit.MILLISECONDS );
-                if ( hasLock )
-                {
-                    localApplication = this.pwmApplication;
-                    if ( localApplication == null )
-                    {
-                        LOGGER.trace( () -> "could not read pwmApplication after waiting " + TimeDuration.compactFromCurrent( startTime ) );
-                    }
-                    else
-                    {
-                        LOGGER.trace( () -> "waited " + TimeDuration.compactFromCurrent( startTime )
-                                + " to read pwmApplication due to restart in progress" );
-                    }
-                }
-            }
-            catch ( InterruptedException e )
-            {
-                LOGGER.warn( "getPwmApplication restartLock unexpectedly interrupted" );
-            }
-            finally
-            {
-                restartLock.unlock();
-            }
+            TimeDuration.SECOND.pause();
+            localApplication = pwmApplication;
         }
         }
 
 
         if ( localApplication != null )
         if ( localApplication != null )
         {
         {
+            if ( TimeDuration.fromCurrent( startTime ).isLongerThan( TimeDuration.SECOND ) )
+            {
+                LOGGER.trace( () -> "waited " + TimeDuration.compactFromCurrent( startTime )
+                        + " to read pwmApplication due to restart in progress" );
+            }
             return localApplication;
             return localApplication;
         }
         }
+        else
+        {
+            LOGGER.trace( () -> "could not read pwmApplication after waiting " + TimeDuration.compactFromCurrent( startTime ) );
+        }
+
 
 
         final ErrorInformation errorInformation;
         final ErrorInformation errorInformation;
         if ( startupErrorInformation != null )
         if ( startupErrorInformation != null )
@@ -484,6 +473,11 @@ public class ContextManager implements Serializable
         {
         {
             final Instant startTime = Instant.now();
             final Instant startTime = Instant.now();
 
 
+            if ( restartInProgressFlag.get() )
+            {
+                return;
+            }
+
             if ( configReader != null && configReader.isSaveInProgress() )
             if ( configReader != null && configReader.isSaveInProgress() )
             {
             {
                 final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
                 final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
@@ -492,12 +486,13 @@ public class ContextManager implements Serializable
                 return;
                 return;
             }
             }
 
 
-            restartLock.lock();
             final PwmApplication oldPwmApplication = pwmApplication;
             final PwmApplication oldPwmApplication = pwmApplication;
             pwmApplication = null;
             pwmApplication = null;
 
 
             try
             try
             {
             {
+                restartInProgressFlag.set( true );
+
                 waitForRequestsToComplete( oldPwmApplication );
                 waitForRequestsToComplete( oldPwmApplication );
 
 
                 {
                 {
@@ -542,7 +537,7 @@ public class ContextManager implements Serializable
             }
             }
             finally
             finally
             {
             {
-                restartLock.unlock();
+                restartInProgressFlag.set( false );
             }
             }
         }
         }
 
 
@@ -551,22 +546,22 @@ public class ContextManager implements Serializable
             final Instant startTime = Instant.now();
             final Instant startTime = Instant.now();
             final TimeDuration maxRequestWaitTime = TimeDuration.of(
             final TimeDuration maxRequestWaitTime = TimeDuration.of(
                     Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.APPLICATION_RESTART_MAX_REQUEST_WAIT_MS ) ),
                     Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.APPLICATION_RESTART_MAX_REQUEST_WAIT_MS ) ),
-                    TimeDuration.Unit.SECONDS );
-            final int startingRequetsInProgress = pwmApplication.getInprogressRequests().get();
+                    TimeDuration.Unit.MILLISECONDS );
+            final int startingRequestInProgress = pwmApplication.getInprogressRequests().get();
 
 
-            if ( startingRequetsInProgress == 0 )
+            if ( startingRequestInProgress == 0 )
             {
             {
                 return;
                 return;
             }
             }
 
 
             LOGGER.trace( () -> "waiting up to " + maxRequestWaitTime.asCompactString()
             LOGGER.trace( () -> "waiting up to " + maxRequestWaitTime.asCompactString()
-                    + " for " + startingRequetsInProgress  + " requests to complete." );
+                    + " for " + startingRequestInProgress  + " requests to complete." );
             maxRequestWaitTime.pause( TimeDuration.of( 10, TimeDuration.Unit.MILLISECONDS ), () -> pwmApplication.getInprogressRequests().get() == 0
             maxRequestWaitTime.pause( TimeDuration.of( 10, TimeDuration.Unit.MILLISECONDS ), () -> pwmApplication.getInprogressRequests().get() == 0
             );
             );
 
 
-            final int requestsInPrgoress = pwmApplication.getInprogressRequests().get();
+            final int requestsInProgress = pwmApplication.getInprogressRequests().get();
             final TimeDuration waitTime = TimeDuration.fromCurrent( startTime  );
             final TimeDuration waitTime = TimeDuration.fromCurrent( startTime  );
-            LOGGER.trace( () -> "after " + waitTime.asCompactString() + ", " + requestsInPrgoress
+            LOGGER.trace( () -> "after " + waitTime.asCompactString() + ", " + requestsInProgress
                     + " requests in progress, proceeding with restart" );
                     + " requests in progress, proceeding with restart" );
         }
         }
     }
     }

+ 25 - 1
server/src/main/java/password/pwm/http/HttpEventManager.java

@@ -22,10 +22,13 @@
 
 
 package password.pwm.http;
 package password.pwm.http;
 
 
+import com.novell.ldapchai.util.StringHelper;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
+import password.pwm.bean.LocalSessionStateBean;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.EpsStatistic;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
 import javax.servlet.ServletContextEvent;
 import javax.servlet.ServletContextEvent;
@@ -34,6 +37,9 @@ import javax.servlet.http.HttpSession;
 import javax.servlet.http.HttpSessionActivationListener;
 import javax.servlet.http.HttpSessionActivationListener;
 import javax.servlet.http.HttpSessionEvent;
 import javax.servlet.http.HttpSessionEvent;
 import javax.servlet.http.HttpSessionListener;
 import javax.servlet.http.HttpSessionListener;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.logging.Logger;
 
 
@@ -82,17 +88,21 @@ public class HttpEventManager implements
         {
         {
             if ( httpSession.getAttribute( PwmConstants.SESSION_ATTR_PWM_SESSION ) != null )
             if ( httpSession.getAttribute( PwmConstants.SESSION_ATTR_PWM_SESSION ) != null )
             {
             {
+                String debugMsg = "destroyed session";
                 final PwmSession pwmSession = PwmSessionWrapper.readPwmSession( httpSession );
                 final PwmSession pwmSession = PwmSessionWrapper.readPwmSession( httpSession );
                 if ( pwmSession != null )
                 if ( pwmSession != null )
                 {
                 {
+                    debugMsg += ": " + makeSessionDestroyedDebugMsg( pwmSession );
                     pwmSession.unauthenticateUser( null );
                     pwmSession.unauthenticateUser( null );
                 }
                 }
+
                 final PwmApplication pwmApplication = ContextManager.getPwmApplication( httpSession.getServletContext() );
                 final PwmApplication pwmApplication = ContextManager.getPwmApplication( httpSession.getServletContext() );
                 if ( pwmApplication != null )
                 if ( pwmApplication != null )
                 {
                 {
                     pwmApplication.getSessionTrackService().removeSessionData( pwmSession );
                     pwmApplication.getSessionTrackService().removeSessionData( pwmSession );
                 }
                 }
-                LOGGER.trace( pwmSession, () -> "destroyed session" );
+                final String outputMsg = debugMsg;
+                LOGGER.trace( pwmSession, () -> outputMsg );
             }
             }
             else
             else
             {
             {
@@ -180,5 +190,19 @@ public class HttpEventManager implements
             LOGGER.error( "unable to activate (de-passivate) session: " + e.getMessage() );
             LOGGER.error( "unable to activate (de-passivate) session: " + e.getMessage() );
         }
         }
     }
     }
+
+    private static String makeSessionDestroyedDebugMsg( final PwmSession pwmSession )
+    {
+        final LocalSessionStateBean sessionStateBean = pwmSession.getSessionStateBean();
+        final Map<String, String> debugItems = new LinkedHashMap<>();
+        debugItems.put( "requests", sessionStateBean.getRequestCount().toString() );
+        final Instant startTime = sessionStateBean.getSessionCreationTime();
+        final Instant lastAccessedTime = sessionStateBean.getSessionLastAccessedTime();
+        final TimeDuration timeDuration = TimeDuration.between( startTime, lastAccessedTime );
+        debugItems.put( "firstToLastRequestInterval", timeDuration.asCompactString() );
+        final TimeDuration avgReqDuration = TimeDuration.of( sessionStateBean.getAvgRequestDuration().getLastMillis(), TimeDuration.Unit.MILLISECONDS );
+        debugItems.put( "avgRequestDuration", avgReqDuration.asCompactString() );
+        return StringHelper.stringMapToString( debugItems, "," );
+    }
 }
 }
 
 

+ 1 - 1
server/src/main/java/password/pwm/http/PwmRequest.java

@@ -270,7 +270,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
                     final FileUploadItem fileUploadItem = new FileUploadItem(
                     final FileUploadItem fileUploadItem = new FileUploadItem(
                             item.getName(),
                             item.getName(),
                             item.getContentType(),
                             item.getContentType(),
-                            new ImmutableByteArray( outputFile )
+                            ImmutableByteArray.of( outputFile )
                     );
                     );
                     returnObj.put( item.getFieldName(), fileUploadItem );
                     returnObj.put( item.getFieldName(), fileUploadItem );
                 }
                 }

+ 1 - 1
server/src/main/java/password/pwm/http/auth/CASFilterAuthenticationProvider.java

@@ -225,7 +225,7 @@ public class CASFilterAuthenticationProvider implements PwmHttpFilterAuthenticat
         if ( privatekey != null && !privatekey.isEmpty() )
         if ( privatekey != null && !privatekey.isEmpty() )
         {
         {
             final FileValue.FileContent fileContent = privatekey.values().iterator().next();
             final FileValue.FileContent fileContent = privatekey.values().iterator().next();
-            privateKeyBytes = fileContent.getContents().getBytes();
+            privateKeyBytes = fileContent.getContents().copyOf();
         }
         }
         else
         else
         {
         {

+ 7 - 2
server/src/main/java/password/pwm/http/bean/ImmutableByteArray.java

@@ -29,12 +29,17 @@ public class ImmutableByteArray implements Serializable
 {
 {
     private final byte[] bytes;
     private final byte[] bytes;
 
 
-    public ImmutableByteArray( final byte[] bytes )
+    private ImmutableByteArray( final byte[] bytes )
     {
     {
         this.bytes = bytes == null ? null : Arrays.copyOf( bytes, bytes.length );
         this.bytes = bytes == null ? null : Arrays.copyOf( bytes, bytes.length );
     }
     }
 
 
-    public byte[] getBytes( )
+    public static ImmutableByteArray of( final byte[] bytes )
+    {
+        return new ImmutableByteArray( bytes );
+    }
+
+    public byte[] copyOf( )
     {
     {
         return bytes == null ? null : Arrays.copyOf( bytes, bytes.length );
         return bytes == null ? null : Arrays.copyOf( bytes, bytes.length );
     }
     }

+ 1 - 0
server/src/main/java/password/pwm/http/bean/SetupResponsesBean.java

@@ -45,6 +45,7 @@ public class SetupResponsesBean extends PwmSessionBean
     private boolean helpdeskResponsesSatisfied;
     private boolean helpdeskResponsesSatisfied;
     private boolean confirmed;
     private boolean confirmed;
     private Locale userLocale;
     private Locale userLocale;
+    private boolean initialized;
 
 
     public Type getType( )
     public Type getType( )
     {
     {

+ 4 - 0
server/src/main/java/password/pwm/http/filter/SessionFilter.java

@@ -45,6 +45,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmResponse;
 import password.pwm.http.PwmResponse;
 import password.pwm.http.PwmSession;
 import password.pwm.http.PwmSession;
 import password.pwm.http.PwmURL;
 import password.pwm.http.PwmURL;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
@@ -137,6 +138,9 @@ public class SessionFilter extends AbstractPwmFilter
 
 
         final TimeDuration requestExecuteTime = TimeDuration.fromCurrent( startTime );
         final TimeDuration requestExecuteTime = TimeDuration.fromCurrent( startTime );
         pwmRequest.debugHttpRequestToLog( "completed requestID=" + requestID + " in " + requestExecuteTime.asCompactString() );
         pwmRequest.debugHttpRequestToLog( "completed requestID=" + requestID + " in " + requestExecuteTime.asCompactString() );
+        pwmRequest.getPwmApplication().getStatisticsManager().updateAverageValue( AvgStatistic.AVG_REQUEST_PROCESS_TIME, requestExecuteTime.asMillis() );
+        pwmRequest.getPwmSession().getSessionStateBean().getRequestCount().incrementAndGet();
+        pwmRequest.getPwmSession().getSessionStateBean().getAvgRequestDuration().update( requestExecuteTime.asMillis() );
     }
     }
 
 
     private ProcessStatus handleStandardRequestOperations(
     private ProcessStatus handleStandardRequestOperations(

+ 9 - 4
server/src/main/java/password/pwm/http/servlet/SetupResponsesServlet.java

@@ -122,7 +122,13 @@ public class SetupResponsesServlet extends ControlledPwmServlet
 
 
     private SetupResponsesBean getSetupResponseBean( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
     private SetupResponsesBean getSetupResponseBean( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
     {
     {
-        return pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, SetupResponsesBean.class );
+        final SetupResponsesBean setupResponsesBean = pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, SetupResponsesBean.class );
+        if ( !setupResponsesBean.isInitialized() )
+        {
+            initializeBean( pwmRequest, setupResponsesBean );
+        }
+        return setupResponsesBean;
+
     }
     }
 
 
     @Override
     @Override
@@ -130,7 +136,6 @@ public class SetupResponsesServlet extends ControlledPwmServlet
     {
     {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final SetupResponsesBean setupResponsesBean = getSetupResponseBean( pwmRequest );
 
 
         if ( !pwmSession.isAuthenticated() )
         if ( !pwmSession.isAuthenticated() )
         {
         {
@@ -161,10 +166,10 @@ public class SetupResponsesServlet extends ControlledPwmServlet
             pwmApplication.getSessionStateService().getBean( pwmRequest, SetupResponsesBean.class ).setUserLocale( pwmSession.getSessionStateBean().getLocale() );
             pwmApplication.getSessionStateService().getBean( pwmRequest, SetupResponsesBean.class ).setUserLocale( pwmSession.getSessionStateBean().getLocale() );
         }
         }
 
 
-        initializeBean( pwmRequest, setupResponsesBean );
-
         // check to see if the user has any challenges assigned
         // check to see if the user has any challenges assigned
         final UserInfo uiBean = pwmSession.getUserInfo();
         final UserInfo uiBean = pwmSession.getUserInfo();
+        final SetupResponsesBean setupResponsesBean = getSetupResponseBean( pwmRequest );
+
         if ( setupResponsesBean.getResponseData().getChallengeSet() == null || setupResponsesBean.getResponseData().getChallengeSet().getChallenges().isEmpty() )
         if ( setupResponsesBean.getResponseData().getChallengeSet() == null || setupResponsesBean.getResponseData().getChallengeSet().getChallenges().isEmpty() )
         {
         {
             final String errorMsg = "no challenge sets configured for user " + uiBean.getUserIdentity();
             final String errorMsg = "no challenge sets configured for user " + uiBean.getUserIdentity();

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java

@@ -53,7 +53,7 @@ import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.AuditRecordFactory;
-import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PwmPasswordRuleValidator;
 import password.pwm.util.PwmPasswordRuleValidator;
 import password.pwm.util.RandomPasswordGenerator;
 import password.pwm.util.RandomPasswordGenerator;
@@ -366,7 +366,7 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
                 final TimeDuration totalTime = TimeDuration.fromCurrent( progressTracker.getBeginTime() );
                 final TimeDuration totalTime = TimeDuration.fromCurrent( progressTracker.getBeginTime() );
                 try
                 try
                 {
                 {
-                    pwmRequest.getPwmApplication().getStatisticsManager().updateAverageValue( Statistic.AVG_PASSWORD_SYNC_TIME, totalTime.asMillis() );
+                    pwmRequest.getPwmApplication().getStatisticsManager().updateAverageValue( AvgStatistic.AVG_PASSWORD_SYNC_TIME, totalTime.asMillis() );
                     LOGGER.trace( pwmRequest, () -> "password sync process marked completed (" + totalTime.asCompactString() + ")" );
                     LOGGER.trace( pwmRequest, () -> "password sync process marked completed (" + totalTime.asCompactString() + ")" );
                 }
                 }
                 catch ( Exception e )
                 catch ( Exception e )

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -776,7 +776,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 }
                 }
 
 
                 final Map<String, PwmRequest.FileUploadItem> fileUploads = pwmRequest.readFileUploads( maxFileSize, 1 );
                 final Map<String, PwmRequest.FileUploadItem> fileUploads = pwmRequest.readFileUploads( maxFileSize, 1 );
-                final ByteArrayInputStream fileIs = new ByteArrayInputStream( fileUploads.get( PwmConstants.PARAM_FILE_UPLOAD ).getContent().getBytes() );
+                final ByteArrayInputStream fileIs = new ByteArrayInputStream( fileUploads.get( PwmConstants.PARAM_FILE_UPLOAD ).getContent().copyOf() );
 
 
                 HttpsServerCertificateManager.importKey(
                 HttpsServerCertificateManager.importKey(
                         configManagerBean.getStoredConfiguration(),
                         configManagerBean.getStoredConfiguration(),

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

@@ -38,6 +38,7 @@ import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ConfigGuideBean;
 import password.pwm.http.bean.ConfigGuideBean;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
@@ -129,7 +130,7 @@ public class ConfigGuideForm
             storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_URLS, LDAP_PROFILE_NAME, newValue, null );
             storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_URLS, LDAP_PROFILE_NAME, newValue, null );
         }
         }
 
 
-        if ( configGuideBean.isUseConfiguredCerts() )
+        if ( configGuideBean.isUseConfiguredCerts() && !JavaHelper.isEmpty( configGuideBean.getLdapCertificates() ) )
         {
         {
             final StoredValue newStoredValue = new X509CertificateValue( configGuideBean.getLdapCertificates() );
             final StoredValue newStoredValue = new X509CertificateValue( configGuideBean.getLdapCertificates() );
             storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_CERTS, LDAP_PROFILE_NAME, newStoredValue, null );
             storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_CERTS, LDAP_PROFILE_NAME, newStoredValue, null );

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

@@ -372,39 +372,20 @@ public class ConfigManagerServlet extends AbstractPwmServlet
     }
     }
 
 
     private void doGenerateSupportZip( final PwmRequest pwmRequest )
     private void doGenerateSupportZip( final PwmRequest pwmRequest )
-            throws IOException, ServletException
+            throws IOException, PwmUnrecoverableException
     {
     {
+        final DebugItemGenerator debugItemGenerator = new DebugItemGenerator( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel() );
         final PwmResponse resp = pwmRequest.getPwmResponse();
         final PwmResponse resp = pwmRequest.getPwmResponse();
         resp.setHeader( HttpHeader.ContentDisposition, "attachment;filename=" + PwmConstants.PWM_APP_NAME + "-Support.zip" );
         resp.setHeader( HttpHeader.ContentDisposition, "attachment;filename=" + PwmConstants.PWM_APP_NAME + "-Support.zip" );
         resp.setContentType( HttpContentType.zip );
         resp.setContentType( HttpContentType.zip );
-
-        final String pathPrefix = PwmConstants.PWM_APP_NAME + "-Support" + "/";
-
-        ZipOutputStream zipOutput = null;
-        try
+        try ( ZipOutputStream zipOutput = new ZipOutputStream( resp.getOutputStream(), PwmConstants.DEFAULT_CHARSET ) )
         {
         {
-            zipOutput = new ZipOutputStream( resp.getOutputStream(), PwmConstants.DEFAULT_CHARSET );
-            DebugItemGenerator.outputZipDebugFile( pwmRequest, zipOutput, pathPrefix );
+            debugItemGenerator.outputZipDebugFile( zipOutput );
         }
         }
         catch ( Exception e )
         catch ( Exception e )
         {
         {
             LOGGER.error( pwmRequest, "error during zip debug building: " + e.getMessage() );
             LOGGER.error( pwmRequest, "error during zip debug building: " + e.getMessage() );
         }
         }
-        finally
-        {
-            if ( zipOutput != null )
-            {
-                try
-                {
-                    zipOutput.close();
-                }
-                catch ( Exception e )
-                {
-                    LOGGER.error( pwmRequest, "error during zip debug closing: " + e.getMessage() );
-                }
-            }
-        }
-
     }
     }
 
 
 
 

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerWordlistServlet.java

@@ -259,8 +259,8 @@ public class ConfigManagerWordlistServlet extends AbstractPwmServlet
                         presentableValues.add( new DisplayElement(
                         presentableValues.add( new DisplayElement(
                                 wordlistType.name() + "_sha256Hash",
                                 wordlistType.name() + "_sha256Hash",
                                 DisplayElement.Type.string,
                                 DisplayElement.Type.string,
-                                "SHA-256 Checksum Hash",
-                                StringUtil.truncate( wordlistStatus.getRemoteInfo().getChecksum(), 32 ) + "..." ) );
+                                "CRC Checksum",
+                                wordlistStatus.getRemoteInfo().getChecksum() ) );
                     }
                     }
                 }
                 }
                 if ( wordlist.getAutoImportError() != null )
                 if ( wordlist.getAutoImportError() != null )

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

@@ -25,17 +25,18 @@ package password.pwm.http.servlet.configmanager;
 import lombok.Builder;
 import lombok.Builder;
 import lombok.Value;
 import lombok.Value;
 import org.apache.commons.csv.CSVPrinter;
 import org.apache.commons.csv.CSVPrinter;
+import org.apache.commons.io.output.CountingOutputStream;
 import password.pwm.AppProperty;
 import password.pwm.AppProperty;
 import password.pwm.PwmAboutProperty;
 import password.pwm.PwmAboutProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.ContextManager;
 import password.pwm.http.ContextManager;
-import password.pwm.http.PwmRequest;
 import password.pwm.http.servlet.admin.AppDashboardData;
 import password.pwm.http.servlet.admin.AppDashboardData;
 import password.pwm.http.servlet.admin.UserDebugDataBean;
 import password.pwm.http.servlet.admin.UserDebugDataBean;
 import password.pwm.http.servlet.admin.UserDebugDataReader;
 import password.pwm.http.servlet.admin.UserDebugDataReader;
@@ -43,7 +44,11 @@ import password.pwm.ldap.LdapDebugDataGenerator;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.cache.CacheService;
 import password.pwm.svc.cache.CacheService;
 import password.pwm.svc.node.NodeService;
 import password.pwm.svc.node.NodeService;
+import password.pwm.svc.stats.EpsStatistic;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.LDAPPermissionCalculator;
 import password.pwm.util.LDAPPermissionCalculator;
+import password.pwm.util.java.DebugOutputBuilder;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
@@ -67,6 +72,7 @@ import java.io.StringWriter;
 import java.io.Writer;
 import java.io.Writer;
 import java.lang.management.ManagementFactory;
 import java.lang.management.ManagementFactory;
 import java.lang.management.ThreadInfo;
 import java.lang.management.ThreadInfo;
+import java.math.BigDecimal;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Arrays;
@@ -74,6 +80,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
 import java.util.Properties;
 import java.util.Properties;
 import java.util.Set;
 import java.util.Set;
@@ -106,54 +113,85 @@ public class DebugItemGenerator
             LdapRecentUserDebugGenerator.class,
             LdapRecentUserDebugGenerator.class,
             ClusterInfoDebugGenerator.class,
             ClusterInfoDebugGenerator.class,
             CacheServiceDebugItemGenerator.class,
             CacheServiceDebugItemGenerator.class,
-            RootFileSystemDebugItemGenerator.class
+            RootFileSystemDebugItemGenerator.class,
+            StatisticsDataDebugItemGenerator.class,
+            StatisticsEpsDataDebugItemGenerator.class
     ) );
     ) );
 
 
-    static void outputZipDebugFile(
-            final PwmRequest pwmRequest,
-            final ZipOutputStream zipOutput,
-            final String pathPrefix
-    )
-            throws IOException, PwmUnrecoverableException
+    private final PwmApplication pwmApplication;
+    private final Configuration obfuscatedConfiguration;
+    private final SessionLabel sessionLabel;
+
+    private static final Locale LOCALE = PwmConstants.DEFAULT_LOCALE;
+
+    public DebugItemGenerator( final PwmApplication pwmApplication, final SessionLabel sessionLabel )
+            throws PwmUnrecoverableException
     {
     {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final String debugFileName = "zipDebugGeneration.csv";
+        this.pwmApplication = pwmApplication;
+        this.sessionLabel = sessionLabel;
 
 
-        final ByteArrayOutputStream debugGeneratorLogBaos = new ByteArrayOutputStream();
-        final CSVPrinter debugGeneratorLogFile = JavaHelper.makeCsvPrinter( debugGeneratorLogBaos );
+        final StoredConfigurationImpl storedConfiguration = StoredConfigurationImpl.copy( pwmApplication.getConfig().getStoredConfiguration() );
+        storedConfiguration.resetAllPasswordValues( "value removed from " + getFilenameBase() + " configuration export" );
+        this.obfuscatedConfiguration = new Configuration( storedConfiguration );
+    }
+
+    private String getFilenameBase()
+    {
+        return PwmConstants.PWM_APP_NAME + "-Support";
+    }
+
+    public String getFilename()
+    {
+        return getFilenameBase() + ".zip";
+    }
+
+    public void outputZipDebugFile( final ZipOutputStream zipOutput )
+            throws IOException
+    {
+        final String debugFileName = "zipDebugGeneration.csv";
+        final Instant startTime = Instant.now();
+        final DebugOutputBuilder debugGeneratorLogFile = new DebugOutputBuilder();
+        final DebugItemInput debugItemInput = new DebugItemInput( pwmApplication, sessionLabel, obfuscatedConfiguration );
+        debugGeneratorLogFile.appendLine( "beginning debug output" );
+        final String pathPrefix = getFilenameBase() + "/";
 
 
         for ( final Class<? extends DebugItemGenerator.Generator> serviceClass : DEBUG_ZIP_ITEM_GENERATORS )
         for ( final Class<? extends DebugItemGenerator.Generator> serviceClass : DEBUG_ZIP_ITEM_GENERATORS )
         {
         {
             try
             try
             {
             {
-                final Instant startTime = Instant.now();
-                LOGGER.trace( pwmRequest, () -> "beginning output of item " + serviceClass.getSimpleName() );
+                final Instant itemStartTime = Instant.now();
+                LOGGER.trace( sessionLabel, () -> "beginning output of item " + serviceClass.getSimpleName() );
                 final DebugItemGenerator.Generator newGeneratorItem = serviceClass.getDeclaredConstructor().newInstance();
                 final DebugItemGenerator.Generator newGeneratorItem = serviceClass.getDeclaredConstructor().newInstance();
                 zipOutput.putNextEntry( new ZipEntry( pathPrefix + newGeneratorItem.getFilename() ) );
                 zipOutput.putNextEntry( new ZipEntry( pathPrefix + newGeneratorItem.getFilename() ) );
-                newGeneratorItem.outputItem( pwmApplication, pwmRequest, zipOutput );
+                newGeneratorItem.outputItem( debugItemInput, zipOutput );
                 zipOutput.closeEntry();
                 zipOutput.closeEntry();
                 zipOutput.flush();
                 zipOutput.flush();
                 final String finishMsg = "completed output of " + newGeneratorItem.getFilename()
                 final String finishMsg = "completed output of " + newGeneratorItem.getFilename()
-                        + " in " + TimeDuration.fromCurrent( startTime ).asCompactString();
-                LOGGER.trace( pwmRequest, () -> finishMsg );
-                debugGeneratorLogFile.printRecord( JavaHelper.toIsoDate( Instant.now() ), finishMsg );
+                        + " in " + TimeDuration.fromCurrent( itemStartTime ).asCompactString();
+                LOGGER.trace( sessionLabel, () -> finishMsg );
+                debugGeneratorLogFile.appendLine( finishMsg );
             }
             }
             catch ( Throwable e )
             catch ( Throwable e )
             {
             {
                 final String errorMsg = "unexpected error executing debug item output class '" + serviceClass.getName() + "', error: " + e.toString();
                 final String errorMsg = "unexpected error executing debug item output class '" + serviceClass.getName() + "', error: " + e.toString();
-                LOGGER.error( pwmRequest, errorMsg );
-                debugGeneratorLogFile.printRecord( JavaHelper.toIsoDate( Instant.now() ), errorMsg );
+                LOGGER.error( sessionLabel, errorMsg, e );
+                debugGeneratorLogFile.appendLine( errorMsg );
                 final Writer stackTraceOutput = new StringWriter();
                 final Writer stackTraceOutput = new StringWriter();
                 e.printStackTrace( new PrintWriter( stackTraceOutput ) );
                 e.printStackTrace( new PrintWriter( stackTraceOutput ) );
-                debugGeneratorLogFile.printRecord( stackTraceOutput );
+                debugGeneratorLogFile.appendLine( stackTraceOutput.toString() );
             }
             }
         }
         }
 
 
+        {
+            final String msg = "completed in " + TimeDuration.compactFromCurrent( startTime );
+            debugGeneratorLogFile.appendLine( msg );
+            LOGGER.trace( sessionLabel, () -> msg );
+        }
+
         try
         try
         {
         {
             zipOutput.putNextEntry( new ZipEntry( pathPrefix + debugFileName ) );
             zipOutput.putNextEntry( new ZipEntry( pathPrefix + debugFileName ) );
-            debugGeneratorLogFile.flush();
-            zipOutput.write( debugGeneratorLogBaos.toByteArray() );
+            zipOutput.write( debugGeneratorLogFile.toString().getBytes( PwmConstants.DEFAULT_CHARSET ) );
             zipOutput.closeEntry();
             zipOutput.closeEntry();
         }
         }
         catch ( Exception e )
         catch ( Exception e )
@@ -173,10 +211,9 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
-            final StoredConfigurationImpl storedConfiguration = ConfigManagerServlet.readCurrentConfiguration( pwmRequest );
-            storedConfiguration.resetAllPasswordValues( "value removed from " + PwmConstants.PWM_APP_NAME + "-Support configuration export" );
+            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
             final String jsonOutput = JsonUtil.serialize( storedConfiguration.toJsonDebugObject(), JsonUtil.Flag.PrettyPrint );
             final String jsonOutput = JsonUtil.serialize( storedConfiguration.toJsonDebugObject(), JsonUtil.Flag.PrettyPrint );
             outputStream.write( jsonOutput.getBytes( PwmConstants.DEFAULT_CHARSET ) );
             outputStream.write( jsonOutput.getBytes( PwmConstants.DEFAULT_CHARSET ) );
         }
         }
@@ -191,10 +228,9 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
-            final StoredConfigurationImpl storedConfiguration = ConfigManagerServlet.readCurrentConfiguration( pwmRequest );
-            storedConfiguration.resetAllPasswordValues( "value removed from " + PwmConstants.PWM_APP_NAME + "-Support configuration export" );
+            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
 
 
             final StringWriter writer = new StringWriter();
             final StringWriter writer = new StringWriter();
             writer.write( "Configuration Debug Output for "
             writer.write( "Configuration Debug Output for "
@@ -205,7 +241,7 @@ public class DebugItemGenerator
 
 
             writer.write( "\n" );
             writer.write( "\n" );
             final Map<String, String> modifiedSettings = new TreeMap<>(
             final Map<String, String> modifiedSettings = new TreeMap<>(
-                    storedConfiguration.getModifiedSettingDebugValues( PwmConstants.DEFAULT_LOCALE, true )
+                    storedConfiguration.getModifiedSettingDebugValues( LOCALE, true )
             );
             );
 
 
             for ( final Map.Entry<String, String> entry : modifiedSettings.entrySet() )
             for ( final Map.Entry<String, String> entry : modifiedSettings.entrySet() )
@@ -232,15 +268,14 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
-            final StoredConfigurationImpl storedConfiguration = ConfigManagerServlet.readCurrentConfiguration( pwmRequest );
-            storedConfiguration.resetAllPasswordValues( "value removed from " + PwmConstants.PWM_APP_NAME + "-Support configuration export" );
+            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
 
 
-            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            storedConfiguration.toXml( baos );
-            outputStream.write( baos.toByteArray() );
-        }
+            // temporary output stream required because .toXml closes stream.
+            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            storedConfiguration.toXml( byteArrayOutputStream );
+            outputStream.write( byteArrayOutputStream.toByteArray() );        }
     }
     }
 
 
     static class AboutItemGenerator implements Generator
     static class AboutItemGenerator implements Generator
@@ -252,10 +287,10 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
             final Properties outputProps = new JavaHelper.SortedProperties();
             final Properties outputProps = new JavaHelper.SortedProperties();
-            final Map<PwmAboutProperty, String> infoBean = PwmAboutProperty.makeInfoBean( pwmApplication );
+            final Map<PwmAboutProperty, String> infoBean = PwmAboutProperty.makeInfoBean( debugItemInput.getPwmApplication() );
             outputProps.putAll( PwmAboutProperty.toStringMap( infoBean ) );
             outputProps.putAll( PwmAboutProperty.toStringMap( infoBean ) );
             outputProps.store( outputStream, JavaHelper.toIsoDate( Instant.now() ) );
             outputProps.store( outputStream, JavaHelper.toIsoDate( Instant.now() ) );
         }
         }
@@ -270,7 +305,7 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
             final Properties outputProps = new JavaHelper.SortedProperties();
             final Properties outputProps = new JavaHelper.SortedProperties();
             outputProps.putAll( System.getenv() );
             outputProps.putAll( System.getenv() );
@@ -287,10 +322,10 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
 
 
-            final Configuration config = pwmRequest.getConfig();
+            final Configuration config = debugItemInput.getObfuscatedConfiguration();
             final Properties outputProps = new JavaHelper.SortedProperties();
             final Properties outputProps = new JavaHelper.SortedProperties();
 
 
             for ( final AppProperty appProperty : AppProperty.values() )
             for ( final AppProperty appProperty : AppProperty.values() )
@@ -311,9 +346,9 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final LinkedHashMap<String, Object> outputMap = new LinkedHashMap<>();
             final LinkedHashMap<String, Object> outputMap = new LinkedHashMap<>();
 
 
             {
             {
@@ -346,8 +381,9 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final Set<HealthRecord> records = pwmApplication.getHealthMonitor().getHealthRecords();
             final Set<HealthRecord> records = pwmApplication.getHealthMonitor().getHealthRecords();
             final String recordJson = JsonUtil.serializeCollection( records, JsonUtil.Flag.PrettyPrint );
             final String recordJson = JsonUtil.serializeCollection( records, JsonUtil.Flag.PrettyPrint );
             outputStream.write( recordJson.getBytes( PwmConstants.DEFAULT_CHARSET ) );
             outputStream.write( recordJson.getBytes( PwmConstants.DEFAULT_CHARSET ) );
@@ -363,18 +399,18 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
 
 
-            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            final PrintWriter writer = new PrintWriter( new OutputStreamWriter( baos, PwmConstants.DEFAULT_CHARSET ) );
+            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            final PrintWriter writer = new PrintWriter( new OutputStreamWriter( byteArrayOutputStream, PwmConstants.DEFAULT_CHARSET ) );
             final ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads( true, true );
             final ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads( true, true );
             for ( final ThreadInfo threadInfo : threads )
             for ( final ThreadInfo threadInfo : threads )
             {
             {
                 writer.write( JavaHelper.threadInfoToString( threadInfo ) );
                 writer.write( JavaHelper.threadInfoToString( threadInfo ) );
             }
             }
             writer.flush();
             writer.flush();
-            outputStream.write( baos.toByteArray() );
+            outputStream.write( byteArrayOutputStream.toByteArray() );
         }
         }
     }
     }
 
 
@@ -387,13 +423,13 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
             final List<LdapDebugDataGenerator.LdapDebugInfo> ldapDebugInfos = LdapDebugDataGenerator.makeLdapDebugInfos(
             final List<LdapDebugDataGenerator.LdapDebugInfo> ldapDebugInfos = LdapDebugDataGenerator.makeLdapDebugInfos(
-                    pwmApplication,
-                    pwmRequest.getSessionLabel(),
-                    pwmApplication.getConfig(),
-                    pwmRequest.getLocale()
+                    debugItemInput.getPwmApplication(),
+                    debugItemInput.getSessionLabel(),
+                    debugItemInput.getObfuscatedConfiguration(),
+                    LOCALE
             );
             );
             final Writer writer = new OutputStreamWriter( outputStream, PwmConstants.DEFAULT_CHARSET );
             final Writer writer = new OutputStreamWriter( outputStream, PwmConstants.DEFAULT_CHARSET );
             writer.write( JsonUtil.serializeCollection( ldapDebugInfos, JsonUtil.Flag.PrettyPrint ) );
             writer.write( JsonUtil.serializeCollection( ldapDebugInfos, JsonUtil.Flag.PrettyPrint ) );
@@ -411,9 +447,10 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
             final List<FileSystemUtility.FileSummaryInformation> fileSummaryInformations = new ArrayList<>();
             final List<FileSystemUtility.FileSummaryInformation> fileSummaryInformations = new ArrayList<>();
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final File applicationPath = pwmApplication.getPwmEnvironment().getApplicationPath();
             final File applicationPath = pwmApplication.getPwmEnvironment().getApplicationPath();
 
 
             if ( pwmApplication.getPwmEnvironment().getContextManager() != null )
             if ( pwmApplication.getPwmEnvironment().getContextManager() != null )
@@ -433,7 +470,7 @@ public class DebugItemGenerator
                 }
                 }
                 catch ( Exception e )
                 catch ( Exception e )
                 {
                 {
-                    LOGGER.error( pwmRequest, "unable to generate webInfPath fileMd5sums during zip debug building: " + e.getMessage() );
+                    LOGGER.error( debugItemInput.getSessionLabel(), "unable to generate webInfPath fileMd5sums during zip debug building: " + e.getMessage() );
                 }
                 }
             }
             }
 
 
@@ -445,7 +482,7 @@ public class DebugItemGenerator
                 }
                 }
                 catch ( Exception e )
                 catch ( Exception e )
                 {
                 {
-                    LOGGER.error( pwmRequest, "unable to generate appPath fileMd5sums during zip debug building: " + e.getMessage() );
+                    LOGGER.error( debugItemInput.getSessionLabel(), "unable to generate appPath fileMd5sums during zip debug building: " + e.getMessage() );
                 }
                 }
             }
             }
 
 
@@ -457,7 +494,7 @@ public class DebugItemGenerator
                     headerRow.add( "Filename" );
                     headerRow.add( "Filename" );
                     headerRow.add( "Last Modified" );
                     headerRow.add( "Last Modified" );
                     headerRow.add( "Size" );
                     headerRow.add( "Size" );
-                    headerRow.add( "sha1sum" );
+                    headerRow.add( "Checksum" );
                     csvPrinter.printComment( StringUtil.join( headerRow, "," ) );
                     csvPrinter.printComment( StringUtil.join( headerRow, "," ) );
                 }
                 }
                 for ( final FileSystemUtility.FileSummaryInformation fileSummaryInformation : fileSummaryInformations )
                 for ( final FileSystemUtility.FileSummaryInformation fileSummaryInformation : fileSummaryInformations )
@@ -469,7 +506,7 @@ public class DebugItemGenerator
                         dataRow.add( fileSummaryInformation.getFilename() );
                         dataRow.add( fileSummaryInformation.getFilename() );
                         dataRow.add( JavaHelper.toIsoDate( fileSummaryInformation.getModified() ) );
                         dataRow.add( JavaHelper.toIsoDate( fileSummaryInformation.getModified() ) );
                         dataRow.add( String.valueOf( fileSummaryInformation.getSize() ) );
                         dataRow.add( String.valueOf( fileSummaryInformation.getSize() ) );
-                        dataRow.add( fileSummaryInformation.getSha1sum() );
+                        dataRow.add( Long.toString( fileSummaryInformation.getChecksum() ) );
                         csvPrinter.printRecord( dataRow );
                         csvPrinter.printRecord( dataRow );
                     }
                     }
                     catch ( Exception e )
                     catch ( Exception e )
@@ -491,40 +528,36 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
-
-            final int maxCount = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MANAGER_ZIPDEBUG_MAXLOGLINES ) );
-            final int maxSeconds = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS ) );
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final long maxByteCount = JavaHelper.silentParseLong( pwmApplication.getConfig().readAppProperty( AppProperty.CONFIG_MANAGER_ZIPDEBUG_MAXLOGBYTES ), 10_000_000 );
+            final int maxSeconds = JavaHelper.silentParseInt( pwmApplication.getConfig().readAppProperty( AppProperty.CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS ), 60 );
             final LocalDBSearchQuery searchParameters = LocalDBSearchQuery.builder()
             final LocalDBSearchQuery searchParameters = LocalDBSearchQuery.builder()
                     .minimumLevel( PwmLogLevel.TRACE )
                     .minimumLevel( PwmLogLevel.TRACE )
-                    .maxEvents( maxCount )
+                    .maxEvents( Integer.MAX_VALUE )
                     .maxQueryTime( TimeDuration.of( maxSeconds, TimeDuration.Unit.SECONDS ) )
                     .maxQueryTime( TimeDuration.of( maxSeconds, TimeDuration.Unit.SECONDS ) )
                     .build();
                     .build();
 
 
-            final LocalDBSearchResults searchResults = pwmApplication.getLocalDBLogger().readStoredEvents(
-                    searchParameters );
-            int counter = 0;
-            while ( searchResults.hasNext() )
+            final LocalDBSearchResults searchResults = pwmApplication.getLocalDBLogger().readStoredEvents( searchParameters );
+            final CountingOutputStream countingOutputStream = new CountingOutputStream( outputStream );
+
+            final Writer writer = new OutputStreamWriter( countingOutputStream, PwmConstants.DEFAULT_CHARSET );
             {
             {
-                final PwmLogEvent event = searchResults.next();
-                outputStream.write( event.toLogString().getBytes( PwmConstants.DEFAULT_CHARSET ) );
-                outputStream.write( "\n".getBytes( PwmConstants.DEFAULT_CHARSET ) );
-                counter++;
-                if ( counter % 1000 == 0 )
+                while ( searchResults.hasNext() && countingOutputStream.getByteCount() < maxByteCount )
                 {
                 {
-                    outputStream.flush();
+                    final PwmLogEvent event = searchResults.next();
+                    writer.write( event.toLogString() );
+                    writer.write( "\n" );
                 }
                 }
-            }
 
 
-            {
-                final int finalCounter = counter;
-                LOGGER.trace( () -> "output " + finalCounter + " lines to " + this.getFilename() );
+                final String outputMsg = "debug output " + searchResults.getReturnedEvents() + " lines in " + searchResults.getSearchTime().asCompactString();
+                writer.write( "\n#" + outputMsg + "\n" );
+                LOGGER.trace( () ->  outputMsg );
             }
             }
+
+            // do not close writer because underlying stream should not be closed.
+            writer.flush();
         }
         }
     }
     }
 
 
@@ -537,14 +570,10 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
 
 
-            final StoredConfigurationImpl storedConfiguration = ConfigManagerServlet.readCurrentConfiguration( pwmRequest );
+            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
             final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
             final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
 
 
             final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( outputStream );
             final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( outputStream );
@@ -581,13 +610,9 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        ) throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
-            final LocalDB localDB = pwmApplication.getLocalDB();
+            final LocalDB localDB = debugItemInput.getPwmApplication().getLocalDB();
             final Map<String, Serializable> serializableMap = localDB.debugInfo();
             final Map<String, Serializable> serializableMap = localDB.debugInfo();
             outputStream.write( JsonUtil.serializeMap( serializableMap, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
             outputStream.write( JsonUtil.serializeMap( serializableMap, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
         }
         }
@@ -602,14 +627,10 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
-            pwmApplication.getSessionTrackService().outputToCsv( pwmRequest.getLocale(), pwmRequest.getConfig(), outputStream );
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            pwmApplication.getSessionTrackService().outputToCsv( LOCALE, pwmApplication.getConfig(), outputStream );
         }
         }
     }
     }
 
 
@@ -621,14 +642,9 @@ public class DebugItemGenerator
             return "recentUserDebugData.json";
             return "recentUserDebugData.json";
         }
         }
 
 
-        @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final List<UserIdentity> recentUsers = pwmApplication.getSessionTrackService().getRecentLogins();
             final List<UserIdentity> recentUsers = pwmApplication.getSessionTrackService().getRecentLogins();
             final List<UserDebugDataBean> recentDebugBeans = new ArrayList<>();
             final List<UserDebugDataBean> recentDebugBeans = new ArrayList<>();
 
 
@@ -636,8 +652,8 @@ public class DebugItemGenerator
             {
             {
                 final UserDebugDataBean dataBean = UserDebugDataReader.readUserDebugData(
                 final UserDebugDataBean dataBean = UserDebugDataReader.readUserDebugData(
                         pwmApplication,
                         pwmApplication,
-                        pwmRequest.getLocale(),
-                        pwmRequest.getSessionLabel(),
+                        LOCALE,
+                        debugItemInput.getSessionLabel(),
                         userIdentity
                         userIdentity
                 );
                 );
                 recentDebugBeans.add( dataBean );
                 recentDebugBeans.add( dataBean );
@@ -656,13 +672,9 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final NodeService nodeService = pwmApplication.getClusterService();
             final NodeService nodeService = pwmApplication.getClusterService();
 
 
             final Map<String, Serializable> debugOutput = new LinkedHashMap<>();
             final Map<String, Serializable> debugOutput = new LinkedHashMap<>();
@@ -687,13 +699,9 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
             final CacheService cacheService = pwmApplication.getCacheService();
             final CacheService cacheService = pwmApplication.getCacheService();
 
 
             final Map<String, Serializable> debugOutput = new LinkedHashMap<>( cacheService.debugInfo() );
             final Map<String, Serializable> debugOutput = new LinkedHashMap<>( cacheService.debugInfo() );
@@ -710,24 +718,82 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
-            final ContextManager contextManager = ContextManager.getContextManager( pwmRequest );
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final ContextManager contextManager = pwmApplication.getPwmEnvironment().getContextManager();
             final AppDashboardData appDashboardData = AppDashboardData.makeDashboardData(
             final AppDashboardData appDashboardData = AppDashboardData.makeDashboardData(
                     pwmApplication,
                     pwmApplication,
                     contextManager,
                     contextManager,
-                    pwmRequest.getLocale()
+                    LOCALE
             );
             );
 
 
             outputStream.write( JsonUtil.serialize( appDashboardData, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
             outputStream.write( JsonUtil.serialize( appDashboardData, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
         }
         }
     }
     }
 
 
+    static class StatisticsDataDebugItemGenerator implements Generator
+    {
+        @Override
+        public String getFilename()
+        {
+            return "statistics.csv";
+        }
+
+        @Override
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
+        {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final StatisticsManager statsManager = pwmApplication.getStatisticsManager();
+            statsManager.outputStatsToCsv( outputStream, LOCALE, true );
+        }
+    }
+
+    static class StatisticsEpsDataDebugItemGenerator implements Generator
+    {
+        @Override
+        public String getFilename()
+        {
+            return "statistics-eps.csv";
+        }
+
+        @Override
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
+        {
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final StatisticsManager statsManager = pwmApplication.getStatisticsManager();
+            final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( outputStream );
+            {
+                final List<String> headerRow = new ArrayList<>();
+                headerRow.add( "Counter" );
+                headerRow.add( "Duration" );
+                headerRow.add( "Events/Second" );
+                csvPrinter.printComment( StringUtil.join( headerRow, "," ) );
+            }
+            for ( final EpsStatistic epsStatistic : EpsStatistic.values() )
+            {
+                for ( final Statistic.EpsDuration epsDuration : Statistic.EpsDuration.values() )
+                {
+                    try
+                    {
+                        final List<String> dataRow = new ArrayList<>();
+                        final BigDecimal value = statsManager.readEps( epsStatistic, epsDuration );
+                        final String sValue = value.toPlainString();
+                        dataRow.add( epsStatistic.getLabel( LOCALE ) );
+                        dataRow.add( epsDuration.getTimeDuration().asCompactString() );
+                        dataRow.add( sValue );
+                        csvPrinter.printRecord( dataRow );
+                    }
+                    catch ( Exception e )
+                    {
+                        LOGGER.trace( () -> "error generating csv-stats summary info: " + e.getMessage() );
+                    }
+                }
+            }
+            csvPrinter.flush();
+        }
+    }
+
     static class RootFileSystemDebugItemGenerator implements Generator
     static class RootFileSystemDebugItemGenerator implements Generator
     {
     {
         @Override
         @Override
@@ -737,12 +803,7 @@ public class DebugItemGenerator
         }
         }
 
 
         @Override
         @Override
-        public void outputItem(
-                final PwmApplication pwmApplication,
-                final PwmRequest pwmRequest,
-                final OutputStream outputStream
-        )
-                throws Exception
+        public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
             final Collection<RootFileSystemInfo> rootInfos = RootFileSystemInfo.forAllRootFileSystems();
             final Collection<RootFileSystemInfo> rootInfos = RootFileSystemInfo.forAllRootFileSystems();
             outputStream.write( JsonUtil.serializeCollection( rootInfos, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
             outputStream.write( JsonUtil.serializeCollection( rootInfos, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
@@ -782,10 +843,17 @@ public class DebugItemGenerator
         String getFilename( );
         String getFilename( );
 
 
         void outputItem(
         void outputItem(
-                PwmApplication pwmApplication,
-                PwmRequest pwmRequest,
+                DebugItemInput debugItemInput,
                 OutputStream outputStream
                 OutputStream outputStream
         ) throws Exception;
         ) throws Exception;
     }
     }
 
 
+    @Value
+    private static class DebugItemInput
+    {
+        private final PwmApplication pwmApplication;
+        private final SessionLabel sessionLabel;
+        private final Configuration obfuscatedConfiguration;
+    }
+
 }
 }

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -1390,7 +1390,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
             final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
             resp.setContentType( photoData.getMimeType() );
             resp.setContentType( photoData.getMimeType() );
 
 
-            outputStream.write( photoData.getContents().getBytes() );
+            outputStream.write( photoData.getContents().copyOf() );
         }
         }
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
     }
     }

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/oauth/OAuthState.java

@@ -39,7 +39,7 @@ import java.time.Instant;
 @Builder
 @Builder
 class OAuthState implements Serializable
 class OAuthState implements Serializable
 {
 {
-    private static final AtomicLoopIntIncrementer OAUTH_STATE_ID_COUNTER = new AtomicLoopIntIncrementer( Integer.MAX_VALUE );
+    private static final AtomicLoopIntIncrementer OAUTH_STATE_ID_COUNTER = new AtomicLoopIntIncrementer();
 
 
     @SerializedName( "c" )
     @SerializedName( "c" )
     @Builder.Default
     @Builder.Default

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchService.java

@@ -42,7 +42,7 @@ public class PeopleSearchService implements PwmService
     @Override
     @Override
     public STATUS status()
     public STATUS status()
     {
     {
-        return null;
+        return STATUS.OPEN;
     }
     }
 
 
     @Override
     @Override

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java

@@ -272,7 +272,7 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
 
 
             if ( photoData.getContents() != null )
             if ( photoData.getContents() != null )
             {
             {
-                outputStream.write( photoData.getContents().getBytes() );
+                outputStream.write( photoData.getContents().copyOf() );
             }
             }
         }
         }
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/resource/CacheEntry.java

@@ -34,13 +34,13 @@ final class CacheEntry implements Serializable
 
 
     CacheEntry( final byte[] entity, final Map<String, String> headerStrings )
     CacheEntry( final byte[] entity, final Map<String, String> headerStrings )
     {
     {
-        this.entity = new ImmutableByteArray( entity );
+        this.entity = ImmutableByteArray.of( entity );
         this.headerStrings = headerStrings;
         this.headerStrings = headerStrings;
     }
     }
 
 
     public byte[] getEntity( )
     public byte[] getEntity( )
     {
     {
-        return entity.getBytes();
+        return entity.copyOf();
     }
     }
 
 
     public Map<String, String> getHeaderStrings( )
     public Map<String, String> getHeaderStrings( )

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/resource/MemoryFileResource.java

@@ -43,12 +43,12 @@ class MemoryFileResource implements FileResource
 
 
     public InputStream getInputStream( ) throws IOException
     public InputStream getInputStream( ) throws IOException
     {
     {
-        return new ByteArrayInputStream( contents.getBytes() );
+        return new ByteArrayInputStream( contents.copyOf() );
     }
     }
 
 
     public long length( )
     public long length( )
     {
     {
-        return contents.getBytes().length;
+        return contents.copyOf().length;
     }
     }
 
 
     public long lastModified( )
     public long lastModified( )

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/resource/ResourceFileServlet.java

@@ -32,7 +32,7 @@ import password.pwm.http.HttpHeader;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.servlet.PwmServlet;
 import password.pwm.http.servlet.PwmServlet;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/resource/ResourceServletConfiguration.java

@@ -196,7 +196,7 @@ class ResourceServletConfiguration
     private static Map<String, FileResource> makeMemoryFileMapFromZipInput( final ImmutableByteArray content )
     private static Map<String, FileResource> makeMemoryFileMapFromZipInput( final ImmutableByteArray content )
             throws IOException
             throws IOException
     {
     {
-        final ZipInputStream stream = new ZipInputStream( new ByteArrayInputStream( content.getBytes() ) );
+        final ZipInputStream stream = new ZipInputStream( new ByteArrayInputStream( content.copyOf() ) );
         final Map<String, FileResource> memoryMap = new HashMap<>();
         final Map<String, FileResource> memoryMap = new HashMap<>();
 
 
         ZipEntry entry;
         ZipEntry entry;
@@ -208,7 +208,7 @@ class ResourceServletConfiguration
                 final long lastModified = entry.getTime();
                 final long lastModified = entry.getTime();
                 final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                 final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                 IOUtils.copy( stream, byteArrayOutputStream );
                 IOUtils.copy( stream, byteArrayOutputStream );
-                final ImmutableByteArray contents = new ImmutableByteArray( byteArrayOutputStream.toByteArray() );
+                final ImmutableByteArray contents = ImmutableByteArray.of( byteArrayOutputStream.toByteArray() );
                 memoryMap.put( name, new MemoryFileResource( name, contents, lastModified ) );
                 memoryMap.put( name, new MemoryFileResource( name, contents, lastModified ) );
                 {
                 {
                     final String finalEntry = entry.getName();
                     final String finalEntry = entry.getName();

+ 64 - 52
server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java

@@ -32,8 +32,9 @@ import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequest;
+import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.Percent;
 import password.pwm.util.java.Percent;
@@ -41,7 +42,6 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.ChecksumOutputStream;
 import password.pwm.util.secure.ChecksumOutputStream;
-import password.pwm.util.secure.PwmHashAlgorithm;
 
 
 import javax.servlet.ServletContext;
 import javax.servlet.ServletContext;
 import java.io.File;
 import java.io.File;
@@ -175,7 +175,7 @@ public class ResourceServletService implements PwmService
     }
     }
 
 
     private String makeResourcePathNonce( )
     private String makeResourcePathNonce( )
-            throws PwmUnrecoverableException, IOException
+            throws IOException
     {
     {
         final int nonceLength = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_PATH_NONCE_LENGTH ) );
         final int nonceLength = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_PATH_NONCE_LENGTH ) );
         final boolean enablePathNonce = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_ENABLE_PATH_NONCE ) );
         final boolean enablePathNonce = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_ENABLE_PATH_NONCE ) );
@@ -185,56 +185,9 @@ public class ResourceServletService implements PwmService
         }
         }
 
 
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
-        final ChecksumOutputStream checksumStream = new ChecksumOutputStream( PwmHashAlgorithm.SHA512, new NullOutputStream() );
+        final ImmutableByteArray checksumBytes = checksumAllResources( pwmApplication );
 
 
-        if ( pwmApplication.getPwmEnvironment().getContextManager() != null )
-        {
-            try
-            {
-                final File webInfPath = pwmApplication.getPwmEnvironment().getContextManager().locateWebInfFilePath();
-                if ( webInfPath != null && webInfPath.exists() )
-                {
-                    final File basePath = webInfPath.getParentFile();
-                    if ( basePath != null && basePath.exists() )
-                    {
-                        final File resourcePath = new File( basePath.getAbsolutePath() + File.separator + "public" + File.separator + "resources" );
-                        if ( resourcePath.exists() )
-                        {
-                            for ( final FileSystemUtility.FileSummaryInformation fileSummaryInformation : FileSystemUtility.readFileInformation( resourcePath ) )
-                            {
-                                checksumStream.write( ( fileSummaryInformation.getSha1sum() ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
-                            }
-                        }
-                    }
-                }
-            }
-            catch ( Exception e )
-            {
-                LOGGER.error( "unable to generate resource path nonce: " + e.getMessage() );
-            }
-        }
-
-        for ( final FileResource fileResource : getResourceServletConfiguration().getCustomFileBundle().values() )
-        {
-            JavaHelper.copy( fileResource.getInputStream(), checksumStream );
-        }
-
-        if ( getResourceServletConfiguration().getZipResources() != null )
-        {
-            for ( final String key : getResourceServletConfiguration().getZipResources().keySet() )
-            {
-                final ZipFile zipFile = getResourceServletConfiguration().getZipResources().get( key );
-                checksumStream.write( key.getBytes( PwmConstants.DEFAULT_CHARSET ) );
-                for ( Enumeration<? extends ZipEntry> zipEnum = zipFile.entries(); zipEnum.hasMoreElements(); )
-                {
-                    final ZipEntry entry = zipEnum.nextElement();
-                    JavaHelper.copy( zipFile.getInputStream( entry ), checksumStream );
-                }
-            }
-        }
-
-        final byte[] checksumBytes = checksumStream.getInProgressChecksum();
-        final String nonce = StringUtil.truncate( JavaHelper.byteArrayToHexString( checksumBytes ).toLowerCase(), nonceLength );
+        final String nonce = StringUtil.truncate( JavaHelper.byteArrayToHexString( checksumBytes.copyOf() ).toLowerCase(), nonceLength );
         LOGGER.debug( () -> "completed generation of nonce '" + nonce + "' in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
         LOGGER.debug( () -> "completed generation of nonce '" + nonce + "' in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
 
 
         final String noncePrefix = pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_NONCE_PATH_PREFIX );
         final String noncePrefix = pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_NONCE_PATH_PREFIX );
@@ -286,4 +239,63 @@ public class ResourceServletService implements PwmService
         LOGGER.debug( pwmRequest, () -> "check for theme validity of '" + themeName + "' returned false" );
         LOGGER.debug( pwmRequest, () -> "check for theme validity of '" + themeName + "' returned false" );
         return false;
         return false;
     }
     }
+
+    private ImmutableByteArray checksumAllResources( final PwmApplication pwmApplication )
+            throws IOException
+    {
+        try ( ChecksumOutputStream checksumStream = new ChecksumOutputStream( new NullOutputStream() ) )
+        {
+            checksumResourceFilePath( pwmApplication, checksumStream );
+
+            for ( final FileResource fileResource : getResourceServletConfiguration().getCustomFileBundle().values() )
+            {
+                JavaHelper.copy( fileResource.getInputStream(), checksumStream );
+            }
+
+            if ( getResourceServletConfiguration().getZipResources() != null )
+            {
+                for ( final String key : getResourceServletConfiguration().getZipResources().keySet() )
+                {
+                    final ZipFile zipFile = getResourceServletConfiguration().getZipResources().get( key );
+                    checksumStream.write( key.getBytes( PwmConstants.DEFAULT_CHARSET ) );
+                    for ( Enumeration<? extends ZipEntry> zipEnum = zipFile.entries(); zipEnum.hasMoreElements(); )
+                    {
+                        final ZipEntry entry = zipEnum.nextElement();
+                        JavaHelper.copy( zipFile.getInputStream( entry ), checksumStream );
+                    }
+                }
+            }
+            return checksumStream.checksum();
+        }
+    }
+
+    private static void checksumResourceFilePath( final PwmApplication pwmApplication, final ChecksumOutputStream checksumStream )
+    {
+        if ( pwmApplication.getPwmEnvironment().getContextManager() != null )
+        {
+            try
+            {
+                final File webInfPath = pwmApplication.getPwmEnvironment().getContextManager().locateWebInfFilePath();
+                if ( webInfPath != null && webInfPath.exists() )
+                {
+                    final File basePath = webInfPath.getParentFile();
+                    if ( basePath != null && basePath.exists() )
+                    {
+                        final File resourcePath = new File( basePath.getAbsolutePath() + File.separator + "public" + File.separator + "resources" );
+                        if ( resourcePath.exists() )
+                        {
+                            for ( final FileSystemUtility.FileSummaryInformation fileSummaryInformation : FileSystemUtility.readFileInformation( resourcePath ) )
+                            {
+                                checksumStream.write( JavaHelper.longToBytes( fileSummaryInformation.getChecksum() ) );
+                            }
+                        }
+                    }
+                }
+            }
+            catch ( Exception e )
+            {
+                LOGGER.error( "unable to generate resource path nonce: " + e.getMessage() );
+            }
+        }
+    }
 }
 }

+ 28 - 8
server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileUtil.java

@@ -45,6 +45,7 @@ import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.bean.UpdateProfileBean;
 import password.pwm.http.bean.UpdateProfileBean;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.UserInfoFactory;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenUtil;
 import password.pwm.svc.token.TokenUtil;
@@ -368,28 +369,47 @@ public class UpdateProfileUtil
 
 
         LdapOperationsHelper.writeFormValuesToLdap( theUser, formMap, macroMachine, false );
         LdapOperationsHelper.writeFormValuesToLdap( theUser, formMap, macroMachine, false );
 
 
-        final UserIdentity userIdentity = userInfo.getUserIdentity();
+        postUpdateActionsAndEmail( pwmApplication, sessionLabel, locale, userInfo.getUserIdentity(), updateProfileProfile );
+
+        // success, so forward to success page
+        pwmApplication.getStatisticsManager().incrementValue( Statistic.UPDATE_ATTRIBUTES );
+    }
+
+    private static void postUpdateActionsAndEmail(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final Locale locale,
+            final UserIdentity userIdentity,
+            final UpdateProfileProfile updateProfileProfile
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException
+    {
+        // obtain new macro machine (with a new UserInfo) so old cached values won't be used for next op
+        final UserInfo reloadedUserInfo = UserInfoFactory.newUserInfo(
+                pwmApplication,
+                sessionLabel,
+                locale,
+                userIdentity,
+                pwmApplication.getProxiedChaiUser( userIdentity ).getChaiProvider() );
+        final MacroMachine reloadedMacroMachine = MacroMachine.forUser( pwmApplication, sessionLabel, reloadedUserInfo, null, null );
 
 
         {
         {
             // execute configured actions
             // execute configured actions
             final List<ActionConfiguration> actions = updateProfileProfile.readSettingAsAction( PwmSetting.UPDATE_PROFILE_WRITE_ATTRIBUTES );
             final List<ActionConfiguration> actions = updateProfileProfile.readSettingAsAction( PwmSetting.UPDATE_PROFILE_WRITE_ATTRIBUTES );
             if ( actions != null && !actions.isEmpty() )
             if ( actions != null && !actions.isEmpty() )
             {
             {
-                LOGGER.debug( sessionLabel, () -> "executing configured actions to user " + userIdentity );
-
+                LOGGER.debug( sessionLabel, () -> "executing configured actions to user " + reloadedUserInfo.getUserIdentity() );
 
 
-                final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
+                final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, reloadedUserInfo.getUserIdentity() )
                         .setExpandPwmMacros( true )
                         .setExpandPwmMacros( true )
-                        .setMacroMachine( macroMachine )
+                        .setMacroMachine( reloadedMacroMachine )
                         .createActionExecutor();
                         .createActionExecutor();
 
 
                 actionExecutor.executeActions( actions, sessionLabel );
                 actionExecutor.executeActions( actions, sessionLabel );
             }
             }
         }
         }
-        sendProfileUpdateEmailNotice( pwmApplication, macroMachine, userInfo, locale, sessionLabel );
 
 
-        // success, so forward to success page
-        pwmApplication.getStatisticsManager().incrementValue( Statistic.UPDATE_ATTRIBUTES );
+        sendProfileUpdateEmailNotice( pwmApplication, reloadedMacroMachine, reloadedUserInfo, locale, sessionLabel );
     }
     }
 
 
     static TokenDestinationItem tokenDestinationItemForCurrentValidation(
     static TokenDestinationItem tokenDestinationItemForCurrentValidation(

+ 7 - 2
server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java

@@ -55,6 +55,7 @@ import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.cache.CacheKey;
 import password.pwm.svc.cache.CacheKey;
 import password.pwm.svc.cache.CachePolicy;
 import password.pwm.svc.cache.CachePolicy;
+import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
@@ -605,7 +606,7 @@ public class LdapOperationsHelper
     )
     )
             throws ChaiUnavailableException, PwmUnrecoverableException
             throws ChaiUnavailableException, PwmUnrecoverableException
     {
     {
-        return createChaiProvider(
+        final ChaiProvider chaiProvider = createChaiProvider(
                 pwmApplication.getLdapConnectionService().getChaiProviderFactory(),
                 pwmApplication.getLdapConnectionService().getChaiProviderFactory(),
                 sessionLabel,
                 sessionLabel,
                 ldapProfile,
                 ldapProfile,
@@ -613,6 +614,10 @@ public class LdapOperationsHelper
                 userDN,
                 userDN,
                 userPassword
                 userPassword
         );
         );
+
+        pwmApplication.getStatisticsManager().updateEps( EpsStatistic.LDAP_BINDS, 1 );
+
+        return chaiProvider;
     }
     }
 
 
     public static ChaiProvider createChaiProvider(
     public static ChaiProvider createChaiProvider(
@@ -1039,7 +1044,7 @@ public class LdapOperationsHelper
         {
         {
             throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_INTERNAL, "error reading user photo ldap attribute: " + e.getMessage() ) );
             throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_INTERNAL, "error reading user photo ldap attribute: " + e.getMessage() ) );
         }
         }
-        return new PhotoDataBean( mimeType, new ImmutableByteArray( photoData ) );
+        return new PhotoDataBean( mimeType, ImmutableByteArray.of( photoData ) );
     }
     }
 
 
 
 

+ 3 - 2
server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -52,6 +52,7 @@ import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.intruder.IntruderManager;
 import password.pwm.svc.intruder.IntruderManager;
 import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.intruder.RecordType;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.stats.StatisticsManager;
@@ -86,7 +87,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest
     private AuthenticationStrategy strategy = AuthenticationStrategy.BIND;
     private AuthenticationStrategy strategy = AuthenticationStrategy.BIND;
     private Instant startTime;
     private Instant startTime;
 
 
-    private static final AtomicLoopIntIncrementer OPERATION_COUNTER = new AtomicLoopIntIncrementer( 0 );
+    private static final AtomicLoopIntIncrementer OPERATION_COUNTER = new AtomicLoopIntIncrementer();
     private final int operationNumber;
     private final int operationNumber;
 
 
 
 
@@ -349,7 +350,7 @@ class LDAPAuthenticationRequest implements AuthenticationRequest
         final StatisticsManager statisticsManager = pwmApplication.getStatisticsManager();
         final StatisticsManager statisticsManager = pwmApplication.getStatisticsManager();
         statisticsManager.incrementValue( Statistic.AUTHENTICATIONS );
         statisticsManager.incrementValue( Statistic.AUTHENTICATIONS );
         statisticsManager.updateEps( EpsStatistic.AUTHENTICATION, 1 );
         statisticsManager.updateEps( EpsStatistic.AUTHENTICATION, 1 );
-        statisticsManager.updateAverageValue( Statistic.AVG_AUTHENTICATION_TIME,
+        statisticsManager.updateAverageValue( AvgStatistic.AVG_AUTHENTICATION_TIME,
                 TimeDuration.fromCurrent( startTime ).asMillis() );
                 TimeDuration.fromCurrent( startTime ).asMillis() );
 
 
 
 

+ 28 - 9
server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java

@@ -46,7 +46,7 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
@@ -66,6 +66,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeMap;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.ArrayBlockingQueue;
@@ -533,7 +534,7 @@ public class UserSearchEngine implements PwmService
 
 
         if ( pwmApplication.getStatisticsManager() != null && pwmApplication.getStatisticsManager().status() == PwmService.STATUS.OPEN )
         if ( pwmApplication.getStatisticsManager() != null && pwmApplication.getStatisticsManager().status() == PwmService.STATUS.OPEN )
         {
         {
-            pwmApplication.getStatisticsManager().updateAverageValue( Statistic.AVG_LDAP_SEARCH_TIME, searchDuration.asMillis() );
+            pwmApplication.getStatisticsManager().updateAverageValue( AvgStatistic.AVG_LDAP_SEARCH_TIME, searchDuration.asMillis() );
         }
         }
 
 
         if ( results.isEmpty() )
         if ( results.isEmpty() )
@@ -558,21 +559,39 @@ public class UserSearchEngine implements PwmService
     private void validateSpecifiedContext( final LdapProfile profile, final String context )
     private void validateSpecifiedContext( final LdapProfile profile, final String context )
             throws PwmOperationalException, PwmUnrecoverableException
             throws PwmOperationalException, PwmUnrecoverableException
     {
     {
-        final Map<String, String> selectableContexts = profile.getSelectableContexts( pwmApplication );
-        if ( selectableContexts == null || selectableContexts.isEmpty() )
+        Objects.requireNonNull( profile, "ldapProfile can not be null for ldap search context validation" );
+        Objects.requireNonNull( context, "context can not be null for ldap search context validation" );
+
+        final String canonicalContext = profile.readCanonicalDN( pwmApplication, context );
+
         {
         {
-            throw new PwmOperationalException( PwmError.ERROR_INTERNAL, "context specified, but no selectable contexts are configured" );
+            final Map<String, String> selectableContexts = profile.getSelectableContexts( pwmApplication );
+            if ( !JavaHelper.isEmpty( selectableContexts ) && selectableContexts.containsKey( canonicalContext ) )
+            {
+                // config pre-validates selectable contexts so this should be permitted
+                return;
+            }
         }
         }
 
 
-        for ( final String loopContext : selectableContexts.keySet() )
         {
         {
-            if ( loopContext.equals( context ) )
+            final List<String> rootContexts = profile.getRootContexts( pwmApplication );
+            if ( !JavaHelper.isEmpty( rootContexts ) )
             {
             {
-                return;
+                for ( final String rootContext : rootContexts )
+                {
+                    if ( canonicalContext.endsWith( rootContext ) )
+                    {
+                        return;
+                    }
+                }
+
+                final String msg = "specified search context '" + canonicalContext + "' is not contained by a configured root context";
+                throw new PwmUnrecoverableException( PwmError.CONFIG_FORMAT_ERROR, msg );
             }
             }
         }
         }
 
 
-        throw new PwmOperationalException( PwmError.ERROR_INTERNAL, "context '" + context + "' is specified, but is not in configuration" );
+        final String msg = "specified search context '" + canonicalContext + "', but no selectable contexts or root are configured";
+        throw new PwmOperationalException( PwmError.ERROR_INTERNAL, msg );
     }
     }
 
 
     private boolean checkIfStringIsDN(
     private boolean checkIfStringIsDN(

+ 3 - 3
server/src/main/java/password/pwm/svc/node/LDAPNodeDataService.java

@@ -86,7 +86,7 @@ class LDAPNodeDataService implements NodeDataServiceProvider
         }
         }
         catch ( ChaiException e )
         catch ( ChaiException e )
         {
         {
-            throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error reading cluster data: " + e.getMessage() );
+            throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error reading node service data: " + e.getMessage() );
         }
         }
 
 
         return returnData;
         return returnData;
@@ -116,7 +116,7 @@ class LDAPNodeDataService implements NodeDataServiceProvider
         }
         }
         catch ( ChaiException e )
         catch ( ChaiException e )
         {
         {
-            throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error writing cluster data: " + e.getMessage() );
+            throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error writing node service data: " + e.getMessage() );
         }
         }
 
 
     }
     }
@@ -148,7 +148,7 @@ class LDAPNodeDataService implements NodeDataServiceProvider
                 }
                 }
                 catch ( ChaiException e )
                 catch ( ChaiException e )
                 {
                 {
-                    throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error purging cluster data: " + e.getMessage() );
+                    throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error purging node service data: " + e.getMessage() );
                 }
                 }
             }
             }
         }
         }

+ 2 - 2
server/src/main/java/password/pwm/svc/node/NodeService.java

@@ -84,7 +84,7 @@ public class NodeService implements PwmService
                 {
                 {
                     case DB:
                     case DB:
                     {
                     {
-                        LOGGER.trace( () -> "starting database-backed cluster provider" );
+                        LOGGER.trace( () -> "starting database-backed node service provider" );
                         nodeServiceSettings = NodeServiceSettings.fromConfigForDB( pwmApplication.getConfig() );
                         nodeServiceSettings = NodeServiceSettings.fromConfigForDB( pwmApplication.getConfig() );
                         clusterDataServiceProvider = new DatabaseNodeDataService( pwmApplication );
                         clusterDataServiceProvider = new DatabaseNodeDataService( pwmApplication );
                     }
                     }
@@ -92,7 +92,7 @@ public class NodeService implements PwmService
 
 
                     case LDAP:
                     case LDAP:
                     {
                     {
-                        LOGGER.trace( () -> "starting ldap-backed cluster provider" );
+                        LOGGER.trace( () -> "starting ldap-backed node service provider" );
                         nodeServiceSettings = NodeServiceSettings.fromConfigForLDAP( pwmApplication.getConfig() );
                         nodeServiceSettings = NodeServiceSettings.fromConfigForLDAP( pwmApplication.getConfig() );
                         clusterDataServiceProvider = new LDAPNodeDataService( pwmApplication );
                         clusterDataServiceProvider = new LDAPNodeDataService( pwmApplication );
                     }
                     }

+ 5 - 0
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java

@@ -185,6 +185,11 @@ public class PwNotifyEngine
                     + ", sent " + noticeCount + " notices."
                     + ", sent " + noticeCount + " notices."
             );
             );
         }
         }
+        catch ( PwmUnrecoverableException | PwmOperationalException e )
+        {
+            log( "error while executing job: " + e.getMessage() );
+            throw e;
+        }
         finally
         finally
         {
         {
             running = false;
             running = false;

+ 1 - 1
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java

@@ -259,7 +259,7 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
         if ( !isRunning() )
         if ( !isRunning() )
         {
         {
             nextExecutionTime = Instant.now();
             nextExecutionTime = Instant.now();
-            pwmApplication.getPwmScheduler().scheduleFutureJob( new PwNotifyJob(), executorService, TimeDuration.ZERO );
+            pwmApplication.getPwmScheduler().scheduleJob( new PwNotifyJob(), executorService, TimeDuration.ZERO );
         }
         }
     }
     }
 
 

+ 3 - 3
server/src/main/java/password/pwm/svc/report/ReportService.java

@@ -40,7 +40,7 @@ import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.TransactionSizeCalculator;
 import password.pwm.util.TransactionSizeCalculator;
 import password.pwm.util.java.BlockingThreadPool;
 import password.pwm.util.java.BlockingThreadPool;
@@ -398,7 +398,7 @@ public class ReportService implements PwmService
                         if ( executorService != null )
                         if ( executorService != null )
                         {
                         {
                             LOGGER.error( SessionLabel.REPORTING_SESSION_LABEL, "directory unavailable error during background SearchLDAP, will retry; error: " + e.getMessage() );
                             LOGGER.error( SessionLabel.REPORTING_SESSION_LABEL, "directory unavailable error during background SearchLDAP, will retry; error: " + e.getMessage() );
-                            pwmApplication.getPwmScheduler().scheduleFutureJob( new ReadLDAPTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
+                            pwmApplication.getPwmScheduler().scheduleJob( new ReadLDAPTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
                             errorProcessed = true;
                             errorProcessed = true;
                         }
                         }
                     }
                     }
@@ -477,7 +477,7 @@ public class ReportService implements PwmService
                         if ( executorService != null )
                         if ( executorService != null )
                         {
                         {
                             LOGGER.error( SessionLabel.REPORTING_SESSION_LABEL, "directory unavailable error during background ReadData, will retry; error: " + e.getMessage() );
                             LOGGER.error( SessionLabel.REPORTING_SESSION_LABEL, "directory unavailable error during background ReadData, will retry; error: " + e.getMessage() );
-                            pwmApplication.getPwmScheduler().scheduleFutureJob( new ProcessWorkQueueTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
+                            pwmApplication.getPwmScheduler().scheduleJob( new ProcessWorkQueueTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
                         }
                         }
                     }
                     }
                     else
                     else

+ 4 - 0
server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java

@@ -32,9 +32,11 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpHeader;
 import password.pwm.http.HttpHeader;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequest;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
 import java.io.IOException;
 import java.io.IOException;
+import java.time.Instant;
 
 
 public class UserAgentUtils
 public class UserAgentUtils
 {
 {
@@ -62,7 +64,9 @@ public class UserAgentUtils
 
 
     public static void initializeCache() throws PwmUnrecoverableException
     public static void initializeCache() throws PwmUnrecoverableException
     {
     {
+        final Instant startTime = Instant.now();
         getUserAgentParser();
         getUserAgentParser();
+        LOGGER.trace( () -> "loaded useragent parser in " + TimeDuration.compactFromCurrent( startTime ) );
     }
     }
 
 
     public static void checkIfPreIE11( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
     public static void checkIfPreIE11( final PwmRequest pwmRequest ) throws PwmUnrecoverableException

+ 74 - 0
server/src/main/java/password/pwm/svc/stats/AvgStatistic.java

@@ -0,0 +1,74 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+import password.pwm.i18n.Admin;
+import password.pwm.util.i18n.LocaleHelper;
+
+import java.util.Locale;
+
+public enum AvgStatistic
+{
+    AVG_PASSWORD_SYNC_TIME( "AvgPasswordSyncTime", null, "ms" ),
+    AVG_AUTHENTICATION_TIME( "AvgAuthenticationTime", null, "ms" ),
+    AVG_PASSWORD_STRENGTH( "AvgPasswordStrength", null, "" ),
+    AVG_LDAP_SEARCH_TIME( "AvgLdapSearchTime", null, "ms" ),
+    AVG_REQUEST_PROCESS_TIME( "AvgRequestProcessTime", null, "ms" ),;
+
+    private final String key;
+    private final Statistic.StatDetail statDetail;
+    private final String unit;
+
+    AvgStatistic(
+            final String key,
+            final Statistic.StatDetail statDetail,
+            final String unit
+    )
+    {
+        this.key = key;
+        this.statDetail = statDetail;
+        this.unit = unit;
+    }
+
+    public String getKey( )
+    {
+        return key;
+    }
+
+    public String getUnit()
+    {
+        return unit;
+    }
+
+    public String getLabel( final Locale locale )
+    {
+        final String keyName = Admin.STATISTICS_LABEL_PREFIX + this.getKey();
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
+    }
+
+    public String getDescription( final Locale locale )
+    {
+        final String keyName = Admin.STATISTICS_DESCRIPTION_PREFIX + this.getKey();
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
+    }
+}

+ 79 - 0
server/src/main/java/password/pwm/svc/stats/DailyKey.java

@@ -0,0 +1,79 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+import lombok.Value;
+
+import java.time.LocalDate;
+
+@Value
+public class DailyKey
+{
+    private static final String DB_KEY_PREFIX_DAILY = "DAILY_";
+    private int year;
+    private int day;
+
+    private DailyKey()
+    {
+        final LocalDate localDate = LocalDate.now();
+        year = localDate.getYear();
+        day = localDate.getDayOfYear();
+    }
+
+    DailyKey( final String value )
+    {
+        final String strippedValue = value.substring( DB_KEY_PREFIX_DAILY.length() );
+        final String[] splitValue = strippedValue.split( "_" );
+        year = Integer.parseInt( splitValue[ 0 ] );
+        day = Integer.parseInt( splitValue[ 1 ] );
+    }
+
+    private DailyKey( final int year, final int day )
+    {
+        this.year = year;
+        this.day = day;
+    }
+
+    @Override
+    public String toString( )
+    {
+        return DB_KEY_PREFIX_DAILY + year + "_" + day;
+    }
+
+    public static DailyKey forToday()
+    {
+        return new DailyKey( );
+    }
+
+    public DailyKey previous( )
+    {
+        final LocalDate thisDay = localDate();
+        final LocalDate previousDay = thisDay.minusDays( 1 );
+        return new DailyKey( previousDay.getYear(), previousDay.getDayOfYear() );
+    }
+
+    public LocalDate localDate()
+    {
+        return LocalDate.ofYearDay( year, day );
+    }
+}

+ 49 - 0
server/src/main/java/password/pwm/svc/stats/EpsKey.java

@@ -0,0 +1,49 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+import lombok.Value;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Value
+class EpsKey
+{
+    static final String DB_KEY_PREFIX = "EPS-";
+    private EpsStatistic epsStatistic;
+    private Statistic.EpsDuration epsDuration;
+
+    static Set<EpsKey> allKeys()
+    {
+        final Set<EpsKey> returnSet = new HashSet<>();
+        for ( final EpsStatistic epsStatistic : EpsStatistic.values() )
+        {
+            for ( final Statistic.EpsDuration epsDuration : Statistic.EpsDuration.values() )
+            {
+                returnSet.add( new EpsKey( epsStatistic, epsDuration ) );
+            }
+        }
+        return returnSet;
+    }
+}

+ 10 - 21
server/src/main/java/password/pwm/svc/stats/EpsStatistic.java

@@ -29,27 +29,16 @@ import java.util.Locale;
 
 
 public enum EpsStatistic
 public enum EpsStatistic
 {
 {
-    REQUESTS( null ),
-    SESSIONS( null ),
-    PASSWORD_CHANGES( Statistic.PASSWORD_CHANGES ),
-    AUTHENTICATION( Statistic.AUTHENTICATIONS ),
-    INTRUDER_ATTEMPTS( Statistic.INTRUDER_ATTEMPTS ),
-    PWMDB_WRITES( null ),
-    PWMDB_READS( null ),
-    DB_WRITES( null ),
-    DB_READS( null ),;
-
-    private Statistic relatedStatistic;
-
-    EpsStatistic( final Statistic relatedStatistic )
-    {
-        this.relatedStatistic = relatedStatistic;
-    }
-
-    public Statistic getRelatedStatistic( )
-    {
-        return relatedStatistic;
-    }
+    REQUESTS(),
+    SESSIONS(),
+    PASSWORD_CHANGES(),
+    AUTHENTICATION(),
+    INTRUDER_ATTEMPTS(),
+    PWMDB_WRITES(),
+    PWMDB_READS(),
+    DB_WRITES(),
+    DB_READS(),
+    LDAP_BINDS,;
 
 
     public String getLabel( final Locale locale )
     public String getLabel( final Locale locale )
     {
     {

+ 27 - 0
server/src/main/java/password/pwm/svc/stats/StatKey.java

@@ -0,0 +1,27 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+public interface StatKey
+{
+}

+ 80 - 117
server/src/main/java/password/pwm/svc/stats/Statistic.java

@@ -27,112 +27,101 @@ import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Admin;
 import password.pwm.i18n.Admin;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
-import password.pwm.util.logging.PwmLogger;
 
 
 import java.util.Arrays;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Comparator;
 import java.util.Locale;
 import java.util.Locale;
-import java.util.MissingResourceException;
 import java.util.SortedSet;
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.TreeSet;
 
 
 public enum Statistic
 public enum Statistic
 {
 {
-    AUDIT_EVENTS( Type.INCREMENTER, "AuditEvents", null ),
-    AUTHENTICATIONS( Type.INCREMENTER, "Authentications", null ),
-    AUTHENTICATION_FAILURES( Type.INCREMENTER, "AuthenticationFailures", null ),
-    AUTHENTICATION_EXPIRED( Type.INCREMENTER, "Authentications_Expired", null ),
-    AUTHENTICATION_PRE_EXPIRED( Type.INCREMENTER, "Authentications_PreExpired", null ),
-    AUTHENTICATION_EXPIRED_WARNING( Type.INCREMENTER, "Authentications_ExpiredWarning", null ),
-    PWM_STARTUPS( Type.INCREMENTER, "PWM_Startups", null ),
-    PWM_UNKNOWN_ERRORS( Type.INCREMENTER, "PWM_UnknownErrors", null ),
-    PASSWORD_CHANGES( Type.INCREMENTER, "PasswordChanges", null ),
-    FORGOTTEN_USERNAME_FAILURES( Type.INCREMENTER, "ForgottenUsernameFailures", null ),
-    FORGOTTEN_USERNAME_SUCCESSES( Type.INCREMENTER, "ForgottenUsernameSuccesses", null ),
-    EMAIL_SEND_SUCCESSES( Type.INCREMENTER, "EmailSendSuccesses", null ),
-    EMAIL_SEND_FAILURES( Type.INCREMENTER, "EmailSendFailures", null ),
-    EMAIL_SEND_DISCARDS( Type.INCREMENTER, "EmailSendDiscards", null ),
-    SMS_SEND_SUCCESSES( Type.INCREMENTER, "SmsSendSuccesses", null ),
-    SMS_SEND_FAILURES( Type.INCREMENTER, "SmsSendFailures", null ),
-    SMS_SEND_DISCARDS( Type.INCREMENTER, "SmsSendDiscards", null ),
-    PASSWORD_RULE_CHECKS( Type.INCREMENTER, "PasswordRuleChecks", null ),
-    HTTP_REQUESTS( Type.INCREMENTER, "HttpRequests", null ),
-    HTTP_RESOURCE_REQUESTS( Type.INCREMENTER, "HttpResourceRequests", null ),
-    HTTP_SESSIONS( Type.INCREMENTER, "HttpSessions", null ),
-    ACTIVATED_USERS( Type.INCREMENTER, "ActivatedUsers", null ),
-    NEW_USERS( Type.INCREMENTER, "NewUsers", new ConfigSettingDetail( PwmSetting.NEWUSER_ENABLE ) ),
-    GUESTS( Type.INCREMENTER, "Guests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
-    UPDATED_GUESTS( Type.INCREMENTER, "UpdatedGuests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
-    LOCKED_USERS( Type.INCREMENTER, "LockedUsers", null ),
-    LOCKED_ADDRESSES( Type.INCREMENTER, "LockedAddresses", null ),
-    LOCKED_USERIDS( Type.INCREMENTER, "LockedUserDNs", null ),
-    LOCKED_ATTRIBUTES( Type.INCREMENTER, "LockedAttributes", null ),
-    LOCKED_TOKENDESTS( Type.INCREMENTER, "LockedTokenDests", null ),
-    CAPTCHA_SUCCESSES( Type.INCREMENTER, "CaptchaSuccessess", null ),
-    CAPTCHA_FAILURES( Type.INCREMENTER, "CaptchaFailures", null ),
-    CAPTCHA_PRESENTATIONS( Type.INCREMENTER, "CaptchaPresentations", null ),
-    LDAP_UNAVAILABLE_COUNT( Type.INCREMENTER, "LdapUnavailableCount", null ),
-    DB_UNAVAILABLE_COUNT( Type.INCREMENTER, "DatabaseUnavailableCount", null ),
-    SETUP_RESPONSES( Type.INCREMENTER, "SetupResponses", null ),
-    SETUP_OTP_SECRET( Type.INCREMENTER, "SetupOtpSecret", null ),
-    UPDATE_ATTRIBUTES( Type.INCREMENTER, "UpdateAttributes", new ConfigSettingDetail( PwmSetting.UPDATE_PROFILE_ENABLE ) ),
-    SHORTCUTS_SELECTED( Type.INCREMENTER, "ShortcutsSelected", new ConfigSettingDetail( PwmSetting.SHORTCUT_ENABLE ) ),
-    GENERATED_PASSWORDS( Type.INCREMENTER, "GeneratedPasswords", null ),
-    RECOVERY_SUCCESSES( Type.INCREMENTER, "RecoverySuccesses", null ),
-    RECOVERY_FAILURES( Type.INCREMENTER, "RecoveryFailures", null ),
-    TOKENS_SENT( Type.INCREMENTER, "TokensSent", null ),
-    TOKENS_PASSSED( Type.INCREMENTER, "TokensPassed", null ),
-    RECOVERY_TOKENS_SENT( Type.INCREMENTER, "RecoveryTokensSent", null ),
-    RECOVERY_TOKENS_PASSED( Type.INCREMENTER, "RecoveryTokensPassed", null ),
-    RECOVERY_TOKENS_FAILED( Type.INCREMENTER, "RecoveryTokensFailed", null ),
-    RECOVERY_OTP_PASSED( Type.INCREMENTER, "RecoveryOTPPassed", null ),
-    RECOVERY_OTP_FAILED( Type.INCREMENTER, "RecoveryOTPFailed", null ),
-    PEOPLESEARCH_CACHE_HITS( Type.INCREMENTER, "PeopleSearchCacheHits", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_CACHE_MISSES( Type.INCREMENTER, "PeopleSearchCacheMisses", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_SEARCHES( Type.INCREMENTER, "PeopleSearchSearches", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_DETAILS( Type.INCREMENTER, "PeopleSearchDetails", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PEOPLESEARCH_ORGCHART( Type.INCREMENTER, "PeopleSearchOrgChart", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
-    PWNOTIFY_JOBS ( Type.INCREMENTER, "PwNotifyJobs", null ),
-    PWNOTIFY_JOB_ERRORS ( Type.INCREMENTER, "PwNotifyJobErrors", null ),
-    PWNOTIFY_EMAILS_SENT ( Type.INCREMENTER, "PwNotifyJobEmailsSent", null ),
-    HELPDESK_PASSWORD_SET( Type.INCREMENTER, "HelpdeskPasswordSet", null ),
-    HELPDESK_USER_LOOKUP( Type.INCREMENTER, "HelpdeskUserLookup", null ),
-    HELPDESK_TOKENS_SENT( Type.INCREMENTER, "HelpdeskTokenSent", null ),
-    HELPDESK_UNLOCK( Type.INCREMENTER, "HelpdeskUnlock", null ),
-    HELPDESK_VERIFY_OTP( Type.INCREMENTER, "HelpdeskVerifyOTP", null ),
-    REST_STATUS( Type.INCREMENTER, "RestStatus", null ),
-    REST_CHECKPASSWORD( Type.INCREMENTER, "RestCheckPassword", null ),
-    REST_SETPASSWORD( Type.INCREMENTER, "RestSetPassword", null ),
-    REST_RANDOMPASSWORD( Type.INCREMENTER, "RestRandomPassword", null ),
-    REST_PROFILE( Type.INCREMENTER, "RestProfile", null ),
-    REST_SIGNING_FORM( Type.INCREMENTER, "RestSigningForm", null ),
-    REST_CHALLENGES( Type.INCREMENTER, "RestChallenges", null ),
-    REST_HEALTH( Type.INCREMENTER, "RestHealth", null ),
-    REST_STATISTICS( Type.INCREMENTER, "RestStatistics", null ),
-    REST_VERIFYCHALLENGES( Type.INCREMENTER, "RestVerifyChallenges", null ),
-    REST_VERIFYOTP( Type.INCREMENTER, "RestVerifyOTP", null ),
-    INTRUDER_ATTEMPTS( Type.INCREMENTER, "IntruderAttempts", null ),
-    FOREIGN_SESSIONS_ACCEPTED( Type.INCREMENTER, "ForeignSessionsAccepted", null ),
-    OBSOLETE_URL_REQUESTS( Type.INCREMENTER, "ObsoleteUrlRequests", null ),
-    SYSLOG_MESSAGES_SENT( Type.INCREMENTER, "SyslogMessagesSent", null ),
+    AUDIT_EVENTS( "AuditEvents", null ),
+    AUTHENTICATIONS( "Authentications", null ),
+    AUTHENTICATION_FAILURES( "AuthenticationFailures", null ),
+    AUTHENTICATION_EXPIRED( "Authentications_Expired", null ),
+    AUTHENTICATION_PRE_EXPIRED( "Authentications_PreExpired", null ),
+    AUTHENTICATION_EXPIRED_WARNING( "Authentications_ExpiredWarning", null ),
+    PWM_STARTUPS( "PWM_Startups", null ),
+    PWM_UNKNOWN_ERRORS( "PWM_UnknownErrors", null ),
+    PASSWORD_CHANGES( "PasswordChanges", null ),
+    FORGOTTEN_USERNAME_FAILURES( "ForgottenUsernameFailures", null ),
+    FORGOTTEN_USERNAME_SUCCESSES( "ForgottenUsernameSuccesses", null ),
+    EMAIL_SEND_SUCCESSES( "EmailSendSuccesses", null ),
+    EMAIL_SEND_FAILURES( "EmailSendFailures", null ),
+    EMAIL_SEND_DISCARDS( "EmailSendDiscards", null ),
+    SMS_SEND_SUCCESSES( "SmsSendSuccesses", null ),
+    SMS_SEND_FAILURES( "SmsSendFailures", null ),
+    SMS_SEND_DISCARDS( "SmsSendDiscards", null ),
+    PASSWORD_RULE_CHECKS( "PasswordRuleChecks", null ),
+    HTTP_REQUESTS( "HttpRequests", null ),
+    HTTP_RESOURCE_REQUESTS( "HttpResourceRequests", null ),
+    HTTP_SESSIONS( "HttpSessions", null ),
+    ACTIVATED_USERS( "ActivatedUsers", null ),
+    NEW_USERS( "NewUsers", new ConfigSettingDetail( PwmSetting.NEWUSER_ENABLE ) ),
+    GUESTS( "Guests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
+    UPDATED_GUESTS( "UpdatedGuests", new ConfigSettingDetail( PwmSetting.GUEST_ENABLE ) ),
+    LOCKED_USERS( "LockedUsers", null ),
+    LOCKED_ADDRESSES( "LockedAddresses", null ),
+    LOCKED_USERIDS( "LockedUserDNs", null ),
+    LOCKED_ATTRIBUTES( "LockedAttributes", null ),
+    LOCKED_TOKENDESTS( "LockedTokenDests", null ),
+    CAPTCHA_SUCCESSES( "CaptchaSuccessess", null ),
+    CAPTCHA_FAILURES( "CaptchaFailures", null ),
+    CAPTCHA_PRESENTATIONS( "CaptchaPresentations", null ),
+    LDAP_UNAVAILABLE_COUNT( "LdapUnavailableCount", null ),
+    DB_UNAVAILABLE_COUNT( "DatabaseUnavailableCount", null ),
+    SETUP_RESPONSES( "SetupResponses", null ),
+    SETUP_OTP_SECRET( "SetupOtpSecret", null ),
+    UPDATE_ATTRIBUTES( "UpdateAttributes", new ConfigSettingDetail( PwmSetting.UPDATE_PROFILE_ENABLE ) ),
+    SHORTCUTS_SELECTED( "ShortcutsSelected", new ConfigSettingDetail( PwmSetting.SHORTCUT_ENABLE ) ),
+    GENERATED_PASSWORDS( "GeneratedPasswords", null ),
+    RECOVERY_SUCCESSES( "RecoverySuccesses", null ),
+    RECOVERY_FAILURES( "RecoveryFailures", null ),
+    TOKENS_SENT( "TokensSent", null ),
+    TOKENS_PASSSED( "TokensPassed", null ),
+    RECOVERY_TOKENS_SENT( "RecoveryTokensSent", null ),
+    RECOVERY_TOKENS_PASSED( "RecoveryTokensPassed", null ),
+    RECOVERY_TOKENS_FAILED( "RecoveryTokensFailed", null ),
+    RECOVERY_OTP_PASSED( "RecoveryOTPPassed", null ),
+    RECOVERY_OTP_FAILED( "RecoveryOTPFailed", null ),
+    PEOPLESEARCH_CACHE_HITS( "PeopleSearchCacheHits", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_CACHE_MISSES( "PeopleSearchCacheMisses", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_SEARCHES( "PeopleSearchSearches", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_DETAILS( "PeopleSearchDetails", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PEOPLESEARCH_ORGCHART( "PeopleSearchOrgChart", new ConfigSettingDetail( PwmSetting.PEOPLE_SEARCH_ENABLE ) ),
+    PWNOTIFY_JOBS ( "PwNotifyJobs", null ),
+    PWNOTIFY_JOB_ERRORS ( "PwNotifyJobErrors", null ),
+    PWNOTIFY_EMAILS_SENT ( "PwNotifyJobEmailsSent", null ),
+    HELPDESK_PASSWORD_SET( "HelpdeskPasswordSet", null ),
+    HELPDESK_USER_LOOKUP( "HelpdeskUserLookup", null ),
+    HELPDESK_TOKENS_SENT( "HelpdeskTokenSent", null ),
+    HELPDESK_UNLOCK( "HelpdeskUnlock", null ),
+    HELPDESK_VERIFY_OTP( "HelpdeskVerifyOTP", null ),
+    REST_STATUS( "RestStatus", null ),
+    REST_CHECKPASSWORD( "RestCheckPassword", null ),
+    REST_SETPASSWORD( "RestSetPassword", null ),
+    REST_RANDOMPASSWORD( "RestRandomPassword", null ),
+    REST_PROFILE( "RestProfile", null ),
+    REST_SIGNING_FORM( "RestSigningForm", null ),
+    REST_CHALLENGES( "RestChallenges", null ),
+    REST_HEALTH( "RestHealth", null ),
+    REST_STATISTICS( "RestStatistics", null ),
+    REST_VERIFYCHALLENGES( "RestVerifyChallenges", null ),
+    REST_VERIFYOTP( "RestVerifyOTP", null ),
+    INTRUDER_ATTEMPTS( "IntruderAttempts", null ),
+    FOREIGN_SESSIONS_ACCEPTED( "ForeignSessionsAccepted", null ),
+    OBSOLETE_URL_REQUESTS( "ObsoleteUrlRequests", null ),
+    SYSLOG_MESSAGES_SENT( "SyslogMessagesSent", null ),;
 
 
-    AVG_PASSWORD_SYNC_TIME( Type.AVERAGE, "AvgPasswordSyncTime", null ),
-    AVG_AUTHENTICATION_TIME( Type.AVERAGE, "AvgAuthenticationTime", null ),
-    AVG_PASSWORD_STRENGTH( Type.AVERAGE, "AvgPasswordStrength", null ),
-    AVG_LDAP_SEARCH_TIME( Type.AVERAGE, "AvgLdapSearchTime", null ),;
-
-    private static final PwmLogger LOGGER = PwmLogger.forClass( Statistic.class );
-    private final Type type;
     private final String key;
     private final String key;
     private final StatDetail statDetail;
     private final StatDetail statDetail;
 
 
     Statistic(
     Statistic(
-            final Type type,
             final String key,
             final String key,
             final StatDetail statDetail
             final StatDetail statDetail
     )
     )
     {
     {
-        this.type = type;
         this.key = key;
         this.key = key;
         this.statDetail = statDetail;
         this.statDetail = statDetail;
     }
     }
@@ -142,11 +131,6 @@ public enum Statistic
         return key;
         return key;
     }
     }
 
 
-    public Type getType( )
-    {
-        return type;
-    }
-
     public boolean isActive( final PwmApplication pwmApplication )
     public boolean isActive( final PwmApplication pwmApplication )
     {
     {
         if ( statDetail == null )
         if ( statDetail == null )
@@ -164,37 +148,16 @@ public enum Statistic
         return set;
         return set;
     }
     }
 
 
-    public enum Type
-    {
-        INCREMENTER,
-        AVERAGE,
-    }
-
     public String getLabel( final Locale locale )
     public String getLabel( final Locale locale )
     {
     {
-        try
-        {
-            final String keyName = Admin.STATISTICS_LABEL_PREFIX + this.getKey();
-            return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
-        }
-        catch ( MissingResourceException e )
-        {
-            return "MISSING STATISTIC LABEL for " + this.getKey();
-        }
+        final String keyName = Admin.STATISTICS_LABEL_PREFIX + this.getKey();
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
     }
     }
 
 
     public String getDescription( final Locale locale )
     public String getDescription( final Locale locale )
     {
     {
         final String keyName = Admin.STATISTICS_DESCRIPTION_PREFIX + this.getKey();
         final String keyName = Admin.STATISTICS_DESCRIPTION_PREFIX + this.getKey();
-        try
-        {
-            return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
-        }
-        catch ( Exception e )
-        {
-            LOGGER.error( "unable to load localization for " + keyName + ", error: " + e.getMessage() );
-            return "missing localization for " + keyName;
-        }
+        return LocaleHelper.getLocalizedMessage( locale, keyName, null, Admin.class );
     }
     }
 
 
     public enum EpsDuration
     public enum EpsDuration

+ 30 - 0
server/src/main/java/password/pwm/svc/stats/StatisticType.java

@@ -0,0 +1,30 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.stats;
+
+public enum StatisticType
+{
+    INCREMENTER,
+    AVERAGE,
+    EPS,
+}

+ 63 - 106
server/src/main/java/password/pwm/svc/stats/StatisticsBundle.java

@@ -22,153 +22,105 @@
 
 
 package password.pwm.svc.stats;
 package password.pwm.svc.stats;
 
 
+import password.pwm.util.java.AtomicLoopLongIncrementer;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
-import password.pwm.util.logging.PwmLogger;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.math.BigInteger;
 import java.math.BigInteger;
-import java.text.SimpleDateFormat;
-import java.util.HashMap;
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Map;
-import java.util.TimeZone;
 
 
 public class StatisticsBundle
 public class StatisticsBundle
 {
 {
+    private final Map<Statistic, AtomicLoopLongIncrementer> incrementerMap = new EnumMap<>( Statistic.class );
+    private final Map<AvgStatistic, AverageBean> avgMap = new EnumMap<>( AvgStatistic.class );
 
 
-    private static final PwmLogger LOGGER = PwmLogger.forClass( StatisticsBundle.class );
-
-    static final SimpleDateFormat STORED_DATETIME_FORMATTER = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss Z" );
-
-    static
-    {
-        STORED_DATETIME_FORMATTER.setTimeZone( TimeZone.getTimeZone( "Zulu" ) );
-    }
-
-
-    private final Map<Statistic, String> valueMap = new HashMap<>();
-
-    public StatisticsBundle( )
+    StatisticsBundle( )
     {
     {
+        for ( final Statistic statistic : Statistic.values() )
+        {
+            incrementerMap.put( statistic, new AtomicLoopLongIncrementer( 0, Long.MAX_VALUE ) );
+        }
+        for ( final AvgStatistic avgStatistic : AvgStatistic.values() )
+        {
+            avgMap.put( avgStatistic, new AverageBean() );
+        }
     }
     }
 
 
     public String output( )
     public String output( )
     {
     {
-        return JsonUtil.serializeMap( valueMap );
-    }
+        final Map<String, String> outputMap = new LinkedHashMap<>();
 
 
-    public static StatisticsBundle input( final String inputString )
-    {
-        final Map<Statistic, String> srcMap = new HashMap<>();
-        final Map<String, String> loadedMap = JsonUtil.deserializeStringMap( inputString );
-        for ( final Map.Entry<String, String> entry : loadedMap.entrySet() )
+        for ( final Statistic statistic : Statistic.values() )
         {
         {
-            final String key = entry.getKey();
-            try
+            final long currentValue = incrementerMap.get( statistic ).get();
+            if ( currentValue > 0 )
             {
             {
-                srcMap.put( Statistic.valueOf( key ), entry.getValue() );
-            }
-            catch ( IllegalArgumentException e )
-            {
-                LOGGER.error( "error parsing statistic key '" + key + "', reason: " + e.getMessage() );
+                outputMap.put( statistic.name(), Long.toString( currentValue ) );
             }
             }
         }
         }
-        final StatisticsBundle bundle = new StatisticsBundle();
-
-        for ( final Statistic loopStat : Statistic.values() )
+        for ( final AvgStatistic epsStatistic : AvgStatistic.values() )
         {
         {
-            final String value = srcMap.get( loopStat );
-            if ( !StringUtil.isEmpty( value ) )
+            final AverageBean averageBean = avgMap.get( epsStatistic );
+            if ( !averageBean.isZero() )
             {
             {
-                bundle.valueMap.put( loopStat, value );
+                outputMap.put( epsStatistic.name(), JsonUtil.serialize( averageBean ) );
             }
             }
         }
         }
 
 
-        return bundle;
+        return JsonUtil.serializeMap( outputMap );
     }
     }
 
 
-    public synchronized void incrementValue( final Statistic statistic )
+    public static StatisticsBundle input( final String inputString )
     {
     {
-        if ( Statistic.Type.INCREMENTER != statistic.getType() )
-        {
-            LOGGER.error( "attempt to increment non-counter/incremental stat " + statistic );
-            return;
-        }
+        final Map<String, String> loadedMap = JsonUtil.deserializeStringMap( inputString );
+        final StatisticsBundle bundle = new StatisticsBundle();
 
 
-        BigInteger currentValue = BigInteger.ZERO;
-        try
+        for ( final Statistic loopStat : Statistic.values() )
         {
         {
-            if ( valueMap.containsKey( statistic ) )
-            {
-                currentValue = new BigInteger( valueMap.get( statistic ) );
-            }
-            else
+            final String value = loadedMap.get( loopStat.name() );
+            if ( !StringUtil.isEmpty( value ) )
             {
             {
-                currentValue = BigInteger.ZERO;
+                final long longValue = JavaHelper.silentParseLong( value, 0 );
+                final AtomicLoopLongIncrementer incrementer = new AtomicLoopLongIncrementer( longValue, Long.MAX_VALUE );
+                bundle.incrementerMap.put( loopStat, incrementer );
             }
             }
         }
         }
-        catch ( NumberFormatException e )
-        {
-            LOGGER.error( "error reading counter/incremental stat " + statistic );
-        }
-        final BigInteger newValue = currentValue.add( BigInteger.ONE );
-        valueMap.put( statistic, newValue.toString() );
-    }
-
-    public synchronized void updateAverageValue( final Statistic statistic, final long timeDuration )
-    {
-        if ( Statistic.Type.AVERAGE != statistic.getType() )
-        {
-            LOGGER.error( "attempt to update average value of non-average stat " + statistic );
-            return;
-        }
 
 
-        final String avgStrValue = valueMap.get( statistic );
-
-        AverageBean avgBean = new AverageBean();
-        if ( avgStrValue != null && avgStrValue.length() > 0 )
+        for ( final AvgStatistic loopStat : AvgStatistic.values() )
         {
         {
-            try
-            {
-                avgBean = JsonUtil.deserialize( avgStrValue, AverageBean.class );
-            }
-            catch ( Exception e )
+            final String value = loadedMap.get( loopStat.name() );
+            if ( !StringUtil.isEmpty( value ) )
             {
             {
-                LOGGER.trace( () -> "unable to parse statistics value for stat " + statistic.toString() + ", value=" + avgStrValue );
+                final AverageBean avgBean = JsonUtil.deserialize( value, AverageBean.class );
+                bundle.avgMap.put( loopStat, avgBean );
             }
             }
         }
         }
 
 
-        avgBean.appendValue( timeDuration );
-        valueMap.put( statistic, JsonUtil.serialize( avgBean ) );
+        return bundle;
+    }
+
+    void incrementValue( final Statistic statistic )
+    {
+        incrementerMap.get( statistic ).incrementAndGet();
+    }
+
+    void updateAverageValue( final AvgStatistic statistic, final long timeDuration )
+    {
+        avgMap.get( statistic ).appendValue( timeDuration );
     }
     }
 
 
     public String getStatistic( final Statistic statistic )
     public String getStatistic( final Statistic statistic )
     {
     {
-        switch ( statistic.getType() )
-        {
-            case INCREMENTER:
-                return valueMap.containsKey( statistic ) ? valueMap.get( statistic ) : "0";
-
-            case AVERAGE:
-                final String avgStrValue = valueMap.get( statistic );
-
-                AverageBean avgBean = new AverageBean();
-                if ( avgStrValue != null && avgStrValue.length() > 0 )
-                {
-                    try
-                    {
-                        avgBean = JsonUtil.deserialize( avgStrValue, AverageBean.class );
-                    }
-                    catch ( Exception e )
-                    {
-                        LOGGER.trace( () ->  "unable to parse statistics value for stat " + statistic.toString() + ", value=" + avgStrValue );
-                    }
-                }
-                return avgBean.getAverage().toString();
-
-            default:
-                return "";
-        }
+        return Long.toString( incrementerMap.get( statistic ).get() );
+    }
+
+    public String getAvgStatistic( final AvgStatistic statistic )
+    {
+        return avgMap.get( statistic ).getAverage().toString();
     }
     }
 
 
     private static class AverageBean implements Serializable
     private static class AverageBean implements Serializable
@@ -180,7 +132,7 @@ public class StatisticsBundle
         {
         {
         }
         }
 
 
-        BigInteger getAverage( )
+        synchronized BigInteger getAverage( )
         {
         {
             if ( BigInteger.ZERO.equals( count ) )
             if ( BigInteger.ZERO.equals( count ) )
             {
             {
@@ -190,10 +142,15 @@ public class StatisticsBundle
             return total.divide( count );
             return total.divide( count );
         }
         }
 
 
-        void appendValue( final long value )
+        synchronized void appendValue( final long value )
         {
         {
             count = count.add( BigInteger.ONE );
             count = count.add( BigInteger.ONE );
             total = total.add( BigInteger.valueOf( value ) );
             total = total.add( BigInteger.valueOf( value ) );
         }
         }
+
+        synchronized boolean isZero()
+        {
+            return total.equals( BigInteger.ZERO );
+        }
     }
     }
 }
 }

+ 41 - 177
server/src/main/java/password/pwm/svc/stats/StatisticsManager.java

@@ -30,9 +30,10 @@ import password.pwm.error.PwmException;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequest;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmService;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.localdb.LocalDBException;
@@ -41,19 +42,14 @@ import password.pwm.util.logging.PwmLogger;
 import java.io.IOException;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStream;
 import java.math.BigDecimal;
 import java.math.BigDecimal;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
-import java.util.Calendar;
 import java.util.Collections;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
-import java.util.TimeZone;
 import java.util.TimerTask;
 import java.util.TimerTask;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ExecutorService;
 
 
@@ -68,7 +64,6 @@ public class StatisticsManager implements PwmService
     private static final String DB_KEY_VERSION = "STATS_VERSION";
     private static final String DB_KEY_VERSION = "STATS_VERSION";
     private static final String DB_KEY_CUMULATIVE = "CUMULATIVE";
     private static final String DB_KEY_CUMULATIVE = "CUMULATIVE";
     private static final String DB_KEY_INITIAL_DAILY_KEY = "INITIAL_DAILY_KEY";
     private static final String DB_KEY_INITIAL_DAILY_KEY = "INITIAL_DAILY_KEY";
-    private static final String DB_KEY_PREFIX_DAILY = "DAILY_";
     private static final String DB_KEY_TEMP = "TEMP_KEY";
     private static final String DB_KEY_TEMP = "TEMP_KEY";
 
 
     private static final String DB_VALUE_VERSION = "1";
     private static final String DB_VALUE_VERSION = "1";
@@ -78,15 +73,15 @@ public class StatisticsManager implements PwmService
 
 
     private LocalDB localDB;
     private LocalDB localDB;
 
 
-    private DailyKey currentDailyKey = new DailyKey( new Date() );
-    private DailyKey initialDailyKey = new DailyKey( new Date() );
+    private DailyKey currentDailyKey = DailyKey.forToday();
+    private DailyKey initialDailyKey = DailyKey.forToday();
 
 
     private ExecutorService executorService;
     private ExecutorService executorService;
 
 
     private final StatisticsBundle statsCurrent = new StatisticsBundle();
     private final StatisticsBundle statsCurrent = new StatisticsBundle();
     private StatisticsBundle statsDaily = new StatisticsBundle();
     private StatisticsBundle statsDaily = new StatisticsBundle();
     private StatisticsBundle statsCummulative = new StatisticsBundle();
     private StatisticsBundle statsCummulative = new StatisticsBundle();
-    private Map<String, EventRateMeter> epsMeterMap = new HashMap<>();
+    private Map<EpsKey, EventRateMeter> epsMeterMap = new HashMap<>();
 
 
     private PwmApplication pwmApplication;
     private PwmApplication pwmApplication;
 
 
@@ -104,16 +99,20 @@ public class StatisticsManager implements PwmService
 
 
     public StatisticsManager( )
     public StatisticsManager( )
     {
     {
+        for ( final EpsKey epsKey : EpsKey.allKeys() )
+        {
+            epsMeterMap.put( epsKey, new EventRateMeter( epsKey.getEpsDuration().getTimeDuration() ) );
+        }
     }
     }
 
 
-    public synchronized void incrementValue( final Statistic statistic )
+    public void incrementValue( final Statistic statistic )
     {
     {
         statsCurrent.incrementValue( statistic );
         statsCurrent.incrementValue( statistic );
         statsDaily.incrementValue( statistic );
         statsDaily.incrementValue( statistic );
         statsCummulative.incrementValue( statistic );
         statsCummulative.incrementValue( statistic );
     }
     }
 
 
-    public synchronized void updateAverageValue( final Statistic statistic, final long value )
+    public void updateAverageValue( final AvgStatistic statistic, final long value )
     {
     {
         statsCurrent.updateAverageValue( statistic, value );
         statsCurrent.updateAverageValue( statistic, value );
         statsDaily.updateAverageValue( statistic, value );
         statsDaily.updateAverageValue( statistic, value );
@@ -130,7 +129,7 @@ public class StatisticsManager implements PwmService
             final StatisticsBundle bundle = getStatBundleForKey( loopKey.toString() );
             final StatisticsBundle bundle = getStatBundleForKey( loopKey.toString() );
             if ( bundle != null )
             if ( bundle != null )
             {
             {
-                final String key = ( new SimpleDateFormat( "MMM dd" ) ).format( loopKey.calendar().getTime() );
+                final String key = loopKey.toString();
                 final String value = bundle.getStatistic( statistic );
                 final String value = bundle.getStatistic( statistic );
                 returnMap.put( key, value );
                 returnMap.put( key, value );
             }
             }
@@ -171,7 +170,7 @@ public class StatisticsManager implements PwmService
         {
         {
             final String storedStat = localDB.get( LocalDB.DB.PWM_STATS, key );
             final String storedStat = localDB.get( LocalDB.DB.PWM_STATS, key );
             final StatisticsBundle returnBundle;
             final StatisticsBundle returnBundle;
-            if ( storedStat != null && storedStat.length() > 0 )
+            if ( !StringUtil.isEmpty( storedStat ) )
             {
             {
                 returnBundle = StatisticsBundle.input( storedStat );
                 returnBundle = StatisticsBundle.input( storedStat );
             }
             }
@@ -192,13 +191,9 @@ public class StatisticsManager implements PwmService
 
 
     public Map<DailyKey, String> getAvailableKeys( final Locale locale )
     public Map<DailyKey, String> getAvailableKeys( final Locale locale )
     {
     {
-        final DateFormat dateFormatter = SimpleDateFormat.getDateInstance( SimpleDateFormat.DEFAULT, locale );
-        final Map<DailyKey, String> returnMap = new LinkedHashMap<DailyKey, String>();
-
-        // add current time;
-        returnMap.put( currentDailyKey, dateFormatter.format( new Date() ) );
+        final Map<DailyKey, String> returnMap = new LinkedHashMap<>();
 
 
-        // if now historical data then we're done
+        // if no historical data then we're done
         if ( currentDailyKey.equals( initialDailyKey ) )
         if ( currentDailyKey.equals( initialDailyKey ) )
         {
         {
             return returnMap;
             return returnMap;
@@ -208,8 +203,7 @@ public class StatisticsManager implements PwmService
         int safetyCounter = 0;
         int safetyCounter = 0;
         while ( !loopKey.equals( initialDailyKey ) && safetyCounter < 5000 )
         while ( !loopKey.equals( initialDailyKey ) && safetyCounter < 5000 )
         {
         {
-            final Calendar c = loopKey.calendar();
-            final String display = dateFormatter.format( c.getTime() );
+            final String display = loopKey.toString();
             returnMap.put( loopKey, display );
             returnMap.put( loopKey, display );
             loopKey = loopKey.previous();
             loopKey = loopKey.previous();
             safetyCounter++;
             safetyCounter++;
@@ -239,14 +233,6 @@ public class StatisticsManager implements PwmService
 
 
     public void init( final PwmApplication pwmApplication ) throws PwmException
     public void init( final PwmApplication pwmApplication ) throws PwmException
     {
     {
-        for ( final EpsStatistic type : EpsStatistic.values() )
-        {
-            for ( final Statistic.EpsDuration duration : Statistic.EpsDuration.values() )
-            {
-                epsMeterMap.put( type.toString() + duration.toString(), new EventRateMeter( duration.getTimeDuration() ) );
-            }
-        }
-
         status = STATUS.OPENING;
         status = STATUS.OPENING;
         this.localDB = pwmApplication.getLocalDB();
         this.localDB = pwmApplication.getLocalDB();
         this.pwmApplication = pwmApplication;
         this.pwmApplication = pwmApplication;
@@ -259,56 +245,32 @@ public class StatisticsManager implements PwmService
         }
         }
 
 
         {
         {
-            final String storedCummulativeBundleStr = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_CUMULATIVE );
-            if ( storedCummulativeBundleStr != null && storedCummulativeBundleStr.length() > 0 )
+            final String storedCumulativeBundleSir = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_CUMULATIVE );
+            if ( !StringUtil.isEmpty( storedCumulativeBundleSir ) )
             {
             {
                 try
                 try
                 {
                 {
-                    statsCummulative = StatisticsBundle.input( storedCummulativeBundleStr );
+                    statsCummulative = StatisticsBundle.input( storedCumulativeBundleSir );
                 }
                 }
                 catch ( Exception e )
                 catch ( Exception e )
                 {
                 {
-                    LOGGER.warn( "error loading saved stored statistics: " + e.getMessage() );
+                    LOGGER.warn( "error loading saved stored cumulative statistics: " + e.getMessage() );
                 }
                 }
             }
             }
         }
         }
 
 
-        {
-            for ( final EpsStatistic loopEpsType : EpsStatistic.values() )
-            {
-                for ( final EpsStatistic loopEpsDuration : EpsStatistic.values() )
-                {
-                    final String key = "EPS-" + loopEpsType.toString() + loopEpsDuration.toString();
-                    final String storedValue = localDB.get( LocalDB.DB.PWM_STATS, key );
-                    if ( storedValue != null && storedValue.length() > 0 )
-                    {
-                        try
-                        {
-                            final EventRateMeter eventRateMeter = JsonUtil.deserialize( storedValue, EventRateMeter.class );
-                            epsMeterMap.put( loopEpsType.toString() + loopEpsDuration.toString(), eventRateMeter );
-                        }
-                        catch ( Exception e )
-                        {
-                            LOGGER.error( "unexpected error reading last EPS rate for " + loopEpsType + " from LocalDB: " + e.getMessage() );
-                        }
-                    }
-                }
-            }
-
-        }
-
         {
         {
             final String storedInitialString = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_INITIAL_DAILY_KEY );
             final String storedInitialString = localDB.get( LocalDB.DB.PWM_STATS, DB_KEY_INITIAL_DAILY_KEY );
-            if ( storedInitialString != null && storedInitialString.length() > 0 )
+            if ( !StringUtil.isEmpty( storedInitialString ) )
             {
             {
                 initialDailyKey = new DailyKey( storedInitialString );
                 initialDailyKey = new DailyKey( storedInitialString );
             }
             }
         }
         }
 
 
         {
         {
-            currentDailyKey = new DailyKey( new Date() );
+            currentDailyKey = DailyKey.forToday();
             final String storedDailyStr = localDB.get( LocalDB.DB.PWM_STATS, currentDailyKey.toString() );
             final String storedDailyStr = localDB.get( LocalDB.DB.PWM_STATS, currentDailyKey.toString() );
-            if ( storedDailyStr != null && storedDailyStr.length() > 0 )
+            if ( !StringUtil.isEmpty( storedDailyStr ) )
             {
             {
                 statsDaily = StatisticsBundle.input( storedDailyStr );
                 statsDaily = StatisticsBundle.input( storedDailyStr );
             }
             }
@@ -316,7 +278,7 @@ public class StatisticsManager implements PwmService
 
 
         try
         try
         {
         {
-            localDB.put( LocalDB.DB.PWM_STATS, DB_KEY_TEMP, JavaHelper.toIsoDate( new Date() ) );
+            localDB.put( LocalDB.DB.PWM_STATS, DB_KEY_TEMP, JavaHelper.toIsoDate( Instant.now() ) );
         }
         }
         catch ( IllegalStateException e )
         catch ( IllegalStateException e )
         {
         {
@@ -340,30 +302,20 @@ public class StatisticsManager implements PwmService
 
 
     private void writeDbValues( )
     private void writeDbValues( )
     {
     {
-        if ( localDB != null )
+        if ( localDB != null && status == STATUS.OPEN )
         {
         {
             try
             try
             {
             {
-                localDB.put( LocalDB.DB.PWM_STATS, DB_KEY_CUMULATIVE, statsCummulative.output() );
-                localDB.put( LocalDB.DB.PWM_STATS, currentDailyKey.toString(), statsDaily.output() );
-
-                for ( final EpsStatistic loopEpsType : EpsStatistic.values() )
-                {
-                    for ( final Statistic.EpsDuration loopEpsDuration : Statistic.EpsDuration.values() )
-                    {
-                        final String key = "EPS-" + loopEpsType.toString();
-                        final String mapKey = loopEpsType.toString() + loopEpsDuration.toString();
-                        final String value = JsonUtil.serialize( this.epsMeterMap.get( mapKey ) );
-                        localDB.put( LocalDB.DB.PWM_STATS, key, value );
-                    }
-                }
+                final Map<String, String> dbData = new LinkedHashMap<>();
+                dbData.put( DB_KEY_CUMULATIVE, statsCummulative.output() );
+                dbData.put( currentDailyKey.toString(), statsDaily.output() );
+                localDB.putAll( LocalDB.DB.PWM_STATS, dbData );
             }
             }
             catch ( LocalDBException e )
             catch ( LocalDBException e )
             {
             {
                 LOGGER.error( "error outputting pwm statistics: " + e.getMessage() );
                 LOGGER.error( "error outputting pwm statistics: " + e.getMessage() );
             }
             }
         }
         }
-
     }
     }
 
 
     public Map<String, String> dailyStatisticsAsLabelValueMap()
     public Map<String, String> dailyStatisticsAsLabelValueMap()
@@ -381,7 +333,7 @@ public class StatisticsManager implements PwmService
 
 
     private void resetDailyStats( )
     private void resetDailyStats( )
     {
     {
-        currentDailyKey = new DailyKey( new Date() );
+        currentDailyKey = DailyKey.forToday();
         statsDaily = new StatisticsBundle();
         statsDaily = new StatisticsBundle();
         LOGGER.debug( () -> "reset daily statistics" );
         LOGGER.debug( () -> "reset daily statistics" );
     }
     }
@@ -431,102 +383,19 @@ public class StatisticsManager implements PwmService
         }
         }
     }
     }
 
 
-
-    public static class DailyKey
-    {
-        int year;
-        int day;
-
-        public DailyKey( final Date date )
-        {
-            final Calendar calendar = Calendar.getInstance( TimeZone.getTimeZone( "Zulu" ) );
-            calendar.setTime( date );
-            year = calendar.get( Calendar.YEAR );
-            day = calendar.get( Calendar.DAY_OF_YEAR );
-        }
-
-        public DailyKey( final String value )
-        {
-            final String strippedValue = value.substring( DB_KEY_PREFIX_DAILY.length() );
-            final String[] splitValue = strippedValue.split( "_" );
-            year = Integer.parseInt( splitValue[ 0 ] );
-            day = Integer.parseInt( splitValue[ 1 ] );
-        }
-
-        private DailyKey( )
-        {
-        }
-
-        @Override
-        public String toString( )
-        {
-            return DB_KEY_PREFIX_DAILY + year + "_" + day;
-        }
-
-        public DailyKey previous( )
-        {
-            final Calendar calendar = calendar();
-            calendar.add( Calendar.HOUR, -24 );
-            final DailyKey newKey = new DailyKey();
-            newKey.year = calendar.get( Calendar.YEAR );
-            newKey.day = calendar.get( Calendar.DAY_OF_YEAR );
-            return newKey;
-        }
-
-        public Calendar calendar( )
-        {
-            final Calendar calendar = Calendar.getInstance( TimeZone.getTimeZone( "Zulu" ) );
-            calendar.set( Calendar.YEAR, year );
-            calendar.set( Calendar.DAY_OF_YEAR, day );
-            return calendar;
-        }
-
-        @Override
-        public boolean equals( final Object o )
-        {
-            if ( this == o )
-            {
-                return true;
-            }
-            if ( o == null || getClass() != o.getClass() )
-            {
-                return false;
-            }
-
-            final DailyKey key = ( DailyKey ) o;
-
-            if ( day != key.day )
-            {
-                return false;
-            }
-            if ( year != key.year )
-            {
-                return false;
-            }
-
-            return true;
-        }
-
-        @Override
-        public int hashCode( )
-        {
-            int result = year;
-            result = 31 * result + day;
-            return result;
-        }
-    }
-
     public void updateEps( final EpsStatistic type, final int itemCount )
     public void updateEps( final EpsStatistic type, final int itemCount )
     {
     {
         for ( final Statistic.EpsDuration duration : Statistic.EpsDuration.values() )
         for ( final Statistic.EpsDuration duration : Statistic.EpsDuration.values() )
         {
         {
-            epsMeterMap.get( type.toString() + duration.toString() ).markEvents( itemCount );
+            final EpsKey epsKey = new EpsKey( type, duration );
+            epsMeterMap.get( epsKey ).markEvents( itemCount );
         }
         }
     }
     }
 
 
     public BigDecimal readEps( final EpsStatistic type, final Statistic.EpsDuration duration )
     public BigDecimal readEps( final EpsStatistic type, final Statistic.EpsDuration duration )
     {
     {
-        return epsMeterMap.get( type.toString() + duration.toString() ).readEventRate();
+        final EpsKey epsKey = new EpsKey( type, duration );
+        return epsMeterMap.get( epsKey ).readEventRate();
     }
     }
 
 
 
 
@@ -553,15 +422,15 @@ public class StatisticsManager implements PwmService
         }
         }
 
 
         int counter = 0;
         int counter = 0;
-        final Map<StatisticsManager.DailyKey, String> keys = statsManger.getAvailableKeys( PwmConstants.DEFAULT_LOCALE );
-        for ( final StatisticsManager.DailyKey loopKey : keys.keySet() )
+        final Map<DailyKey, String> keys = statsManger.getAvailableKeys( PwmConstants.DEFAULT_LOCALE );
+        for ( final DailyKey loopKey : keys.keySet() )
         {
         {
             counter++;
             counter++;
             final StatisticsBundle bundle = statsManger.getStatBundleForKey( loopKey.toString() );
             final StatisticsBundle bundle = statsManger.getStatBundleForKey( loopKey.toString() );
             final List<String> lineOutput = new ArrayList<>();
             final List<String> lineOutput = new ArrayList<>();
             lineOutput.add( loopKey.toString() );
             lineOutput.add( loopKey.toString() );
-            lineOutput.add( String.valueOf( loopKey.year ) );
-            lineOutput.add( String.valueOf( loopKey.day ) );
+            lineOutput.add( String.valueOf( loopKey.getYear() ) );
+            lineOutput.add( String.valueOf( loopKey.getDay() ) );
             for ( final Statistic stat : Statistic.values() )
             for ( final Statistic stat : Statistic.values() )
             {
             {
                 lineOutput.add( bundle.getStatistic( stat ) );
                 lineOutput.add( bundle.getStatistic( stat ) );
@@ -580,14 +449,9 @@ public class StatisticsManager implements PwmService
 
 
     public ServiceInfoBean serviceInfo( )
     public ServiceInfoBean serviceInfo( )
     {
     {
-        if ( status() == STATUS.OPEN )
-        {
-            return new ServiceInfoBean( Collections.singletonList( DataStorageMethod.LOCALDB ) );
-        }
-        else
-        {
-            return new ServiceInfoBean( Collections.<DataStorageMethod>emptyList() );
-        }
+        return status() == STATUS.OPEN
+                ? new ServiceInfoBean( Collections.singletonList( DataStorageMethod.LOCALDB ) )
+                : new ServiceInfoBean( Collections.emptyList() );
     }
     }
 
 
     public static void incrementStat(
     public static void incrementStat(

+ 1 - 1
server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java

@@ -215,7 +215,7 @@ public class TelemetryService implements PwmService
     private void scheduleNextJob( )
     private void scheduleNextJob( )
     {
     {
         final TimeDuration durationUntilNextPublish = durationUntilNextPublish();
         final TimeDuration durationUntilNextPublish = durationUntilNextPublish();
-        pwmApplication.getPwmScheduler().scheduleFutureJob( new PublishJob(), executorService, durationUntilNextPublish );
+        pwmApplication.getPwmScheduler().scheduleJob( new PublishJob(), executorService, durationUntilNextPublish );
         LOGGER.trace( SessionLabel.TELEMETRY_SESSION_LABEL, () -> "next publish time: " + durationUntilNextPublish().asCompactString() );
         LOGGER.trace( SessionLabel.TELEMETRY_SESSION_LABEL, () -> "next publish time: " + durationUntilNextPublish().asCompactString() );
     }
     }
 
 

+ 0 - 2
server/src/main/java/password/pwm/svc/wordlist/AbstractWordlist.java

@@ -40,7 +40,6 @@ import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmHashAlgorithm;
 
 
 import java.io.InputStream;
 import java.io.InputStream;
 import java.time.Instant;
 import java.time.Instant;
@@ -53,7 +52,6 @@ import java.util.function.BooleanSupplier;
 
 
 abstract class AbstractWordlist implements Wordlist, PwmService
 abstract class AbstractWordlist implements Wordlist, PwmService
 {
 {
-    static final PwmHashAlgorithm CHECKSUM_HASH_ALG = PwmHashAlgorithm.SHA256;
     static final TimeDuration DEBUG_OUTPUT_FREQUENCY = TimeDuration.MINUTE;
     static final TimeDuration DEBUG_OUTPUT_FREQUENCY = TimeDuration.MINUTE;
 
 
     private WordlistConfiguration wordlistConfiguration;
     private WordlistConfiguration wordlistConfiguration;

+ 2 - 2
server/src/main/java/password/pwm/svc/wordlist/WordlistSource.java

@@ -141,7 +141,7 @@ class WordlistSource
 
 
             inputStream = this.streamProvider.getInputStream();
             inputStream = this.streamProvider.getInputStream();
 
 
-            final ChecksumInputStream checksumInputStream = new ChecksumInputStream( AbstractWordlist.CHECKSUM_HASH_ALG, inputStream );
+            final ChecksumInputStream checksumInputStream = new ChecksumInputStream( inputStream );
             final CountingInputStream countingInputStream = new CountingInputStream( checksumInputStream );
             final CountingInputStream countingInputStream = new CountingInputStream( checksumInputStream );
 
 
             final ConditionalTaskExecutor debugOutputter = new ConditionalTaskExecutor(
             final ConditionalTaskExecutor debugOutputter = new ConditionalTaskExecutor(
@@ -162,7 +162,7 @@ class WordlistSource
                 return null;
                 return null;
             }
             }
 
 
-            final String hash = JavaHelper.binaryArrayToHex( checksumInputStream.closeAndFinalChecksum() );
+            final String hash = JavaHelper.binaryArrayToHex( checksumInputStream.readUntilEndAndChecksum().copyOf() );
 
 
             final WordlistSourceInfo wordlistSourceInfo = new WordlistSourceInfo(
             final WordlistSourceInfo wordlistSourceInfo = new WordlistSourceInfo(
                     hash,
                     hash,

+ 2 - 2
server/src/main/java/password/pwm/svc/wordlist/WordlistZipReader.java

@@ -57,7 +57,7 @@ class WordlistZipReader implements AutoCloseable, Closeable
 
 
     WordlistZipReader( final InputStream inputStream ) throws PwmUnrecoverableException
     WordlistZipReader( final InputStream inputStream ) throws PwmUnrecoverableException
     {
     {
-        checksumInputStream = new ChecksumInputStream( AbstractWordlist.CHECKSUM_HASH_ALG, inputStream );
+        checksumInputStream = new ChecksumInputStream( inputStream );
         countingInputStream = new CountingInputStream( checksumInputStream );
         countingInputStream = new CountingInputStream( checksumInputStream );
 
 
         zipStream = new ZipInputStream( countingInputStream );
         zipStream = new ZipInputStream( countingInputStream );
@@ -161,6 +161,6 @@ class WordlistZipReader implements AutoCloseable, Closeable
 
 
     String getChecksum()
     String getChecksum()
     {
     {
-        return JavaHelper.binaryArrayToHex( checksumInputStream.getInProgressChecksum() );
+        return JavaHelper.binaryArrayToHex( checksumInputStream.checksum().copyOf() );
     }
     }
 }
 }

+ 7 - 0
server/src/main/java/password/pwm/util/CaptchaUtility.java

@@ -331,6 +331,12 @@ public class CaptchaUtility
     private static boolean checkIfCaptchaParamPresent( final PwmRequest pwmRequest )
     private static boolean checkIfCaptchaParamPresent( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
+        if ( pwmRequest.getPwmSession().getSessionStateBean().isCaptchaBypassedViaParameter() )
+        {
+            LOGGER.trace( pwmRequest, () -> "valid skipCaptcha value previously received in session, skipping captcha check" );
+            return true;
+        }
+
         final String skipCaptcha = pwmRequest.readParameterAsString( PwmConstants.PARAM_SKIP_CAPTCHA );
         final String skipCaptcha = pwmRequest.readParameterAsString( PwmConstants.PARAM_SKIP_CAPTCHA );
         if ( skipCaptcha != null && skipCaptcha.length() > 0 )
         if ( skipCaptcha != null && skipCaptcha.length() > 0 )
         {
         {
@@ -338,6 +344,7 @@ public class CaptchaUtility
             if ( configValue != null && configValue.equals( skipCaptcha ) )
             if ( configValue != null && configValue.equals( skipCaptcha ) )
             {
             {
                 LOGGER.trace( pwmRequest, () -> "valid skipCaptcha value in request, skipping captcha check for this session" );
                 LOGGER.trace( pwmRequest, () -> "valid skipCaptcha value in request, skipping captcha check for this session" );
+                pwmRequest.getPwmSession().getSessionStateBean().setCaptchaBypassedViaParameter( true );
                 return true;
                 return true;
             }
             }
             else
             else

+ 1 - 2
server/src/main/java/password/pwm/svc/stats/EventRateMeter.java → server/src/main/java/password/pwm/util/EventRateMeter.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
  */
 
 
-package password.pwm.svc.stats;
+package password.pwm.util;
 
 
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 
 
@@ -29,7 +29,6 @@ import java.math.BigDecimal;
 
 
 public class EventRateMeter implements Serializable
 public class EventRateMeter implements Serializable
 {
 {
-
     private final TimeDuration maxDuration;
     private final TimeDuration maxDuration;
 
 
     private MovingAverage movingAverage;
     private MovingAverage movingAverage;

+ 13 - 10
server/src/main/java/password/pwm/util/PwmPasswordRuleValidator.java

@@ -57,7 +57,6 @@ import password.pwm.util.operations.PasswordUtility;
 import password.pwm.ws.client.rest.RestClientHelper;
 import password.pwm.ws.client.rest.RestClientHelper;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
@@ -77,28 +76,34 @@ public class PwmPasswordRuleValidator
     private final PwmApplication pwmApplication;
     private final PwmApplication pwmApplication;
     private final PwmPasswordPolicy policy;
     private final PwmPasswordPolicy policy;
     private final Locale locale;
     private final Locale locale;
+    private final Flag[] flags;
+
 
 
     public enum Flag
     public enum Flag
     {
     {
         FailFast,
         FailFast,
+        BypassLdapRuleCheck,
     }
     }
 
 
-    public PwmPasswordRuleValidator( final PwmApplication pwmApplication, final PwmPasswordPolicy policy )
+    public PwmPasswordRuleValidator( final PwmApplication pwmApplication, final PwmPasswordPolicy policy, final Flag... flags )
     {
     {
         this.pwmApplication = pwmApplication;
         this.pwmApplication = pwmApplication;
         this.policy = policy;
         this.policy = policy;
         this.locale = PwmConstants.DEFAULT_LOCALE;
         this.locale = PwmConstants.DEFAULT_LOCALE;
+        this.flags = flags;
     }
     }
 
 
     public PwmPasswordRuleValidator(
     public PwmPasswordRuleValidator(
             final PwmApplication pwmApplication,
             final PwmApplication pwmApplication,
             final PwmPasswordPolicy policy,
             final PwmPasswordPolicy policy,
-            final Locale locale
+            final Locale locale,
+            final Flag... flags
     )
     )
     {
     {
         this.pwmApplication = pwmApplication;
         this.pwmApplication = pwmApplication;
         this.policy = policy;
         this.policy = policy;
         this.locale = locale;
         this.locale = locale;
+        this.flags = flags;
     }
     }
 
 
     public boolean testPassword(
     public boolean testPassword(
@@ -116,7 +121,7 @@ public class PwmPasswordRuleValidator
             throw new PwmDataValidationException( errorResults.iterator().next() );
             throw new PwmDataValidationException( errorResults.iterator().next() );
         }
         }
 
 
-        if ( user != null )
+        if ( user != null && !JavaHelper.enumArrayContainsValue( flags, Flag.BypassLdapRuleCheck ) )
         {
         {
             try
             try
             {
             {
@@ -183,26 +188,24 @@ public class PwmPasswordRuleValidator
     public List<ErrorInformation> internalPwmPolicyValidator(
     public List<ErrorInformation> internalPwmPolicyValidator(
             final PasswordData password,
             final PasswordData password,
             final PasswordData oldPassword,
             final PasswordData oldPassword,
-            final UserInfo userInfo,
-            final Flag... flags
+            final UserInfo userInfo
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
         final String passwordString = password == null ? "" : password.getStringValue();
         final String passwordString = password == null ? "" : password.getStringValue();
         final String oldPasswordString = oldPassword == null ? null : oldPassword.getStringValue();
         final String oldPasswordString = oldPassword == null ? null : oldPassword.getStringValue();
-        return internalPwmPolicyValidator( passwordString, oldPasswordString, userInfo, flags );
+        return internalPwmPolicyValidator( passwordString, oldPasswordString, userInfo );
     }
     }
 
 
     @SuppressWarnings( "checkstyle:MethodLength" )
     @SuppressWarnings( "checkstyle:MethodLength" )
     public List<ErrorInformation> internalPwmPolicyValidator(
     public List<ErrorInformation> internalPwmPolicyValidator(
             final String passwordString,
             final String passwordString,
             final String oldPasswordString,
             final String oldPasswordString,
-            final UserInfo userInfo,
-            final Flag... flags
+            final UserInfo userInfo
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        final boolean failFast = flags != null && Arrays.asList( flags ).contains( Flag.FailFast );
+        final boolean failFast = JavaHelper.enumArrayContainsValue( flags, Flag.FailFast );
 
 
         // null check
         // null check
         if ( passwordString == null )
         if ( passwordString == null )

+ 1 - 1
server/src/main/java/password/pwm/util/PwmScheduler.java

@@ -90,7 +90,7 @@ public class PwmScheduler
         scheduleFixedRateJob( runnable, executorService, delayTillNextOFfiset, TimeDuration.DAY );
         scheduleFixedRateJob( runnable, executorService, delayTillNextOFfiset, TimeDuration.DAY );
     }
     }
 
 
-    public Future scheduleFutureJob(
+    public Future scheduleJob(
             final Runnable runnable,
             final Runnable runnable,
             final ExecutorService executor,
             final ExecutorService executor,
             final TimeDuration delay
             final TimeDuration delay

+ 4 - 2
server/src/main/java/password/pwm/util/RandomPasswordGenerator.java

@@ -197,7 +197,7 @@ public class RandomPasswordGenerator
         password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
         password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
 
 
         // read a rule validator
         // read a rule validator
-        final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator( pwmApplication, randomGenPolicy );
+
 
 
         // modify until it passes all the rules
         // modify until it passes all the rules
         final int maxTryCount = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_ATTEMPTS ) );
         final int maxTryCount = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_ATTEMPTS ) );
@@ -214,8 +214,9 @@ public class RandomPasswordGenerator
                 password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
                 password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
             }
             }
 
 
+            final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator( pwmApplication, randomGenPolicy, PwmPasswordRuleValidator.Flag.FailFast );
             final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(
             final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(
-                    password.toString(), null, null, PwmPasswordRuleValidator.Flag.FailFast );
+                    password.toString(), null, null );
             if ( errors != null && !errors.isEmpty() )
             if ( errors != null && !errors.isEmpty() )
             {
             {
                 validPassword = false;
                 validPassword = false;
@@ -232,6 +233,7 @@ public class RandomPasswordGenerator
         // report outcome
         // report outcome
         {
         {
             final TimeDuration td = TimeDuration.fromCurrent( startTime );
             final TimeDuration td = TimeDuration.fromCurrent( startTime );
+            final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator( pwmApplication, randomGenPolicy );
             if ( validPassword )
             if ( validPassword )
             {
             {
                 final int finalTryCount = tryCount;
                 final int finalTryCount = tryCount;

+ 1 - 1
server/src/main/java/password/pwm/util/db/DBConfiguration.java

@@ -77,7 +77,7 @@ public class DBConfiguration implements Serializable
         if ( fileValue != null && !fileValue.isEmpty() )
         if ( fileValue != null && !fileValue.isEmpty() )
         {
         {
             final FileValue.FileContent fileContent = fileValue.values().iterator().next();
             final FileValue.FileContent fileContent = fileValue.values().iterator().next();
-            jdbcDriverBytes = fileContent.getContents().getBytes();
+            jdbcDriverBytes = fileContent.getContents().copyOf();
         }
         }
         else
         else
         {
         {

+ 6 - 0
server/src/main/java/password/pwm/util/java/AtomicLoopIntIncrementer.java

@@ -33,6 +33,12 @@ public class AtomicLoopIntIncrementer
     private final int ceiling;
     private final int ceiling;
     private final int floor;
     private final int floor;
 
 
+    public AtomicLoopIntIncrementer()
+    {
+        this.ceiling = Integer.MAX_VALUE;
+        this.floor = 0;
+    }
+
     public AtomicLoopIntIncrementer( final int ceiling )
     public AtomicLoopIntIncrementer( final int ceiling )
     {
     {
         this.ceiling = ceiling;
         this.ceiling = ceiling;

+ 60 - 0
server/src/main/java/password/pwm/util/java/AtomicLoopLongIncrementer.java

@@ -0,0 +1,60 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.java;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Thread safe rotating int incrementer with configurable floor and ceiling values.
+ */
+public class AtomicLoopLongIncrementer
+{
+    private final AtomicLong incrementer;
+    private final long ceiling;
+    private final long floor;
+
+    public AtomicLoopLongIncrementer( final long initialValue, final long ceiling )
+    {
+        this.ceiling = ceiling;
+        this.floor = 0;
+        incrementer = new AtomicLong( JavaHelper.rangeCheck( floor, ceiling, initialValue ) );
+    }
+
+    public long get()
+    {
+        return incrementer.get();
+    }
+
+    public long incrementAndGet( )
+    {
+        return incrementer.getAndUpdate( operand ->
+        {
+            operand++;
+            if ( operand >= ceiling )
+            {
+                operand = floor;
+            }
+            return operand;
+        } );
+    }
+}

+ 43 - 0
server/src/main/java/password/pwm/util/java/DebugOutputBuilder.java

@@ -0,0 +1,43 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.java;
+
+import java.time.Instant;
+
+public class DebugOutputBuilder
+{
+    private final StringBuilder stringBuilder = new StringBuilder();
+
+    public void appendLine( final CharSequence charSequence )
+    {
+        stringBuilder.append( JavaHelper.toIsoDate( Instant.now() ) );
+        stringBuilder.append( " " );
+        stringBuilder.append( charSequence );
+        stringBuilder.append( "\n" );
+    }
+
+    public String toString()
+    {
+        return stringBuilder.toString();
+    }
+}

+ 109 - 82
server/src/main/java/password/pwm/util/java/FileSystemUtility.java

@@ -22,87 +22,109 @@
 
 
 package password.pwm.util.java;
 package password.pwm.util.java;
 
 
+import lombok.Value;
+import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmHashAlgorithm;
-import password.pwm.util.secure.SecureEngine;
 
 
 import java.io.File;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.IOException;
 import java.io.Serializable;
 import java.io.Serializable;
 import java.lang.reflect.Method;
 import java.lang.reflect.Method;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.nio.file.Files;
 import java.nio.file.Files;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.RecursiveTask;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.zip.CRC32;
 
 
 public class FileSystemUtility
 public class FileSystemUtility
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( FileSystemUtility.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( FileSystemUtility.class );
 
 
+    private static final AtomicLoopIntIncrementer OP_COUNTER = new AtomicLoopIntIncrementer();
+
     public static List<FileSummaryInformation> readFileInformation( final File rootFile )
     public static List<FileSummaryInformation> readFileInformation( final File rootFile )
-            throws PwmUnrecoverableException, IOException
     {
     {
-        final AtomicInteger fileCounter = new AtomicInteger();
-        final AtomicLong byteCounter = new AtomicLong();
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
-        final ConditionalTaskExecutor debugLogger = ConditionalTaskExecutor.forPeriodicTask(
-                () -> LOGGER.trace( () -> "file info reading for path " + rootFile.getAbsolutePath() + " in progress, "
-                        + fileCounter.get() + " files, "
-                        + StringUtil.formatDiskSizeforDebug( byteCounter.get() )
-                        + " bytes, scanned in " + TimeDuration.compactFromCurrent( startTime )
-                ),
-                TimeDuration.SECONDS_10
-        );
-
-        final List<FileSummaryInformation> results = readFileInformation( rootFile, "", debugLogger, fileCounter, byteCounter );
-        LOGGER.trace( () -> "completed file info reading for path " + rootFile.getAbsolutePath() + ", "
-                + fileCounter.get() + " files, "
-                + StringUtil.formatDiskSizeforDebug( byteCounter.get() )
-                + " bytes, scanned in " + TimeDuration.compactFromCurrent( startTime ) );
-        return results;
+        final int operation = OP_COUNTER.next();
+        LOGGER.trace( () -> "begin file summary load for file '" + rootFile.getAbsolutePath() + ", operation=" + operation );
+        final ForkJoinPool pool = new ForkJoinPool();
+        final RecursiveFileReaderTask task = new RecursiveFileReaderTask( rootFile );
+        final List<FileSummaryInformation> fileSummaryInformations = pool.invoke( task );
+        final AtomicLong byteCount = new AtomicLong( 0 );
+        final AtomicInteger fileCount = new AtomicInteger( 0 );
+        fileSummaryInformations.forEach( fileSummaryInformation -> byteCount.addAndGet( fileSummaryInformation.getSize() ) );
+        fileSummaryInformations.forEach( fileSummaryInformation -> fileCount.incrementAndGet() );
+        final Map<String, String> debugInfo = new LinkedHashMap<>();
+        debugInfo.put( "operation", Integer.toString( operation ) );
+        debugInfo.put( "bytes", StringUtil.formatDiskSizeforDebug( byteCount.get() ) );
+        debugInfo.put( "files", Integer.toString( fileCount.get() ) );
+        debugInfo.put( "duration", TimeDuration.compactFromCurrent( startTime ) );
+        LOGGER.trace( () -> "completed file summary load for file '" + rootFile.getAbsolutePath() + ", " + StringUtil.mapToString( debugInfo ) );
+        return fileSummaryInformations;
     }
     }
 
 
-    private static List<FileSummaryInformation> readFileInformation(
-            final File rootFile,
-            final String relativePath,
-            final ConditionalTaskExecutor debugLogger,
-            final AtomicInteger fileCounter,
-            final AtomicLong byteCounter
-    )
-            throws PwmUnrecoverableException
+    private static class RecursiveFileReaderTask extends RecursiveTask<List<FileSummaryInformation>>
     {
     {
-        final ArrayList<FileSummaryInformation> results = new ArrayList<>();
-        final File[] files = rootFile.listFiles();
-        if ( files != null )
+        private final File theFile;
+
+        RecursiveFileReaderTask( final File theFile )
+        {
+            Objects.requireNonNull( theFile );
+            this.theFile = theFile;
+        }
+
+        @Override
+        protected List<FileSummaryInformation> compute()
         {
         {
-            for ( final File loopFile : files )
+            final List<FileSummaryInformation> results = new ArrayList<>();
+
+            if ( theFile.isDirectory() )
+            {
+                final File[] subFiles = theFile.listFiles();
+                if ( subFiles != null )
+                {
+                    final List<RecursiveFileReaderTask> tasks = new ArrayList<>();
+                    for ( final File file : subFiles )
+                    {
+                        final RecursiveFileReaderTask newTask = new RecursiveFileReaderTask( file );
+                        newTask.fork();
+                        tasks.add( newTask );
+                    }
+                    tasks.forEach( recursiveFileReaderTask -> results.addAll( recursiveFileReaderTask.join() ) );
+                }
+            }
+            else
             {
             {
-                final String path = relativePath + loopFile.getName();
-                if ( loopFile.isDirectory() )
+                try
                 {
                 {
-                    final String subPath = path + File.separator;
-                    results.addAll( readFileInformation( loopFile, subPath, debugLogger, fileCounter, byteCounter ) );
+                    results.add( fileInformationForFile( theFile ) );
                 }
                 }
-                else
+                catch ( Exception e )
                 {
                 {
-                    final FileSummaryInformation fileInformation = fileInformationForFile( loopFile );
-                    results.add( fileInformation );
-                    fileCounter.incrementAndGet();
-                    byteCounter.addAndGet( fileInformation.getSize() );
+                    LOGGER.debug( () -> "error executing file summary reader: " + e.getMessage() );
                 }
                 }
-
-                debugLogger.conditionallyExecuteTask();
             }
             }
+
+            return Collections.unmodifiableList( results );
         }
         }
-        return results;
     }
     }
 
 
     private static FileSummaryInformation fileInformationForFile( final File file )
     private static FileSummaryInformation fileInformationForFile( final File file )
-            throws PwmUnrecoverableException
+            throws IOException
     {
     {
         if ( file == null || !file.exists() )
         if ( file == null || !file.exists() )
         {
         {
@@ -113,7 +135,7 @@ public class FileSystemUtility
                 file.getParentFile().getAbsolutePath(),
                 file.getParentFile().getAbsolutePath(),
                 Instant.ofEpochMilli( file.lastModified() ),
                 Instant.ofEpochMilli( file.lastModified() ),
                 file.length(),
                 file.length(),
-                SecureEngine.hash( file, PwmHashAlgorithm.SHA1 )
+                crc32( file )
         );
         );
     }
     }
 
 
@@ -222,47 +244,14 @@ public class FileSystemUtility
         }
         }
     }
     }
 
 
+    @Value
     public static class FileSummaryInformation implements Serializable
     public static class FileSummaryInformation implements Serializable
     {
     {
         private final String filename;
         private final String filename;
         private final String filepath;
         private final String filepath;
         private final Instant modified;
         private final Instant modified;
         private final long size;
         private final long size;
-        private final String sha1sum;
-
-        public FileSummaryInformation( final String filename, final String filepath, final Instant modified, final long size, final String sha1sum )
-        {
-            this.filename = filename;
-            this.filepath = filepath;
-            this.modified = modified;
-            this.size = size;
-            this.sha1sum = sha1sum;
-        }
-
-        public String getFilename( )
-        {
-            return filename;
-        }
-
-        public String getFilepath( )
-        {
-            return filepath;
-        }
-
-        public Instant getModified( )
-        {
-            return modified;
-        }
-
-        public long getSize( )
-        {
-            return size;
-        }
-
-        public String getSha1sum( )
-        {
-            return sha1sum;
-        }
+        private final long checksum;
     }
     }
 
 
     public static void deleteDirectoryContents( final File path ) throws IOException
     public static void deleteDirectoryContents( final File path ) throws IOException
@@ -270,7 +259,7 @@ public class FileSystemUtility
         deleteDirectoryContents( path, false );
         deleteDirectoryContents( path, false );
     }
     }
 
 
-    public static void deleteDirectoryContents( final File path, final boolean deleteThisLevel )
+    private static void deleteDirectoryContents( final File path, final boolean deleteThisLevel )
             throws IOException
             throws IOException
     {
     {
         if ( !path.exists() )
         if ( !path.exists() )
@@ -303,4 +292,42 @@ public class FileSystemUtility
             }
             }
         }
         }
     }
     }
+
+    private static long crc32( final File file )
+            throws IOException
+    {
+        final CRC32 crc32 = new CRC32();
+        final FileInputStream fileInputStream = new FileInputStream( file );
+        final FileChannel fileChannel = fileInputStream.getChannel();
+        final ByteBuffer byteBuffer = ByteBuffer.allocateDirect( 1024 );
+
+        while ( fileChannel.read( byteBuffer ) > 0 )
+        {
+            // redundant cast to buffer to solve jdk8/9 inter-op issue
+            ( ( Buffer ) byteBuffer ).flip();
+
+            crc32.update( byteBuffer );
+
+            // redundant cast to buffer to solve jdk8/9 inter-op issue
+            ( ( Buffer ) byteBuffer ).clear();
+        }
+
+        return crc32.getValue();
+    }
+
+    public static void mkdirs( final File file )
+            throws PwmUnrecoverableException
+    {
+        if ( !file.exists() )
+        {
+            if ( !file.mkdirs() )
+            {
+                throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "unable to create directory: " + file.getAbsolutePath() );
+            }
+        }
+        else if ( !file.isDirectory() )
+        {
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "unable to create directory, file already exists: " + file.getAbsolutePath() );
+        }
+    }
 }
 }

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

@@ -641,4 +641,16 @@ public class JavaHelper
                 && input <= Long.MAX_VALUE
                 && input <= Long.MAX_VALUE
                 && input >= Long.MIN_VALUE;
                 && input >= Long.MIN_VALUE;
     }
     }
+
+    public static byte[] longToBytes( final long input )
+    {
+        final byte[] result = new byte[Byte.SIZE];
+        long shift = input;
+        for ( int i = Byte.SIZE - 1; i >= 0; i-- )
+        {
+            result[i] = (byte) ( shift & 0xFF );
+            shift >>= Byte.SIZE;
+        }
+        return result;
+    }
 }
 }

+ 1 - 2
server/src/main/java/password/pwm/util/localdb/LocalDBFactory.java

@@ -94,8 +94,7 @@ public class LocalDBFactory
             if ( localDBUtility.readImportInprogressFlag() )
             if ( localDBUtility.readImportInprogressFlag() )
             {
             {
                 LOGGER.error( "previous database import process did not complete successfully, clearing all data" );
                 LOGGER.error( "previous database import process did not complete successfully, clearing all data" );
-                localDBUtility.prepareForImport();
-                localDBUtility.markImportComplete();
+                localDBUtility.cancelImportProcess();
             }
             }
         }
         }
 
 

+ 132 - 103
server/src/main/java/password/pwm/util/localdb/LocalDBUtility.java

@@ -24,16 +24,17 @@ package password.pwm.util.localdb;
 
 
 import org.apache.commons.csv.CSVPrinter;
 import org.apache.commons.csv.CSVPrinter;
 import org.apache.commons.csv.CSVRecord;
 import org.apache.commons.csv.CSVRecord;
-import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.input.CountingInputStream;
 import org.apache.commons.io.input.CountingInputStream;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.ProgressInfo;
 import password.pwm.util.ProgressInfo;
 import password.pwm.util.TransactionSizeCalculator;
 import password.pwm.util.TransactionSizeCalculator;
+import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
@@ -45,7 +46,7 @@ import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.OutputStream;
 import java.io.PrintStream;
 import java.io.PrintStream;
 import java.io.Reader;
 import java.io.Reader;
-import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.text.DecimalFormat;
 import java.text.DecimalFormat;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.Date;
 import java.util.Date;
@@ -62,10 +63,10 @@ public class LocalDBUtility
 {
 {
 
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( LocalDBUtility.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( LocalDBUtility.class );
+    private static final String IN_PROGRESS_STATUS_VALUE = "in-progress";
 
 
     private final LocalDB localDB;
     private final LocalDB localDB;
     private int exportLineCounter;
     private int exportLineCounter;
-    private int importLineCounter;
 
 
     private static final int GZIP_BUFFER_SIZE = 1024 * 512;
     private static final int GZIP_BUFFER_SIZE = 1024 * 512;
 
 
@@ -221,18 +222,20 @@ public class LocalDBUtility
     private void importLocalDB( final InputStream inputStream, final Appendable out, final long totalBytes )
     private void importLocalDB( final InputStream inputStream, final Appendable out, final long totalBytes )
             throws PwmOperationalException, IOException
             throws PwmOperationalException, IOException
     {
     {
-        this.prepareForImport();
 
 
-        importLineCounter = 0;
-        if ( totalBytes > 0 )
-        {
-            writeStringToOut( out, "total bytes in localdb import source: " + totalBytes );
-        }
-
-        writeStringToOut( out, "beginning localdb import..." );
+        final ImportLocalDBMachine importLocalDBMachine = new ImportLocalDBMachine( localDB, totalBytes, out );
+        importLocalDBMachine.doImport( inputStream );
+    }
 
 
-        final Instant startTime = Instant.now();
-        final TransactionSizeCalculator transactionCalculator = new TransactionSizeCalculator(
+    private static class ImportLocalDBMachine
+    {
+        private int lineReaderCounter;
+        private long byteReaderCounter;
+        private int recordImportCounter;
+        private final Instant startTime = Instant.now();
+        final Map<LocalDB.DB, Map<String, String>> transactionMap = new HashMap<>();
+        private final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
+        private final TransactionSizeCalculator transactionCalculator = new TransactionSizeCalculator(
                 TransactionSizeCalculator.Settings.builder()
                 TransactionSizeCalculator.Settings.builder()
                         .durationGoal( TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS ) )
                         .durationGoal( TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS ) )
                         .minTransactions( 50 )
                         .minTransactions( 50 )
@@ -240,96 +243,139 @@ public class LocalDBUtility
                         .build()
                         .build()
         );
         );
 
 
-        final Map<LocalDB.DB, Map<String, String>> transactionMap = new HashMap<>();
-        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
-        {
-            transactionMap.put( loopDB, new TreeMap<>() );
-        }
+        private final long totalBytes;
+        private final Appendable debugOutput;
+        private final LocalDB localDB;
 
 
-        final CountingInputStream countingInputStream = new CountingInputStream( inputStream );
-        final EventRateMeter eventRateMeter = new EventRateMeter( TimeDuration.MINUTE );
+        private final ConditionalTaskExecutor debugOutputWriter;
 
 
-        final Timer statTimer = new Timer( true );
-        statTimer.scheduleAtFixedRate( new TimerTask()
+        ImportLocalDBMachine( final LocalDB localDB, final long totalBytes, final Appendable debugOutput )
         {
         {
-            @Override
-            public void run( )
+            this.localDB = localDB;
+            this.totalBytes = totalBytes;
+            this.debugOutput = debugOutput;
+
+            for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
             {
             {
-                String output = "";
-                if ( totalBytes > 0 )
-                {
-                    final ProgressInfo progressInfo = new ProgressInfo( startTime, totalBytes, countingInputStream.getByteCount() );
-                    output += progressInfo.debugOutput();
-                }
-                else
-                {
-                    output += "recordsImported=" + importLineCounter;
-                }
-                output += ", avgTransactionSize=" + transactionCalculator.getTransactionSize()
-                        + ", recordsPerMinute=" + eventRateMeter.readEventRate().setScale( 2, BigDecimal.ROUND_DOWN );
-                writeStringToOut( out, output );
+                transactionMap.put( loopDB, new TreeMap<>() );
             }
             }
-        }, 30 * 1000, 30 * 1000 );
 
 
+            this.debugOutputWriter = ConditionalTaskExecutor.forPeriodicTask( () ->
+            {
+                writeStringToOut( debugOutput, debugStatsString() );
+            }, TimeDuration.of( 30, TimeDuration.Unit.SECONDS ) );
+        }
 
 
-        Reader csvReader = null;
-        try
+        void doImport( final InputStream inputStream )
+                throws IOException, LocalDBException
         {
         {
-            csvReader = new InputStreamReader( new GZIPInputStream( countingInputStream, GZIP_BUFFER_SIZE ), PwmConstants.DEFAULT_CHARSET );
-            for ( final CSVRecord record : PwmConstants.DEFAULT_CSV_FORMAT.parse( csvReader ) )
+            this.prepareForImport();
+
+            if ( totalBytes > 0 )
             {
             {
-                importLineCounter++;
-                eventRateMeter.markEvents( 1 );
-                final String dbNameRecordStr = record.get( 0 );
-                final LocalDB.DB db = JavaHelper.readEnumFromString( LocalDB.DB.class, null, dbNameRecordStr );
-                final String key = record.get( 1 );
-                final String value = record.get( 2 );
-                if ( db == null )
-                {
-                    writeStringToOut( out, "ignoring localdb import record #" + importLineCounter + ", invalid DB name '" + dbNameRecordStr + "'" );
-                }
-                else
+                writeStringToOut( debugOutput, "total bytes in localdb import source: " + totalBytes );
+            }
+
+            writeStringToOut( debugOutput, "beginning localdb import..." );
+
+            try ( CountingInputStream countingInputStream = new CountingInputStream( inputStream ) )
+            {
+                try ( Reader csvReader = new InputStreamReader( new GZIPInputStream( countingInputStream, GZIP_BUFFER_SIZE ), PwmConstants.DEFAULT_CHARSET ) )
                 {
                 {
-                    transactionMap.get( db ).put( key, value );
                     int cachedTransactions = 0;
                     int cachedTransactions = 0;
-                    for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
-                    {
-                        cachedTransactions += transactionMap.get( loopDB ).size();
-                    }
-                    if ( cachedTransactions >= transactionCalculator.getTransactionSize() )
+                    for ( final CSVRecord record : PwmConstants.DEFAULT_CSV_FORMAT.parse( csvReader ) )
                     {
                     {
-                        final long startTxnTime = System.currentTimeMillis();
-                        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+                        lineReaderCounter++;
+                        eventRateMeter.markEvents( 1 );
+                        byteReaderCounter = countingInputStream.getByteCount();
+                        final String dbNameRecordStr = record.get( 0 );
+                        final LocalDB.DB db = JavaHelper.readEnumFromString( LocalDB.DB.class, null, dbNameRecordStr );
+                        final String key = record.get( 1 );
+                        final String value = record.get( 2 );
+                        if ( db == null )
                         {
                         {
-                            localDB.putAll( loopDB, transactionMap.get( loopDB ) );
-                            transactionMap.get( loopDB ).clear();
+                            writeStringToOut( debugOutput, "ignoring localdb import record #" + lineReaderCounter + ", invalid DB name '" + dbNameRecordStr + "'" );
                         }
                         }
-                        transactionCalculator.recordLastTransactionDuration( TimeDuration.fromCurrent( startTxnTime ) );
+                        else
+                        {
+                            transactionMap.get( db ).put( key, value );
+                            cachedTransactions++;
+                            if ( cachedTransactions >= transactionCalculator.getTransactionSize() )
+                            {
+                                flushCachedTransactions();
+                                cachedTransactions = 0;
+                            }
+                        }
+                        debugOutputWriter.conditionallyExecuteTask();
                     }
                     }
                 }
                 }
             }
             }
+
+            flushCachedTransactions();
+            this.markImportComplete();
+
+            final String completeMsg = "import process completed: " + debugStatsString();
+            LOGGER.info( () -> completeMsg );
+            writeStringToOut( debugOutput, completeMsg );
+        }
+
+        private void flushCachedTransactions( )
+                throws LocalDBException
+        {
+            final Instant startTxnTime = Instant.now();
+            for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+            {
+                localDB.putAll( loopDB, transactionMap.get( loopDB ) );
+                recordImportCounter += transactionMap.get( loopDB ).size();
+                transactionMap.get( loopDB ).clear();
+            }
+            transactionCalculator.recordLastTransactionDuration( TimeDuration.fromCurrent( startTxnTime ) );
         }
         }
-        finally
+
+        private void prepareForImport( )
+                throws LocalDBException
         {
         {
-            LOGGER.trace( () -> "import process completed" );
-            statTimer.cancel();
-            IOUtils.closeQuietly( csvReader );
-            IOUtils.closeQuietly( countingInputStream );
+            LOGGER.info( () -> "preparing LocalDB for import procedure" );
+            localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), IN_PROGRESS_STATUS_VALUE );
+            for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+            {
+                if ( loopDB != LocalDB.DB.PWM_META )
+                {
+                    localDB.truncate( loopDB );
+                }
+            }
+
+            // save meta for last so flag is cleared last.
+            localDB.truncate( LocalDB.DB.PWM_META );
+            localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), IN_PROGRESS_STATUS_VALUE  );
         }
         }
 
 
-        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
+        private void markImportComplete()
+                throws LocalDBException
         {
         {
-            localDB.putAll( loopDB, transactionMap.get( loopDB ) );
-            transactionMap.get( loopDB ).clear();
+            LOGGER.info( () -> "marking LocalDB import procedure completed" );
+            localDB.remove( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey() );
         }
         }
 
 
-        this.markImportComplete();
+        private String debugStatsString()
+        {
+            final Map<String, String> stats = new LinkedHashMap<>();
+            if ( totalBytes > 0 && byteReaderCounter > 0 )
+            {
+                final ProgressInfo progressInfo = new ProgressInfo( startTime, totalBytes, byteReaderCounter );
+                stats.put( "progress", progressInfo.debugOutput() );
+            }
 
 
-        writeStringToOut( out, "restore complete, restored " + importLineCounter + " records in " + TimeDuration.fromCurrent( startTime ).asLongString() );
-        statTimer.cancel();
+            stats.put( "linesRead", Integer.toString( lineReaderCounter ) );
+            stats.put( "bytesRead", Long.toString( byteReaderCounter ) );
+            stats.put( "recordsImported", Integer.toString( recordImportCounter ) );
+            stats.put( "avgTransactionSize", Integer.toString( transactionCalculator.getTransactionSize() ) );
+            stats.put( "recordsPerMinute", eventRateMeter.readEventRate().setScale( 2, RoundingMode.DOWN ).toString() );
+            stats.put( "duration", TimeDuration.compactFromCurrent( startTime ) );
+            return StringUtil.mapToString( stats );
+        }
     }
     }
 
 
-
     public static Map<StatsKey, Object> dbStats(
     public static Map<StatsKey, Object> dbStats(
             final LocalDB localDB,
             final LocalDB localDB,
             final LocalDB.DB db
             final LocalDB.DB db
@@ -381,35 +427,10 @@ public class LocalDBUtility
         AVG_VALUE_LENGTH,
         AVG_VALUE_LENGTH,
     }
     }
 
 
-    public void prepareForImport( )
-            throws LocalDBException
-    {
-        LOGGER.info( () -> "preparing LocalDB for import procedure" );
-        localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), "inprogress" );
-        for ( final LocalDB.DB loopDB : LocalDB.DB.values() )
-        {
-            if ( loopDB != LocalDB.DB.PWM_META )
-            {
-                localDB.truncate( loopDB );
-            }
-        }
-
-        // save meta for last so flag is cleared last.
-        localDB.truncate( LocalDB.DB.PWM_META );
-        localDB.put( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey(), "inprogress" );
-    }
-
-    public void markImportComplete( )
-            throws LocalDBException
-    {
-        LOGGER.info( () -> "marking LocalDB import procedure completed" );
-        localDB.remove( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey() );
-    }
-
     public boolean readImportInprogressFlag( )
     public boolean readImportInprogressFlag( )
             throws LocalDBException
             throws LocalDBException
     {
     {
-        return "inprogress".equals(
+        return IN_PROGRESS_STATUS_VALUE.equals(
                 localDB.get( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey() ) );
                 localDB.get( LocalDB.DB.PWM_META, PwmApplication.AppAttribute.LOCALDB_IMPORT_STATUS.getKey() ) );
     }
     }
 
 
@@ -417,4 +438,12 @@ public class LocalDBUtility
     {
     {
         return parameters != null && parameters.containsKey( parameter ) && Boolean.parseBoolean( parameters.get( parameter ) );
         return parameters != null && parameters.containsKey( parameter ) && Boolean.parseBoolean( parameters.get( parameter ) );
     }
     }
+
+    public void cancelImportProcess()
+            throws LocalDBException
+    {
+        final ImportLocalDBMachine importLocalDBMachine = new ImportLocalDBMachine( localDB, 0, new StringBuilder() );
+        importLocalDBMachine.prepareForImport();
+        importLocalDBMachine.markImportComplete();
+    }
 }
 }

+ 3 - 3
server/src/main/java/password/pwm/util/localdb/WorkQueueProcessor.java

@@ -29,7 +29,7 @@ import password.pwm.PwmApplication;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
@@ -72,12 +72,12 @@ public final class WorkQueueProcessor<W extends Serializable>
 
 
     private volatile WorkerThread workerThread;
     private volatile WorkerThread workerThread;
 
 
-    private final AtomicLoopIntIncrementer idGenerator = new AtomicLoopIntIncrementer( 0 );
+    private final AtomicLoopIntIncrementer idGenerator = new AtomicLoopIntIncrementer();
     private Instant eldestItem = null;
     private Instant eldestItem = null;
 
 
     private ThreadPoolExecutor executorService;
     private ThreadPoolExecutor executorService;
 
 
-    private final EventRateMeter.MovingAverage avgLagTime = new EventRateMeter.MovingAverage( 60 * 60 * 1000 );
+    private final EventRateMeter.MovingAverage avgLagTime = new EventRateMeter.MovingAverage( TimeDuration.HOUR );
     private final EventRateMeter sendRate = new EventRateMeter( TimeDuration.HOUR );
     private final EventRateMeter sendRate = new EventRateMeter( TimeDuration.HOUR );
 
 
     private final AtomicInteger preQueueSubmit = new AtomicInteger( 0 );
     private final AtomicInteger preQueueSubmit = new AtomicInteger( 0 );

+ 4 - 2
server/src/main/java/password/pwm/util/operations/PasswordUtility.java

@@ -76,6 +76,7 @@ import password.pwm.svc.cache.CacheService;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.HelpdeskAuditRecord;
 import password.pwm.svc.event.HelpdeskAuditRecord;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.util.PasswordCharCounter;
 import password.pwm.util.PasswordCharCounter;
@@ -395,7 +396,8 @@ public class PasswordUtility
 
 
             final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator(
             final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator(
                     pwmApplication,
                     pwmApplication,
-                    passwordPolicy
+                    passwordPolicy,
+                    PwmPasswordRuleValidator.Flag.BypassLdapRuleCheck
             );
             );
 
 
             pwmPasswordRuleValidator.testPassword( newPassword, null, userInfo, theUser );
             pwmPasswordRuleValidator.testPassword( newPassword, null, userInfo, theUser );
@@ -465,7 +467,7 @@ public class PasswordUtility
         pwmApplication.getStatisticsManager().updateEps( EpsStatistic.PASSWORD_CHANGES, 1 );
         pwmApplication.getStatisticsManager().updateEps( EpsStatistic.PASSWORD_CHANGES, 1 );
 
 
         final int passwordStrength = PasswordUtility.judgePasswordStrength( pwmApplication.getConfig(), newPassword.getStringValue() );
         final int passwordStrength = PasswordUtility.judgePasswordStrength( pwmApplication.getConfig(), newPassword.getStringValue() );
-        pwmApplication.getStatisticsManager().updateAverageValue( Statistic.AVG_PASSWORD_STRENGTH, passwordStrength );
+        pwmApplication.getStatisticsManager().updateAverageValue( AvgStatistic.AVG_PASSWORD_STRENGTH, passwordStrength );
 
 
         // at this point the password has been changed, so log it.
         // at this point the password has been changed, so log it.
         final String msg = ( bindIsSelf
         final String msg = ( bindIsSelf

+ 1 - 1
server/src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java

@@ -116,7 +116,7 @@ public class NMASCrOperator implements CrOperator
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( NMASCrOperator.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( NMASCrOperator.class );
 
 
-    private final AtomicLoopIntIncrementer threadCounter = new AtomicLoopIntIncrementer( Integer.MAX_VALUE );
+    private final AtomicLoopIntIncrementer threadCounter = new AtomicLoopIntIncrementer();
     private final List<NMASSessionThread> sessionMonitorThreads = Collections.synchronizedList( new ArrayList<NMASSessionThread>() );
     private final List<NMASSessionThread> sessionMonitorThreads = Collections.synchronizedList( new ArrayList<NMASSessionThread>() );
     private final PwmApplication pwmApplication;
     private final PwmApplication pwmApplication;
     private final TimeDuration maxThreadIdleTime;
     private final TimeDuration maxThreadIdleTime;

+ 12 - 25
server/src/main/java/password/pwm/util/secure/ChecksumInputStream.java

@@ -22,34 +22,21 @@
 
 
 package password.pwm.util.secure;
 package password.pwm.util.secure;
 
 
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.bean.ImmutableByteArray;
+import password.pwm.util.java.JavaHelper;
 
 
 import java.io.IOException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
+import java.util.zip.CRC32;
 
 
 public class ChecksumInputStream extends InputStream
 public class ChecksumInputStream extends InputStream
 {
 {
-    private final MessageDigest messageDigest;
+    private final CRC32 crc32 = new CRC32();
     private final InputStream wrappedStream;
     private final InputStream wrappedStream;
 
 
-    public ChecksumInputStream( final PwmHashAlgorithm hash, final InputStream wrappedStream ) throws PwmUnrecoverableException
+    public ChecksumInputStream( final InputStream wrappedStream )
     {
     {
         this.wrappedStream = wrappedStream;
         this.wrappedStream = wrappedStream;
-
-        try
-        {
-            messageDigest = MessageDigest.getInstance( hash.getAlgName() );
-        }
-        catch ( NoSuchAlgorithmException e )
-        {
-            final String errorMsg = "missing hash algorithm: " + e.getMessage();
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CRYPT_ERROR, errorMsg );
-            throw new PwmUnrecoverableException( errorInformation );
-        }
     }
     }
 
 
     @Override
     @Override
@@ -58,7 +45,7 @@ public class ChecksumInputStream extends InputStream
         final int value = wrappedStream.read();
         final int value = wrappedStream.read();
         if ( value >= 0 )
         if ( value >= 0 )
         {
         {
-            messageDigest.update( ( byte ) value );
+            crc32.update( ( byte ) value );
         }
         }
         return value;
         return value;
     }
     }
@@ -69,7 +56,7 @@ public class ChecksumInputStream extends InputStream
         final int length = wrappedStream.read( b );
         final int length = wrappedStream.read( b );
         if ( length > 0 )
         if ( length > 0 )
         {
         {
-            messageDigest.update( b, 0, length );
+            crc32.update( b, 0, length );
         }
         }
         return length;
         return length;
     }
     }
@@ -80,7 +67,7 @@ public class ChecksumInputStream extends InputStream
         final int length = wrappedStream.read( b, off, len );
         final int length = wrappedStream.read( b, off, len );
         if ( length > 0 )
         if ( length > 0 )
         {
         {
-            messageDigest.update( b, off, length );
+            crc32.update( b, off, length );
         }
         }
         return length;
         return length;
     }
     }
@@ -121,12 +108,12 @@ public class ChecksumInputStream extends InputStream
         return false;
         return false;
     }
     }
 
 
-    public byte[] getInProgressChecksum( )
+    public ImmutableByteArray checksum( )
     {
     {
-        return messageDigest.digest();
+        return ImmutableByteArray.of( JavaHelper.longToBytes( crc32.getValue() ) );
     }
     }
 
 
-    public byte[] closeAndFinalChecksum( ) throws IOException
+    public ImmutableByteArray readUntilEndAndChecksum( ) throws IOException
     {
     {
         final byte[] buffer = new byte[ 1024 ];
         final byte[] buffer = new byte[ 1024 ];
 
 
@@ -135,6 +122,6 @@ public class ChecksumInputStream extends InputStream
             // read out the remainder of the stream contents
             // read out the remainder of the stream contents
         }
         }
 
 
-        return getInProgressChecksum();
+        return checksum();
     }
     }
 }
 }

+ 10 - 23
server/src/main/java/password/pwm/util/secure/ChecksumOutputStream.java

@@ -22,34 +22,21 @@
 
 
 package password.pwm.util.secure;
 package password.pwm.util.secure;
 
 
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.bean.ImmutableByteArray;
+import password.pwm.util.java.JavaHelper;
 
 
 import java.io.IOException;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
+import java.util.zip.CRC32;
 
 
 public class ChecksumOutputStream extends OutputStream
 public class ChecksumOutputStream extends OutputStream
 {
 {
-    private final MessageDigest messageDigest;
+    private final CRC32 crc32 = new CRC32();
     private final OutputStream wrappedStream;
     private final OutputStream wrappedStream;
 
 
-    public ChecksumOutputStream( final PwmHashAlgorithm hash, final OutputStream wrappedStream ) throws PwmUnrecoverableException
+    public ChecksumOutputStream( final OutputStream wrappedStream )
     {
     {
         this.wrappedStream = wrappedStream;
         this.wrappedStream = wrappedStream;
-
-        try
-        {
-            messageDigest = MessageDigest.getInstance( hash.getAlgName() );
-        }
-        catch ( NoSuchAlgorithmException e )
-        {
-            final String errorMsg = "missing hash algorithm: " + e.getMessage();
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CRYPT_ERROR, errorMsg );
-            throw new PwmUnrecoverableException( errorInformation );
-        }
     }
     }
 
 
     @Override
     @Override
@@ -61,7 +48,7 @@ public class ChecksumOutputStream extends OutputStream
     @Override
     @Override
     public void write( final byte[] b ) throws IOException
     public void write( final byte[] b ) throws IOException
     {
     {
-        messageDigest.update( b );
+        crc32.update( b );
         wrappedStream.write( b );
         wrappedStream.write( b );
     }
     }
 
 
@@ -70,7 +57,7 @@ public class ChecksumOutputStream extends OutputStream
     {
     {
         if ( len > 0 )
         if ( len > 0 )
         {
         {
-            messageDigest.update( b, off, len );
+            crc32.update( b, off, len );
         }
         }
 
 
         wrappedStream.write( b, off, len );
         wrappedStream.write( b, off, len );
@@ -85,12 +72,12 @@ public class ChecksumOutputStream extends OutputStream
     @Override
     @Override
     public void write( final int b ) throws IOException
     public void write( final int b ) throws IOException
     {
     {
-        messageDigest.update( ( byte ) b );
+        crc32.update( ( byte ) b );
         wrappedStream.write( b );
         wrappedStream.write( b );
     }
     }
 
 
-    public byte[] getInProgressChecksum( )
+    public ImmutableByteArray checksum( )
     {
     {
-        return messageDigest.digest();
+        return ImmutableByteArray.of( JavaHelper.longToBytes( crc32.getValue() ) );
     }
     }
 }
 }

+ 1 - 1
server/src/main/java/password/pwm/util/secure/X509Utils.java

@@ -302,7 +302,7 @@ public abstract class X509Utils
                 {
                 {
                     try
                     try
                     {
                     {
-                        LOGGER.warn( sessionLabel, "blind trusting certificate during authType=" + authType + ", subject=" + cert.getSubjectDN().toString() );
+                        LOGGER.debug( sessionLabel, () -> "promiscuous trusting certificate during authType=" + authType + ", subject=" + cert.getSubjectDN().toString() );
                     }
                     }
                     catch ( Exception e )
                     catch ( Exception e )
                     {
                     {

+ 1 - 1
server/src/main/java/password/pwm/ws/server/RestServlet.java

@@ -67,7 +67,7 @@ import java.util.Locale;
 
 
 public abstract class RestServlet extends HttpServlet
 public abstract class RestServlet extends HttpServlet
 {
 {
-    private static final AtomicLoopIntIncrementer REQUEST_COUNTER = new AtomicLoopIntIncrementer( Integer.MAX_VALUE );
+    private static final AtomicLoopIntIncrementer REQUEST_COUNTER = new AtomicLoopIntIncrementer();
 
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( RestServlet.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( RestServlet.class );
 
 

+ 19 - 9
server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java

@@ -34,8 +34,11 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpContentType;
 import password.pwm.http.HttpContentType;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.PwmHttpRequestWrapper;
 import password.pwm.http.PwmHttpRequestWrapper;
+import password.pwm.svc.stats.AvgStatistic;
+import password.pwm.svc.stats.DailyKey;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticType;
 import password.pwm.svc.stats.StatisticsBundle;
 import password.pwm.svc.stats.StatisticsBundle;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
@@ -54,9 +57,7 @@ import java.math.RoundingMode;
 import java.time.ZoneOffset;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.ArrayList;
-import java.util.Calendar;
 import java.util.Collections;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
@@ -194,7 +195,7 @@ public class RestStatisticsServer extends RestServlet
         {
         {
             final List<HistoryData> outerOutput = new ArrayList<>();
             final List<HistoryData> outerOutput = new ArrayList<>();
 
 
-            StatisticsManager.DailyKey dailyKey = new StatisticsManager.DailyKey( new Date() );
+            DailyKey dailyKey = DailyKey.forToday();
 
 
             for ( int daysAgo = 0; daysAgo < days; daysAgo++ )
             for ( int daysAgo = 0; daysAgo < days; daysAgo++ )
             {
             {
@@ -210,10 +211,10 @@ public class RestStatisticsServer extends RestServlet
                 final HistoryData historyData = HistoryData.builder()
                 final HistoryData historyData = HistoryData.builder()
                         .name( dailyKey.toString() )
                         .name( dailyKey.toString() )
                         .date( DateTimeFormatter.ofPattern( "yyyy-MM-dd" ).withZone( ZoneOffset.UTC )
                         .date( DateTimeFormatter.ofPattern( "yyyy-MM-dd" ).withZone( ZoneOffset.UTC )
-                                .format( dailyKey.calendar().toInstant() ) )
-                        .year( dailyKey.calendar().get( Calendar.YEAR ) )
-                        .month( dailyKey.calendar().get( Calendar.MONTH ) )
-                        .day( dailyKey.calendar().get( Calendar.DAY_OF_MONTH ) )
+                                .format( dailyKey.localDate() ) )
+                        .year( dailyKey.localDate().getYear() )
+                        .month( dailyKey.localDate().getMonthValue() )
+                        .day( dailyKey.localDate().getDayOfMonth() )
                         .daysAgo( daysAgo )
                         .daysAgo( daysAgo )
                         .data( statValues )
                         .data( statValues )
                         .build();
                         .build();
@@ -250,7 +251,16 @@ public class RestStatisticsServer extends RestServlet
                 final StatLabelData statLabelData = new StatLabelData(
                 final StatLabelData statLabelData = new StatLabelData(
                         statistic.name(),
                         statistic.name(),
                         statistic.getLabel( locale ),
                         statistic.getLabel( locale ),
-                        statistic.getType().name(),
+                        StatisticType.INCREMENTER.name(),
+                        statistic.getDescription( locale ) );
+                output.put( statistic.name(), statLabelData );
+            }
+            for ( final AvgStatistic statistic : AvgStatistic.values() )
+            {
+                final StatLabelData statLabelData = new StatLabelData(
+                        statistic.name(),
+                        statistic.getLabel( locale ),
+                        StatisticType.AVERAGE.name(),
                         statistic.getDescription( locale ) );
                         statistic.getDescription( locale ) );
                 output.put( statistic.name(), statLabelData );
                 output.put( statistic.name(), statLabelData );
             }
             }
@@ -262,7 +272,7 @@ public class RestStatisticsServer extends RestServlet
                     final StatLabelData statLabelData = new StatLabelData(
                     final StatLabelData statLabelData = new StatLabelData(
                             name,
                             name,
                             loopEps.getLabel( locale ),
                             loopEps.getLabel( locale ),
-                            "EPS",
+                            StatisticType.EPS.name(),
                             null );
                             null );
                     output.put( name, statLabelData );
                     output.put( name, statLabelData );
                 }
                 }

+ 4 - 2
server/src/main/resources/password/pwm/AppProperty.properties

@@ -76,8 +76,8 @@ configEditor.blockOldIE=true
 configEditor.queryFilter.testLimit=5000
 configEditor.queryFilter.testLimit=5000
 configEditor.idleTimeoutSeconds=900
 configEditor.idleTimeoutSeconds=900
 configGuide.idleTimeoutSeconds=3600
 configGuide.idleTimeoutSeconds=3600
-configManager.zipDebug.maxLogLines=100000
-configManager.zipDebug.maxLogSeconds=30
+configManager.zipDebug.maxLogBytes=50000000
+configManager.zipDebug.maxLogSeconds=120
 db.jdbcLoadStrategy=AppPathFileLoader,Classpath
 db.jdbcLoadStrategy=AppPathFileLoader,Classpath
 db.connections.max=5
 db.connections.max=5
 db.connections.timeoutMs=30000
 db.connections.timeoutMs=30000
@@ -98,6 +98,8 @@ healthCheck.nominalCheckIntervalSeconds=60
 healthCheck.minimumCheckIntervalSeconds=10
 healthCheck.minimumCheckIntervalSeconds=10
 healthCheck.maximumRecordAgeSeconds=300
 healthCheck.maximumRecordAgeSeconds=300
 healthCheck.maximumForceCheckWaitSeconds=30
 healthCheck.maximumForceCheckWaitSeconds=30
+health.supportBundle.file.writeIntervalSeconds=0
+health.supportBundle.file.writeRetentionCount=10
 health.certificate.warnSeconds=2592000
 health.certificate.warnSeconds=2592000
 health.ldap.cautionDurationMS=10800000
 health.ldap.cautionDurationMS=10800000
 health.ldap.proxy.pwExpireWarnSeconds=2592000
 health.ldap.proxy.pwExpireWarnSeconds=2592000

+ 3 - 0
server/src/main/resources/password/pwm/i18n/Admin.properties

@@ -222,6 +222,8 @@ Statistic_Label.AvgPasswordSyncTime=Average Password Sync Time
 Statistic_Description.AvgPasswordSyncTime=Average time (in milliseconds) users spend waiting for the password sync progress to complete.
 Statistic_Description.AvgPasswordSyncTime=Average time (in milliseconds) users spend waiting for the password sync progress to complete.
 Statistic_Label.AvgAuthenticationTime=Average Authentication Time
 Statistic_Label.AvgAuthenticationTime=Average Authentication Time
 Statistic_Description.AvgAuthenticationTime=Average time (in milliseconds) for authentications of all types to complete.
 Statistic_Description.AvgAuthenticationTime=Average time (in milliseconds) for authentications of all types to complete.
+Statistic_Label.AvgRequestProcessTime=Average Request Process Time
+Statistic_Description.AvgRequestProcessTime=Average time (in milliseconds) for page requests (not including resources) to process.
 Statistic_Label.RecoveryTokensSent=Forgotten Password Tokens Sent
 Statistic_Label.RecoveryTokensSent=Forgotten Password Tokens Sent
 Statistic_Description.RecoveryTokensSent=Number of tokens used for forgotten password process issued and sent via email or SMS.
 Statistic_Description.RecoveryTokensSent=Number of tokens used for forgotten password process issued and sent via email or SMS.
 Statistic_Label.RecoveryTokensPassed=Forgotten Password Tokens Passed
 Statistic_Label.RecoveryTokensPassed=Forgotten Password Tokens Passed
@@ -296,6 +298,7 @@ Statistic_Label.ObsoleteUrlRequests=Obsolete URL Requests
 Statistic_Description.ObsoleteUrlRequests=Number of web requests to obsolete URLs.
 Statistic_Description.ObsoleteUrlRequests=Number of web requests to obsolete URLs.
 Statistic_Label.SyslogMessagesSent=Syslog Messages Sent
 Statistic_Label.SyslogMessagesSent=Syslog Messages Sent
 Statistic_Description.SyslogMessagesSent=Number of successfully sent syslog messages.
 Statistic_Description.SyslogMessagesSent=Number of successfully sent syslog messages.
+EpsStatistic_Label.LDAP_BINDS=LDAP Binds
 EpsStatistic_Label.REQUESTS=Requests
 EpsStatistic_Label.REQUESTS=Requests
 EpsStatistic_Label.SESSIONS=Sessions
 EpsStatistic_Label.SESSIONS=Sessions
 EpsStatistic_Label.PASSWORD_CHANGES=Password Changes
 EpsStatistic_Label.PASSWORD_CHANGES=Password Changes

+ 1 - 0
server/src/main/resources/password/pwm/i18n/Display.properties

@@ -355,6 +355,7 @@ Title_TitleBarAuthenticated=@User:ID@  Password Self Service
 Title_TitleBar=Password Self Service
 Title_TitleBar=Password Self Service
 Title_UpdateProfile=Update Profile
 Title_UpdateProfile=Update Profile
 Title_UpdateProfileConfirm=Confirm Profile Data
 Title_UpdateProfileConfirm=Confirm Profile Data
+Title_Upload=Upload
 Title_UploadPhoto=Upload Photo
 Title_UploadPhoto=Upload Photo
 Title_UserData=My Data
 Title_UserData=My Data
 Title_UserEventHistory=Password History
 Title_UserEventHistory=Password History

+ 1 - 1
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -714,7 +714,7 @@ Setting_Description_token.characters=Specify the available characters for the em
 Setting_Description_token.ldap.attribute=Specify the attribute that @PwmAppName@ uses when you enable the LDAP Token Storage Method to store and search for tokens.
 Setting_Description_token.ldap.attribute=Specify the attribute that @PwmAppName@ uses when you enable the LDAP Token Storage Method to store and search for tokens.
 Setting_Description_token.length=Specify the length of the email token
 Setting_Description_token.length=Specify the length of the email token
 Setting_Description_token.lifetime=Specify the default lifetime an token is valid (in seconds). The default is one hour.  This default may be overridden by module specific settings.
 Setting_Description_token.lifetime=Specify the default lifetime an token is valid (in seconds). The default is one hour.  This default may be overridden by module specific settings.
-Setting_Description_token.storageMethod=Select the storage method @PwmAppName@ uses to save issued tokens.<table style\="width\: 400px"><tr><td>Method</td><td>Description</td></tr><tr><td>LocalDB</td><td>Stores the tokens in the local embedded LocalDB database.  Tokens are not common across multiple application instances.</td></tr><tr><td>DB</td><td>Store the tokens in a configured, remote database.  Tokens work across multiple application instances.</td></tr><tr><td>Crypto</td><td>Use crypto to create and read tokens, they are not stored locally.  Tokens work across multiple application instances if they have the same Security Key.  Crypto tokens ignore the length rules and might be too long to use for SMS purposes.</td></tr><tr><td>LDAP</td><td>Use the LDAP directory to store tokens.  Tokens work across multiple application instances.  You cannot use LDAP tokens as New User Registration tokens.</td></tr></table>
+Setting_Description_token.storageMethod=Select the storage method @PwmAppName@ uses to save issued tokens.<table style\="width\: 400px"><tr><td>Method</td><td>Description</td></tr><tr><td>LocalDB</td><td>Stores the tokens in the local embedded LocalDB database.  Tokens are not common across multiple application instances.</td></tr><tr><td>DB</td><td>Store the tokens in a configured, remote database.  Tokens work across multiple application instances.</td></tr><tr><td>Crypto</td><td>Use crypto to create and read tokens, they are not stored locally.  Tokens work across multiple application instances if they have the same Security Key.  Crypto tokens ignore the length and character rules and might be too long to use for SMS purposes.</td></tr><tr><td>LDAP</td><td>Use the LDAP directory to store tokens.  Tokens work across multiple application instances.  You cannot use LDAP tokens as New User Registration tokens.</td></tr></table>
 Setting_Description_token.valueMasking.enable=Enable this option to mask token destination display values (email addresses and telephone numbers).
 Setting_Description_token.valueMasking.enable=Enable this option to mask token destination display values (email addresses and telephone numbers).
 Setting_Description_updateAttributes.check.queryMatch=When you use the "checkProfile" or "checkAll" parameter with the command servlet, @PwmAppName@ uses this query match to determine if the user is required to populate the parameter values. <br/><br/>If this value is blank, then @PwmAppName@ checks the user's current values against the form requirements.
 Setting_Description_updateAttributes.check.queryMatch=When you use the "checkProfile" or "checkAll" parameter with the command servlet, @PwmAppName@ uses this query match to determine if the user is required to populate the parameter values. <br/><br/>If this value is blank, then @PwmAppName@ checks the user's current values against the form requirements.
 Setting_Description_updateAttributes.email.verification=Enable this option to send an email to the user's email address before @PwmAppName@ updates the account. The user's email must change to cause this verification email to be sent. The user must verify receipt of the email before @PwmAppName@ updates the account.
 Setting_Description_updateAttributes.email.verification=Enable this option to send an email to the user's email address before @PwmAppName@ updates the account. The user's email must change to cause this verification email to be sent. The user must verify receipt of the email before @PwmAppName@ updates the account.

+ 18 - 7
server/src/test/java/password/pwm/i18n/AdminPropertyKeysTest.java

@@ -25,9 +25,11 @@ package password.pwm.i18n;
 import org.junit.Assert;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.Test;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
+import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.Statistic;
 
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.HashSet;
 import java.util.ResourceBundle;
 import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.Set;
@@ -48,13 +50,22 @@ public class AdminPropertyKeysTest
                     password.pwm.i18n.Admin.STATISTICS_DESCRIPTION_PREFIX + statistic.getKey(),
                     password.pwm.i18n.Admin.STATISTICS_DESCRIPTION_PREFIX + statistic.getKey(),
                     password.pwm.i18n.Admin.STATISTICS_LABEL_PREFIX + statistic.getKey(),
                     password.pwm.i18n.Admin.STATISTICS_LABEL_PREFIX + statistic.getKey(),
             };
             };
-            for ( final String key : keys )
-            {
-                expectedKeys.add( key );
-                Assert.assertTrue(
-                        "Admin.properties missing record for " + key,
-                        resourceBundle.containsKey( key ) );
-            }
+            Collections.addAll( expectedKeys, keys );
+        }
+        for ( final AvgStatistic statistic : AvgStatistic.values() )
+        {
+            final String[] keys = new String[] {
+                    password.pwm.i18n.Admin.STATISTICS_DESCRIPTION_PREFIX + statistic.getKey(),
+                    password.pwm.i18n.Admin.STATISTICS_LABEL_PREFIX + statistic.getKey(),
+            };
+            Collections.addAll( expectedKeys, keys );
+        }
+
+        for ( final String key : expectedKeys )
+        {
+            Assert.assertTrue(
+                    "Admin.properties missing record for " + key,
+                    resourceBundle.containsKey( key ) );
         }
         }
 
 
         final Set<String> extraKeys = new HashSet<>( resourceBundle.keySet() );
         final Set<String> extraKeys = new HashSet<>( resourceBundle.keySet() );

+ 1 - 1
server/src/test/java/password/pwm/util/localdb/LocalDBLoggerExtendedTest.java

@@ -30,7 +30,7 @@ import password.pwm.AppProperty;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.config.stored.StoredConfigurationImpl;
-import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.EventRateMeter;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.Percent;
 import password.pwm.util.java.Percent;

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/activateuser.jsp

@@ -50,7 +50,6 @@
                 <input type="hidden" name="processAction" value="activate"/>
                 <input type="hidden" name="processAction" value="activate"/>
                 <%@ include file="/WEB-INF/jsp/fragment/cancel-button.jsp" %>
                 <%@ include file="/WEB-INF/jsp/fragment/cancel-button.jsp" %>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
-                <input type="hidden" name="skipCaptcha" value="${param.skipCaptcha}"/>
             </div>
             </div>
         </form>
         </form>
     </div>
     </div>

+ 22 - 9
webapp/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp

@@ -20,16 +20,19 @@
  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 --%>
 --%>
 
 
-<%@ page import="password.pwm.error.PwmError" %>
 <%@ page import="password.pwm.error.PwmException" %>
 <%@ page import="password.pwm.error.PwmException" %>
 <%@ page import="password.pwm.i18n.Admin" %>
 <%@ page import="password.pwm.i18n.Admin" %>
+<%@ page import="password.pwm.svc.stats.AvgStatistic" %>
+<%@ page import="password.pwm.svc.stats.DailyKey" %>
 <%@ page import="password.pwm.svc.stats.Statistic" %>
 <%@ page import="password.pwm.svc.stats.Statistic" %>
+<%@ page import="password.pwm.svc.stats.StatisticType" %>
 <%@ page import="password.pwm.svc.stats.StatisticsBundle" %>
 <%@ page import="password.pwm.svc.stats.StatisticsBundle" %>
 <%@ page import="password.pwm.svc.stats.StatisticsManager" %>
 <%@ page import="password.pwm.svc.stats.StatisticsManager" %>
 <%@ page import="password.pwm.util.java.JavaHelper" %>
 <%@ page import="password.pwm.util.java.JavaHelper" %>
-<%@ page import="java.text.DateFormat" %>
 <%@ page import="java.util.Locale" %>
 <%@ page import="java.util.Locale" %>
 <%@ page import="java.util.Map" %>
 <%@ page import="java.util.Map" %>
+<%@ page import="password.pwm.util.java.StringUtil" %>
+<%@ page import="java.time.format.DateTimeFormatter" %>
 
 
 <!DOCTYPE html>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
@@ -182,16 +185,16 @@
                                             <select name="statsPeriodSelect"
                                             <select name="statsPeriodSelect"
                                                     style="width: 350px;">
                                                     style="width: 350px;">
                                                 <option value="<%=StatisticsManager.KEY_CUMULATIVE%>" <%= StatisticsManager.KEY_CUMULATIVE.equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
                                                 <option value="<%=StatisticsManager.KEY_CUMULATIVE%>" <%= StatisticsManager.KEY_CUMULATIVE.equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                                    since installation - <%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getInstallTime()) %>
+                                                    since installation - <span class="timestamp"><%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getInstallTime()) %></span>
                                                 </option>
                                                 </option>
                                                 <option value="<%=StatisticsManager.KEY_CURRENT%>" <%= StatisticsManager.KEY_CURRENT.equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
                                                 <option value="<%=StatisticsManager.KEY_CURRENT%>" <%= StatisticsManager.KEY_CURRENT.equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                                    since startup - <%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getStartupTime()) %>
+                                                    since startup - <span class="timestamp"><%= JavaHelper.toIsoDate(analysis_pwmRequest.getPwmApplication().getStartupTime()) %></span>
                                                 </option>
                                                 </option>
-                                                <% final Map<StatisticsManager.DailyKey, String> availableKeys = statsManager.getAvailableKeys(locale); %>
-                                                <% for (final Map.Entry<StatisticsManager.DailyKey, String> entry : availableKeys.entrySet()) { %>
-                                                <% final StatisticsManager.DailyKey key = entry.getKey(); %>
+                                                <% final Map<DailyKey, String> availableKeys = statsManager.getAvailableKeys(locale); %>
+                                                <% for (final Map.Entry<DailyKey, String> entry : availableKeys.entrySet()) { %>
+                                                <% final DailyKey key = entry.getKey(); %>
                                                 <option value="<%=key%>" <%= key.toString().equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
                                                 <option value="<%=key%>" <%= key.toString().equals(statsPeriodSelect) ? "selected=\"selected\"" : "" %>>
-                                                    <%= entry.getValue() %>
+                                                    <%=key.localDate().format(DateTimeFormatter.ISO_LOCAL_DATE)%>
                                                 </option>
                                                 </option>
                                                 <% } %>
                                                 <% } %>
                                             </select>
                                             </select>
@@ -208,7 +211,17 @@
                                         <span id="Statistic_Key_<%=loopStat.getKey()%>"><%= loopStat.getLabel(locale) %><span/>
                                         <span id="Statistic_Key_<%=loopStat.getKey()%>"><%= loopStat.getLabel(locale) %><span/>
                                     </td>
                                     </td>
                                     <td>
                                     <td>
-                                        <%= stats.getStatistic(loopStat) %><%= loopStat.getType() == Statistic.Type.AVERAGE && loopStat != Statistic.AVG_PASSWORD_STRENGTH ? " ms" : "" %>
+                                        <%= stats.getStatistic(loopStat) %>
+                                    </td>
+                                </tr>
+                                <% } %>
+                                <% for (final AvgStatistic loopStat : AvgStatistic.values()) { %>
+                                <tr>
+                                    <td >
+                                        <span id="Statistic_Key_<%=loopStat.getKey()%>"><%= loopStat.getLabel(locale) %><span/>
+                                    </td>
+                                    <td>
+                                        <%= stats.getAvgStatistic(loopStat) %><%= loopStat.getUnit() %>
                                     </td>
                                     </td>
                                 </tr>
                                 </tr>
                                 <% } %>
                                 <% } %>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/forgottenpassword-search.jsp

@@ -58,7 +58,6 @@
                     </button>
                     </button>
                 </pwm:if>
                 </pwm:if>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
-                <input type="hidden" name="skipCaptcha" value="${param.skipCaptcha}"/>
             </div>
             </div>
         </form>
         </form>
     </div>
     </div>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/forgottenusername-search.jsp

@@ -51,7 +51,6 @@
                 </button>
                 </button>
                 <%@ include file="/WEB-INF/jsp/fragment/cancel-button.jsp" %>
                 <%@ include file="/WEB-INF/jsp/fragment/cancel-button.jsp" %>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
-                <input type="hidden" name="skipCaptcha" value="${param.skipCaptcha}"/>
             </div>
             </div>
         </form>
         </form>
     </div>
     </div>

+ 1 - 1
webapp/src/main/webapp/WEB-INF/jsp/fragment/form.jsp

@@ -128,7 +128,7 @@
                 </script>
                 </script>
             </pwm:script>
             </pwm:script>
             <% if (!StringUtil.isEmpty( currentValue) ) { %>
             <% if (!StringUtil.isEmpty( currentValue) ) { %>
-            <button type="submit" id="button-deletePhoto-<%=loopConfiguration.getName()%>" name="<%=loopConfiguration.getName()%>" class="btn" title="<pwm:display key="Button_Delete"/>" form="form-deletePhoto-<%=loopConfiguration.getName()%>">
+            <button type="button" id="button-deletePhoto-<%=loopConfiguration.getName()%>" name="<%=loopConfiguration.getName()%>" class="btn" title="<pwm:display key="Button_Delete"/>">
                 <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span></pwm:if>
                 <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span></pwm:if>
                 <pwm:display key="Button_Delete"/>
                 <pwm:display key="Button_Delete"/>
             </button>
             </button>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/login.jsp

@@ -71,7 +71,6 @@
                         <%@ include file="/WEB-INF/jsp/fragment/cancel-button.jsp" %>
                         <%@ include file="/WEB-INF/jsp/fragment/cancel-button.jsp" %>
                     </pwm:if>
                     </pwm:if>
                     <input type="hidden" id="pwmFormID" name="pwmFormID" value="<pwm:FormID/>"/>
                     <input type="hidden" id="pwmFormID" name="pwmFormID" value="<pwm:FormID/>"/>
-                    <input type="hidden" name="skipCaptcha" value="${param.skipCaptcha}"/>
                 </div>
                 </div>
             </div>
             </div>
         </form>
         </form>

+ 0 - 1
webapp/src/main/webapp/WEB-INF/jsp/newuser.jsp

@@ -54,7 +54,6 @@
                     <pwm:display key="Button_Continue"/>
                     <pwm:display key="Button_Continue"/>
                 </button>
                 </button>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
                 <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
-                <input type="hidden" name="skipCaptcha" value="${param.skipCaptcha}"/>
 
 
                 <% if ((Boolean)JspUtility.getAttribute(pageContext, PwmRequestAttribute.NewUser_FormShowBackButton)) { %>
                 <% if ((Boolean)JspUtility.getAttribute(pageContext, PwmRequestAttribute.NewUser_FormShowBackButton)) { %>
                 <button type="button" id="button-goBack" name="button-goBack" class="btn" >
                 <button type="button" id="button-goBack" name="button-goBack" class="btn" >

+ 10 - 7
webapp/src/main/webapp/public/resources/js/main.js

@@ -285,9 +285,10 @@ PWM_MAIN.applyFormAttributes = function() {
     require(["dojo"], function (dojo) {
     require(["dojo"], function (dojo) {
         if(dojo.isIE){
         if(dojo.isIE){
             PWM_MAIN.doQuery("button[type=submit][form]",function(element){
             PWM_MAIN.doQuery("button[type=submit][form]",function(element){
-                PWM_MAIN.log('added event handler for submit button with form attribute ' + element.id);
+                PWM_MAIN.log('added IE event handler for submit button with form attribute ' + element.id);
                 PWM_MAIN.addEventHandler(element,'click',function(e){
                 PWM_MAIN.addEventHandler(element,'click',function(e){
-                    PWM_MAIN.stopEvent(e);
+                    e.preventDefault();
+                    PWM_MAIN.log('IE event handler intercepted submit for referenced form attribute ' + element.id);
                     var formID = element.getAttribute('form');
                     var formID = element.getAttribute('form');
                     PWM_MAIN.handleFormSubmit(PWM_MAIN.getObject(formID));
                     PWM_MAIN.handleFormSubmit(PWM_MAIN.getObject(formID));
                 });
                 });
@@ -424,11 +425,13 @@ PWM_MAIN.handleFormSubmit = function(form, event) {
     PWM_MAIN.cancelEvent(event);
     PWM_MAIN.cancelEvent(event);
 
 
     PWM_GLOBAL['idle_suspendTimeout'] = true;
     PWM_GLOBAL['idle_suspendTimeout'] = true;
-    var formElements = form.elements;
-    for (var i = 0; i < formElements.length; i++) {
-        formElements[i].readOnly = true;
-        if (formElements[i].type === 'button' || formElements[i].type === 'submit') {
-            formElements[i].disabled = true;
+    if ( form.elements ) {
+        var formElements = form.elements;
+        for (var i = 0; i < formElements.length; i++) {
+            formElements[i].readOnly = true;
+            if (formElements[i].type === 'button' || formElements[i].type === 'submit') {
+                formElements[i].disabled = true;
+            }
         }
         }
     }
     }
 
 

+ 1 - 1
webapp/src/main/webapp/public/resources/js/responses.js

@@ -57,7 +57,7 @@ PWM_RESPONSES.validateResponses=function() {
 };
 };
 
 
 PWM_RESPONSES.updateDisplay=function(resultInfo) {
 PWM_RESPONSES.updateDisplay=function(resultInfo) {
-    if (resultInfo === null) {
+    if (!resultInfo) {
         PWM_MAIN.getObject("button-setResponses").disabled = false;
         PWM_MAIN.getObject("button-setResponses").disabled = false;
         return;
         return;
     }
     }

+ 3 - 3
webapp/src/main/webapp/public/resources/js/uilibrary.js

@@ -454,12 +454,12 @@ UILibrary.uploadFileDialog = function(options) {
     body += '<div id="fileList"></div>';
     body += '<div id="fileList"></div>';
     body += '<input style="width:80%" class="btn" name="uploadFile" type="file" label="Select File" id="uploadFile"/>';
     body += '<input style="width:80%" class="btn" name="uploadFile" type="file" label="Select File" id="uploadFile"/>';
     body += '<div class="buttonbar">';
     body += '<div class="buttonbar">';
-    body += '<button class="btn" type="button" id="uploadButton" name="Upload" disabled><span class="pwm-icon pwm-icon-upload"></span> Upload</button>';
-    body += '</div></div>';
+    body += '<button class="btn" type="button" id="uploadButton" name="Upload" disabled><span class="pwm-icon pwm-icon-upload"></span>';
+    body +=  PWM_MAIN.showString('Button_Upload') + '</button></div></div>';
 
 
     var currentUrl = window.location.pathname;
     var currentUrl = window.location.pathname;
     var uploadUrl = 'url' in options ? options['url'] : currentUrl;
     var uploadUrl = 'url' in options ? options['url'] : currentUrl;
-    var title = 'title' in options ? options['title'] : 'Upload File';
+    var title = 'title' in options ? options['title'] : PWM_MAIN.showString('Title_Upload');
 
 
     uploadUrl = PWM_MAIN.addPwmFormIDtoURL(uploadUrl);
     uploadUrl = PWM_MAIN.addPwmFormIDtoURL(uploadUrl);