password.pwm.svc.report.ReportService.java Source code

Java tutorial

Introduction

Here is the source code for password.pwm.svc.report.ReportService.java

Source

/*
 * Password Management Servlets (PWM)
 * http://www.pwm-project.org
 *
 * Copyright (c) 2006-2009 Novell, Inc.
 * Copyright (c) 2009-2016 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.report;

import com.novell.ldapchai.exception.ChaiOperationException;
import com.novell.ldapchai.exception.ChaiUnavailableException;
import com.novell.ldapchai.provider.ChaiProvider;
import org.apache.commons.csv.CSVPrinter;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.PwmApplicationMode;
import password.pwm.PwmConstants;
import password.pwm.bean.UserIdentity;
import password.pwm.bean.UserInfoBean;
import password.pwm.config.Configuration;
import password.pwm.config.PwmSetting;
import password.pwm.config.option.DataStorageMethod;
import password.pwm.error.*;
import password.pwm.health.HealthRecord;
import password.pwm.i18n.Display;
import password.pwm.ldap.UserSearchEngine;
import password.pwm.ldap.UserStatusReader;
import password.pwm.svc.PwmService;
import password.pwm.util.*;
import password.pwm.util.localdb.LocalDB;
import password.pwm.util.localdb.LocalDBException;
import password.pwm.util.logging.PwmLogger;

import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ReportService implements PwmService {
    private static final PwmLogger LOGGER = PwmLogger.forClass(ReportService.class);

    private final AvgTracker avgTracker = new AvgTracker(100);

    private PwmApplication pwmApplication;
    private STATUS status = STATUS.NEW;
    private boolean cancelFlag = false;
    private ReportStatusInfo reportStatus = new ReportStatusInfo("");
    private ReportSummaryData summaryData = ReportSummaryData.newSummaryData(null);
    private ScheduledExecutorService executorService;

    private UserCacheService userCacheService;
    private ReportSettings settings = new ReportSettings();

    public ReportService() {
    }

    public STATUS status() {
        return status;
    }

    public void clear() throws LocalDBException, PwmUnrecoverableException {
        final Date startTime = new Date();
        LOGGER.info(PwmConstants.REPORTING_SESSION_LABEL, "clearing cached report data");
        if (userCacheService != null) {
            userCacheService.clear();
        }
        summaryData = ReportSummaryData.newSummaryData(settings.getTrackDays());
        reportStatus = new ReportStatusInfo(settings.getSettingsHash());
        saveTempData();
        LOGGER.info(PwmConstants.REPORTING_SESSION_LABEL,
                "finished clearing report " + TimeDuration.fromCurrent(startTime).asCompactString());
    }

    @Override
    public void init(PwmApplication pwmApplication) throws PwmException {
        status = STATUS.OPENING;
        this.pwmApplication = pwmApplication;

        if (pwmApplication.getApplicationMode() == PwmApplicationMode.READ_ONLY) {
            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL, "application mode is read-only, will remain closed");
            status = STATUS.CLOSED;
            return;
        }

        if (pwmApplication.getLocalDB() == null || LocalDB.Status.OPEN != pwmApplication.getLocalDB().status()) {
            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL, "LocalDB is not open, will remain closed");
            status = STATUS.CLOSED;
            return;
        }

        if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.REPORTING_ENABLE)) {
            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,
                    "reporting module is not enabled, will remain closed");
            status = STATUS.CLOSED;
            clear();
            return;
        }

        try {
            userCacheService = new UserCacheService();
            userCacheService.init(pwmApplication);
        } catch (Exception e) {
            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL, "unable to init cache service");
            status = STATUS.CLOSED;
            return;
        }

        settings = ReportSettings.readSettingsFromConfig(pwmApplication.getConfig());
        summaryData = ReportSummaryData.newSummaryData(settings.getTrackDays());

        executorService = Executors.newSingleThreadScheduledExecutor(
                Helper.makePwmThreadFactory(Helper.makeThreadName(pwmApplication, this.getClass()) + "-", true));

        String startupMsg = "report service started";
        LOGGER.debug(startupMsg);

        executorService.submit(new InitializationTask());

        status = STATUS.OPEN;
    }

    @Override
    public void close() {
        status = STATUS.CLOSED;
        saveTempData();
        pwmApplication.writeAppAttribute(PwmApplication.AppAttribute.REPORT_CLEAN_FLAG, "true");
        if (userCacheService != null) {
            userCacheService.close();
        }
        if (executorService != null) {
            executorService.shutdown();
        }
        executorService = null;
    }

    private void saveTempData() {
        try {
            final String jsonInfo = JsonUtil.serialize(reportStatus);
            pwmApplication.writeAppAttribute(PwmApplication.AppAttribute.REPORT_STATUS, jsonInfo);
        } catch (Exception e) {
            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,
                    "error writing cached report dredge info into memory: " + e.getMessage());
        }
    }

    private void initTempData() throws LocalDBException, PwmUnrecoverableException {
        final Boolean cleanFlag = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.REPORT_CLEAN_FLAG,
                Boolean.class);
        if (cleanFlag != null && cleanFlag) {
            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL, "did not shut down cleanly");
            reportStatus = new ReportStatusInfo(settings.getSettingsHash());
            reportStatus.setTotal(userCacheService.size());
        } else {
            try {
                reportStatus = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.REPORT_STATUS,
                        ReportStatusInfo.class);
            } catch (Exception e) {
                LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,
                        "error loading cached report status info into memory: " + e.getMessage());
            }
        }

        reportStatus = reportStatus == null ? new ReportStatusInfo(settings.getSettingsHash()) : reportStatus; //safety

        final String currentSettingCache = settings.getSettingsHash();
        if (reportStatus.getSettingsHash() != null && !reportStatus.getSettingsHash().equals(currentSettingCache)) {
            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,
                    "configuration has changed, will clear cached report data");
            clear();
        }

        reportStatus.setInProgress(false);

        pwmApplication.writeAppAttribute(PwmApplication.AppAttribute.REPORT_CLEAN_FLAG, false);
    }

    @Override
    public List<HealthRecord> healthCheck() {
        return null;
    }

    @Override
    public ServiceInfo serviceInfo() {
        return new ServiceInfo(Collections.singletonList(DataStorageMethod.LDAP));
    }

    public void scheduleImmediateUpdate() {
        if (!reportStatus.isInProgress()) {
            executorService.submit(new DredgeTask());
            LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL, "submitted new ldap dredge task to executorService");
        }
    }

    public void cancelUpdate() {
        cancelFlag = true;
    }

    private void updateCacheFromLdap() throws ChaiUnavailableException, ChaiOperationException,
            PwmOperationalException, PwmUnrecoverableException {
        LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,
                "beginning process to updating user cache records from ldap");
        if (status != STATUS.OPEN) {
            return;
        }
        cancelFlag = false;
        reportStatus = new ReportStatusInfo(settings.getSettingsHash());
        reportStatus.setInProgress(true);
        reportStatus.setStartDate(new Date());
        try {
            final Queue<UserIdentity> allUsers = new LinkedList<>(getListOfUsers());
            reportStatus.setTotal(allUsers.size());
            while (status == STATUS.OPEN && !allUsers.isEmpty() && !cancelFlag) {
                final Date startUpdateTime = new Date();
                final UserIdentity userIdentity = allUsers.poll();
                try {
                    if (updateCachedRecordFromLdap(userIdentity)) {
                        reportStatus.setUpdated(reportStatus.getUpdated() + 1);
                    }
                } catch (Exception e) {
                    String errorMsg = "error while updating report cache for " + userIdentity.toString()
                            + ", cause: ";
                    errorMsg += e instanceof PwmException ? ((PwmException) e).getErrorInformation().toDebugStr()
                            : e.getMessage();
                    final ErrorInformation errorInformation;
                    errorInformation = new ErrorInformation(PwmError.ERROR_REPORTING_ERROR, errorMsg);
                    LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL, errorInformation.toDebugStr());
                    reportStatus.setLastError(errorInformation);
                    reportStatus.setErrors(reportStatus.getErrors() + 1);
                }
                reportStatus.setCount(reportStatus.getCount() + 1);
                reportStatus.getEventRateMeter().markEvents(1);
                final TimeDuration totalUpdateTime = TimeDuration.fromCurrent(startUpdateTime);
                if (settings.isAutoCalcRest()) {
                    avgTracker.addSample(totalUpdateTime.getTotalMilliseconds());
                    Helper.pause(avgTracker.avgAsLong());
                } else {
                    Helper.pause(settings.getRestTime().getTotalMilliseconds());
                }
            }
            if (cancelFlag) {
                reportStatus.setLastError(
                        new ErrorInformation(PwmError.ERROR_SERVICE_NOT_AVAILABLE, "report cancelled by operator"));
            }
        } finally {
            reportStatus.setFinishDate(new Date());
            reportStatus.setInProgress(false);
        }
        LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,
                "update user cache process completed: " + JsonUtil.serialize(reportStatus));
    }

    private void updateRestingCacheData() {
        final long startTime = System.currentTimeMillis();
        int examinedRecords = 0;
        ClosableIterator<UserCacheRecord> iterator = null;
        try {
            LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL, "checking size of stored cache records");
            final int totalRecords = userCacheService.size();
            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,
                    "beginning cache review process of " + totalRecords + " records");
            iterator = iterator();
            Date lastLogOutputTime = new Date();
            while (iterator.hasNext() && status == STATUS.OPEN) {
                final UserCacheRecord record = iterator.next(); // (purge routine is embedded in next();

                if (summaryData != null && record != null) {
                    summaryData.update(record);
                }

                examinedRecords++;

                if (TimeDuration.fromCurrent(lastLogOutputTime).isLongerThan(30, TimeUnit.SECONDS)) {
                    final TimeDuration progressDuration = TimeDuration.fromCurrent(startTime);
                    LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL, "cache review process in progress, examined "
                            + examinedRecords + " records in " + progressDuration.asCompactString());
                    lastLogOutputTime = new Date();
                }
            }
            final TimeDuration totalTime = TimeDuration.fromCurrent(startTime);
            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL, "completed cache review process of "
                    + examinedRecords + " cached report records in " + totalTime.asCompactString());
        } finally {
            if (iterator != null) {
                iterator.close();
            }
        }
    }

    private boolean updateCachedRecordFromLdap(final UserIdentity userIdentity)
            throws ChaiUnavailableException, PwmUnrecoverableException, LocalDBException {
        if (status != STATUS.OPEN) {
            return false;
        }

        final UserCacheService.StorageKey storageKey = UserCacheService.StorageKey.fromUserIdentity(pwmApplication,
                userIdentity);
        return updateCachedRecordFromLdap(userIdentity, null, storageKey);
    }

    private boolean updateCachedRecordFromLdap(final UserIdentity userIdentity, final UserInfoBean userInfoBean,
            final UserCacheService.StorageKey storageKey)
            throws ChaiUnavailableException, PwmUnrecoverableException, LocalDBException {
        final UserCacheRecord userCacheRecord = userCacheService.readStorageKey(storageKey);
        TimeDuration cacheAge = null;
        if (userCacheRecord != null && userCacheRecord.getCacheTimestamp() != null) {
            cacheAge = TimeDuration.fromCurrent(userCacheRecord.getCacheTimestamp());
        }

        boolean updateCache = false;
        if (userInfoBean != null) {
            updateCache = true;
        } else {
            if (cacheAge == null) {
                LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL,
                        "stored cache for " + userIdentity + " is missing cache storage timestamp, will update");
                updateCache = true;
            } else if (cacheAge.isLongerThan(settings.getMinCacheAge())) {
                LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL, "stored cache for " + userIdentity + " is "
                        + cacheAge.asCompactString() + " old, will update");
                updateCache = true;
            }
        }

        if (updateCache) {
            if (userCacheRecord != null) {
                if (summaryData != null && summaryData.getEpoch() != null
                        && summaryData.getEpoch().equals(userCacheRecord.getSummaryEpoch())) {
                    summaryData.remove(userCacheRecord);
                }
            }
            final UserInfoBean newUserBean;
            if (userInfoBean != null) {
                newUserBean = userInfoBean;
            } else {
                newUserBean = new UserInfoBean();
                final UserStatusReader.Settings readerSettings = new UserStatusReader.Settings();
                final ChaiProvider chaiProvider = pwmApplication
                        .getProxyChaiProvider(userIdentity.getLdapProfileID());
                final UserStatusReader userStatusReader = new UserStatusReader(pwmApplication,
                        PwmConstants.REPORTING_SESSION_LABEL, readerSettings);
                userStatusReader.populateUserInfoBean(newUserBean, PwmConstants.DEFAULT_LOCALE, userIdentity,
                        chaiProvider);
            }
            final UserCacheRecord newUserCacheRecord = userCacheService.updateUserCache(newUserBean);

            if (summaryData != null && summaryData.getEpoch() != null && newUserCacheRecord != null) {
                if (!summaryData.getEpoch().equals(newUserCacheRecord.getSummaryEpoch())) {
                    newUserCacheRecord.setSummaryEpoch(summaryData.getEpoch());
                    userCacheService.store(newUserCacheRecord);
                }
                summaryData.update(newUserCacheRecord);
            }
        }

        return updateCache;
    }

    public ReportStatusInfo getReportStatusInfo() {
        return reportStatus;
    }

    private List<UserIdentity> getListOfUsers() throws ChaiUnavailableException, ChaiOperationException,
            PwmUnrecoverableException, PwmOperationalException {
        return readAllUsersFromLdap(pwmApplication, settings.getSearchFilter(), settings.getMaxSearchSize());
    }

    private static List<UserIdentity> readAllUsersFromLdap(final PwmApplication pwmApplication,
            final String searchFilter, final int maxResults) throws ChaiUnavailableException,
            ChaiOperationException, PwmUnrecoverableException, PwmOperationalException {
        final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, null);
        final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
        searchConfiguration.setEnableValueEscaping(false);
        searchConfiguration.setSearchTimeout(Long
                .parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.REPORTING_LDAP_SEARCH_TIMEOUT)));

        if (searchFilter == null) {
            searchConfiguration.setUsername("*");
        } else {
            searchConfiguration.setFilter(searchFilter);
        }

        LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,
                "beginning UserReportService user search using parameters: "
                        + (JsonUtil.serialize(searchConfiguration)));

        final Map<UserIdentity, Map<String, String>> searchResults = userSearchEngine
                .performMultiUserSearch(searchConfiguration, maxResults, Collections.<String>emptyList());
        LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,
                "user search found " + searchResults.size() + " users for reporting");
        final List<UserIdentity> returnList = new ArrayList<>(searchResults.keySet());
        Collections.shuffle(returnList);
        return returnList;
    }

    public interface RecordIterator<K> extends ClosableIterator<UserCacheRecord> {
    }

    public RecordIterator<UserCacheRecord> iterator() {
        return new RecordIterator<UserCacheRecord>() {
            private UserCacheService.UserStatusCacheBeanIterator<UserCacheService.StorageKey> storageKeyIterator = userCacheService
                    .iterator();

            @Override
            public boolean hasNext() {
                return this.storageKeyIterator.hasNext();
            }

            @Override
            public UserCacheRecord next() {
                try {
                    UserCacheRecord returnBean = null;
                    while (returnBean == null && this.storageKeyIterator.hasNext()) {
                        UserCacheService.StorageKey key = this.storageKeyIterator.next();
                        returnBean = userCacheService.readStorageKey(key);
                        if (returnBean != null) {
                            if (returnBean.getCacheTimestamp() == null) {
                                LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,
                                        "purging record due to missing cache timestamp: "
                                                + JsonUtil.serialize(returnBean));
                                userCacheService.removeStorageKey(key);
                            } else if (TimeDuration.fromCurrent(returnBean.getCacheTimestamp())
                                    .isLongerThan(settings.getMaxCacheAge())) {
                                LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,
                                        "purging record due to old age timestamp: "
                                                + JsonUtil.serialize(returnBean));
                                userCacheService.removeStorageKey(key);
                            } else {
                                return returnBean;
                            }
                        }

                    }
                } catch (LocalDBException e) {
                    throw new IllegalStateException(
                            "unexpected iterator traversal error while reading LocalDB: " + e.getMessage());
                }
                return null;
            }

            @Override
            public void remove() {

            }

            @Override
            public void close() {
                storageKeyIterator.close();
            }
        };
    }

    public void outputSummaryToCsv(final OutputStream outputStream, final Locale locale) throws IOException {
        final List<ReportSummaryData.PresentationRow> outputList = summaryData
                .asPresentableCollection(pwmApplication.getConfig(), locale);
        final CSVPrinter csvPrinter = Helper.makeCsvPrinter(outputStream);

        for (final ReportSummaryData.PresentationRow presentationRow : outputList) {
            final List<String> headerRow = new ArrayList<>();
            headerRow.add(presentationRow.getLabel());
            headerRow.add(presentationRow.getCount());
            headerRow.add(presentationRow.getPct());
            csvPrinter.printRecord(headerRow);
        }

        csvPrinter.close();
    }

    public void outputToCsv(final OutputStream outputStream, final boolean includeHeader, final Locale locale)
            throws IOException, ChaiUnavailableException, ChaiOperationException, PwmUnrecoverableException,
            PwmOperationalException {
        final Configuration config = pwmApplication.getConfig();
        final ReportColumnFilter columnFilter = new ReportColumnFilter();

        outputToCsv(outputStream, includeHeader, locale, config, columnFilter);
    }

    public void outputToCsv(final OutputStream outputStream, final boolean includeHeader, final Locale locale,
            final ReportColumnFilter columnFilter) throws IOException, ChaiUnavailableException,
            ChaiOperationException, PwmUnrecoverableException, PwmOperationalException {
        final Configuration config = pwmApplication.getConfig();
        outputToCsv(outputStream, includeHeader, locale, config, columnFilter);
    }

    public void outputToCsv(final OutputStream outputStream, final boolean includeHeader, final Locale locale,
            final Configuration config, final ReportColumnFilter columnFilter) throws IOException,
            ChaiUnavailableException, ChaiOperationException, PwmUnrecoverableException, PwmOperationalException {
        final CSVPrinter csvPrinter = Helper.makeCsvPrinter(outputStream);
        final Class localeClass = password.pwm.i18n.Admin.class;
        if (includeHeader) {
            final List<String> headerRow = new ArrayList<>();

            if (columnFilter.isUsernameVisible())
                headerRow.add(
                        LocaleHelper.getLocalizedMessage(locale, "Field_Report_Username", config, localeClass));
            if (columnFilter.isUserDnVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_UserDN", config, localeClass));
            if (columnFilter.isLdapProfileVisible())
                headerRow.add(
                        LocaleHelper.getLocalizedMessage(locale, "Field_Report_LDAP_Profile", config, localeClass));
            if (columnFilter.isEmailVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_Email", config, localeClass));
            if (columnFilter.isUserGuidVisible())
                headerRow.add(
                        LocaleHelper.getLocalizedMessage(locale, "Field_Report_UserGuid", config, localeClass));
            if (columnFilter.isAccountExpirationTimeVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_AccountExpireTime", config,
                        localeClass));
            if (columnFilter.isPasswordExpirationTimeVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdExpireTime", config,
                        localeClass));
            if (columnFilter.isPasswordChangeTimeVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdChangeTime", config,
                        localeClass));
            if (columnFilter.isResponseSetTimeVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_ResponseSaveTime", config,
                        localeClass));
            if (columnFilter.isLastLoginTimeVisible())
                headerRow.add(
                        LocaleHelper.getLocalizedMessage(locale, "Field_Report_LastLogin", config, localeClass));
            if (columnFilter.isHasResponsesVisible())
                headerRow.add(
                        LocaleHelper.getLocalizedMessage(locale, "Field_Report_HasResponses", config, localeClass));
            if (columnFilter.isHasHelpdeskResponsesVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_HasHelpdeskResponses", config,
                        localeClass));
            if (columnFilter.isResponseStorageMethodVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_ResponseStorageMethod", config,
                        localeClass));
            if (columnFilter.isResponseFormatTypeVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_ResponseFormatType", config,
                        localeClass));
            if (columnFilter.isPasswordStatusExpiredVisible())
                headerRow.add(
                        LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdExpired", config, localeClass));
            if (columnFilter.isPasswordStatusPreExpiredVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdPreExpired", config,
                        localeClass));
            if (columnFilter.isPasswordStatusViolatesPolicyVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdViolatesPolicy", config,
                        localeClass));
            if (columnFilter.isPasswordStatusWarnPeriodVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdWarnPeriod", config,
                        localeClass));
            if (columnFilter.isRequiresPasswordUpdateVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_RequiresPasswordUpdate",
                        config, localeClass));
            if (columnFilter.isRequiresResponseUpdateVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_RequiresResponseUpdate",
                        config, localeClass));
            if (columnFilter.isRequiresProfileUpdateVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_RequiresProfileUpdate", config,
                        localeClass));
            if (columnFilter.isCacheTimestampVisible())
                headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_RecordCacheTime", config,
                        localeClass));

            csvPrinter.printRecord(headerRow);
        }

        ClosableIterator<UserCacheRecord> cacheBeanIterator = null;
        try {
            cacheBeanIterator = this.iterator();
            while (cacheBeanIterator.hasNext()) {
                final UserCacheRecord userCacheRecord = cacheBeanIterator.next();
                outputRecordRow(config, locale, userCacheRecord, csvPrinter, columnFilter);
            }
        } finally {
            if (cacheBeanIterator != null) {
                cacheBeanIterator.close();
            }
        }

        csvPrinter.flush();
    }

    private void outputRecordRow(final Configuration config, final Locale locale,
            final UserCacheRecord userCacheRecord, final CSVPrinter csvPrinter,
            final ReportColumnFilter columnFilter) throws IOException {
        final String trueField = Display.getLocalizedMessage(locale, Display.Value_True, config);
        final String falseField = Display.getLocalizedMessage(locale, Display.Value_False, config);
        final String naField = Display.getLocalizedMessage(locale, Display.Value_NotApplicable, config);
        final List<String> csvRow = new ArrayList<>();
        if (columnFilter.isUsernameVisible())
            csvRow.add(userCacheRecord.getUsername());
        if (columnFilter.isUserDnVisible())
            csvRow.add(userCacheRecord.getUserDN());
        if (columnFilter.isLdapProfileVisible())
            csvRow.add(userCacheRecord.getLdapProfile());
        if (columnFilter.isEmailVisible())
            csvRow.add(userCacheRecord.getEmail());
        if (columnFilter.isUserGuidVisible())
            csvRow.add(userCacheRecord.getUserGUID());
        if (columnFilter.isAccountExpirationTimeVisible())
            csvRow.add(userCacheRecord.getAccountExpirationTime() == null ? naField
                    : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getAccountExpirationTime()));

        if (columnFilter.isPasswordExpirationTimeVisible())
            csvRow.add(userCacheRecord.getPasswordExpirationTime() == null ? naField
                    : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getPasswordExpirationTime()));

        if (columnFilter.isPasswordChangeTimeVisible())
            csvRow.add(userCacheRecord.getPasswordChangeTime() == null ? naField
                    : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getPasswordChangeTime()));

        if (columnFilter.isResponseSetTimeVisible())
            csvRow.add(userCacheRecord.getResponseSetTime() == null ? naField
                    : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getResponseSetTime()));

        if (columnFilter.isLastLoginTimeVisible())
            csvRow.add(userCacheRecord.getLastLoginTime() == null ? naField
                    : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getLastLoginTime()));

        if (columnFilter.isHasResponsesVisible())
            csvRow.add(userCacheRecord.isHasResponses() ? trueField : falseField);
        if (columnFilter.isHasHelpdeskResponsesVisible())
            csvRow.add(userCacheRecord.isHasHelpdeskResponses() ? trueField : falseField);

        if (columnFilter.isResponseStorageMethodVisible())
            csvRow.add(userCacheRecord.getResponseStorageMethod() == null ? naField
                    : userCacheRecord.getResponseStorageMethod().toString());

        if (columnFilter.isResponseFormatTypeVisible())
            csvRow.add(userCacheRecord.getResponseFormatType() == null ? naField
                    : userCacheRecord.getResponseFormatType().toString());

        if (columnFilter.isPasswordStatusExpiredVisible())
            csvRow.add(userCacheRecord.getPasswordStatus().isExpired() ? trueField : falseField);
        if (columnFilter.isPasswordStatusPreExpiredVisible())
            csvRow.add(userCacheRecord.getPasswordStatus().isPreExpired() ? trueField : falseField);
        if (columnFilter.isPasswordStatusViolatesPolicyVisible())
            csvRow.add(userCacheRecord.getPasswordStatus().isViolatesPolicy() ? trueField : falseField);
        if (columnFilter.isPasswordStatusWarnPeriodVisible())
            csvRow.add(userCacheRecord.getPasswordStatus().isWarnPeriod() ? trueField : falseField);
        if (columnFilter.isRequiresPasswordUpdateVisible())
            csvRow.add(userCacheRecord.isRequiresPasswordUpdate() ? trueField : falseField);
        if (columnFilter.isRequiresResponseUpdateVisible())
            csvRow.add(userCacheRecord.isRequiresResponseUpdate() ? trueField : falseField);
        if (columnFilter.isRequiresProfileUpdateVisible())
            csvRow.add(userCacheRecord.isRequiresProfileUpdate() ? trueField : falseField);

        if (columnFilter.isCacheTimestampVisible())
            csvRow.add(userCacheRecord.getCacheTimestamp() == null ? naField
                    : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getCacheTimestamp()));

        csvPrinter.printRecord(csvRow);
    }

    public ReportSummaryData getSummaryData() {
        return summaryData;
    }

    public static class AvgTracker {
        private final int maxSamples;
        private final Queue<BigInteger> samples = new LinkedList<>();

        public AvgTracker(int maxSamples) {
            this.maxSamples = maxSamples;
        }

        public void addSample(final long input) {
            samples.add(new BigInteger(Long.toString(input)));
            while (samples.size() > maxSamples) {
                samples.remove();
            }
        }

        public BigDecimal avg() {
            if (samples.isEmpty()) {
                throw new IllegalStateException("unable to compute avg without samples");
            }

            BigInteger total = BigInteger.ZERO;
            for (final BigInteger sample : samples) {
                total = total.add(sample);
            }
            final BigDecimal maxAsBD = new BigDecimal(Integer.toString(maxSamples));
            return new BigDecimal(total).divide(maxAsBD, MathContext.DECIMAL32);
        }

        public long avgAsLong() {
            return avg().longValue();
        }
    }

    private class DredgeTask implements Runnable {
        @Override
        public void run() {
            reportStatus.setCurrentProcess(ReportStatusInfo.ReportEngineProcess.DredgeTask);
            try {
                updateCacheFromLdap();
            } catch (Exception e) {
                if (e instanceof PwmException) {
                    if (((PwmException) e).getErrorInformation()
                            .getError() == PwmError.ERROR_DIRECTORY_UNAVAILABLE) {
                        if (executorService != null) {
                            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,
                                    "directory unavailable error during background DredgeTask, will retry; error: "
                                            + e.getMessage());
                            executorService.schedule(new DredgeTask(), 10, TimeUnit.MINUTES);
                        }
                    } else {
                        LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,
                                "error during background DredgeTask: " + e.getMessage());
                    }
                }
            } finally {
                reportStatus.setCurrentProcess(ReportStatusInfo.ReportEngineProcess.None);
            }
        }
    }

    private class RolloverTask implements Runnable {
        @Override
        public void run() {
            reportStatus.setCurrentProcess(ReportStatusInfo.ReportEngineProcess.RollOver);
            try {
                summaryData = ReportSummaryData.newSummaryData(settings.getTrackDays());
                updateRestingCacheData();
            } finally {
                reportStatus.setCurrentProcess(ReportStatusInfo.ReportEngineProcess.None);
            }
        }
    }

    private class InitializationTask implements Runnable {
        @Override
        public void run() {
            try {
                initTempData();
            } catch (LocalDBException | PwmUnrecoverableException e) {
                LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,
                        "error during initialization: " + e.getMessage());
                status = STATUS.CLOSED;
                return;
            }
            final long secondsUntilNextDredge = settings.getJobOffsetSeconds()
                    + TimeDuration.fromCurrent(Helper.nextZuluZeroTime()).getTotalSeconds();
            executorService.scheduleAtFixedRate(new DredgeTask(), secondsUntilNextDredge,
                    TimeDuration.DAY.getTotalSeconds(), TimeUnit.SECONDS);
            executorService.scheduleAtFixedRate(new RolloverTask(), secondsUntilNextDredge + 1,
                    TimeDuration.DAY.getTotalSeconds(), TimeUnit.SECONDS);
            executorService.submit(new RolloverTask());
        }
    }
}