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

Java tutorial

Introduction

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

Source

/*
 * Password Management Servlets (PWM)
 * http://code.google.com/p/pwm/
 *
 * Copyright (c) 2006-2009 Novell, Inc.
 * Copyright (c) 2009-2015 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.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.PwmConstants;
import password.pwm.PwmService;
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.i18n.LocaleHelper;
import password.pwm.ldap.UserSearchEngine;
import password.pwm.ldap.UserStatusReader;
import password.pwm.util.ClosableIterator;
import password.pwm.util.Helper;
import password.pwm.util.JsonUtil;
import password.pwm.util.TimeDuration;
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.*;

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 Timer timer;

    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() == PwmApplication.MODE.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());
        initTempData();

        reportStatus.setInProgress(false);

        timer = new Timer();

        final long nextZuluZeroTime = Helper.nextZuluZeroTime().getTime();
        if (settings.getJobOffsetSeconds() >= 0) {
            final long nextScheduleTime = nextZuluZeroTime + (settings.getJobOffsetSeconds() * 1000);
            timer.scheduleAtFixedRate(new DredgeTask(), new Date(nextScheduleTime),
                    TimeDuration.DAY.getTotalMilliseconds());
        }

        String startupMsg = "report service started with " + this.userCacheService.size() + " cached records";
        if (summaryData != null && summaryData.getMeanCacheTime() != null) {
            startupMsg += ", mean record timestamp "
                    + PwmConstants.DEFAULT_DATETIME_FORMAT.format(this.summaryData.getMeanCacheTime());
        }
        LOGGER.debug(startupMsg);
        timer.schedule(new RolloverTask(), 1);
        timer.scheduleAtFixedRate(new RolloverTask(), new Date(nextZuluZeroTime),
                TimeDuration.DAY.getTotalMilliseconds());

        status = STATUS.OPEN;
    }

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

    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 loading cached report dredge info into memory: " + e.getMessage());
        }
    }

    private void initTempData() throws LocalDBException, PwmUnrecoverableException {
        final String cleanFlag = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.REPORT_CLEAN_FLAG);
        if (!"true".equals(cleanFlag)) {
            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,
                    "did not shut down cleanly, will clear cached report data");
            clear();
            return;
        }

        try {
            final String jsonInfo = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.REPORT_STATUS);
            if (jsonInfo != null && !jsonInfo.isEmpty()) {
                reportStatus = JsonUtil.deserialize(jsonInfo, 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();
        }

        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()) {
            timer.schedule(new DredgeTask(), 1);
        }
    }

    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 long startUpdateTime = System.currentTimeMillis();
                final UserIdentity userIdentity = allUsers.poll();
                try {
                    if (updateCache(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 long totalUpdateTime = System.currentTimeMillis() - startUpdateTime;
                if (settings.isAutoCalcRest()) {
                    avgTracker.addSample(totalUpdateTime);
                    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();
        LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL, "beginning cache review process");
        final ClosableIterator<UserCacheRecord> iterator = iterator();
        int examinedRecords = 0;
        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++;
        }
        iterator.close();
        final TimeDuration totalTime = TimeDuration.fromCurrent(startTime);
        LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL, "completed cache review process of " + examinedRecords
                + " cached report records in " + totalTime.asCompactString());
    }

    public boolean updateCache(final UserInfoBean uiBean)
            throws LocalDBException, PwmUnrecoverableException, ChaiUnavailableException {
        if (status != STATUS.OPEN) {
            return false;
        }

        final UserCacheService.StorageKey storageKey = UserCacheService.StorageKey.fromUserInfoBean(uiBean);
        return updateCache(uiBean.getUserIdentity(), uiBean, storageKey);
    }

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

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

    private boolean updateCache(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();
                readerSettings.setSkipReportUpdate(true);
                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 ClosableIterator<UserCacheRecord> iterator() {
        return new RecordIterator(userCacheService.iterator());
    }

    public class RecordIterator implements ClosableIterator<UserCacheRecord> {

        private ClosableIterator<UserCacheService.StorageKey> storageKeyIterator;

        public RecordIterator(ClosableIterator<UserCacheService.StorageKey> storageKeyIterator) {
            this.storageKeyIterator = storageKeyIterator;
        }

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

        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;
        }

        public void remove() {

        }

        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 CSVPrinter csvPrinter = Helper.makeCsvPrinter(outputStream);
        final Configuration config = pwmApplication.getConfig();
        final Class localeClass = password.pwm.i18n.Admin.class;
        if (includeHeader) {
            final List<String> headerRow = new ArrayList<>();
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_UserDN", config, localeClass));
            headerRow.add(
                    LocaleHelper.getLocalizedMessage(locale, "Field_Report_LDAP_Profile", config, localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_Username", config, localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_Email", config, localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_UserGuid", config, localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_LastLogin", config, localeClass));
            headerRow.add(
                    LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdExpireTime", config, localeClass));
            headerRow.add(
                    LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdChangeTime", config, localeClass));
            headerRow.add(
                    LocaleHelper.getLocalizedMessage(locale, "Field_Report_ResponseSaveTime", config, localeClass));
            headerRow.add(
                    LocaleHelper.getLocalizedMessage(locale, "Field_Report_HasResponses", config, localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_HasHelpdeskResponses", config,
                    localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_ResponseStorageMethod", config,
                    localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdExpired", config, localeClass));
            headerRow.add(
                    LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdPreExpired", config, localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdViolatesPolicy", config,
                    localeClass));
            headerRow.add(
                    LocaleHelper.getLocalizedMessage(locale, "Field_Report_PwdWarnPeriod", config, localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_RequiresPasswordUpdate", config,
                    localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_RequiresResponseUpdate", config,
                    localeClass));
            headerRow.add(LocaleHelper.getLocalizedMessage(locale, "Field_Report_RequiresProfileUpdate", config,
                    localeClass));
            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);
            }
        } finally {
            if (cacheBeanIterator != null) {
                cacheBeanIterator.close();
            }
        }

        csvPrinter.flush();
    }

    private void outputRecordRow(final Configuration config, final Locale locale,
            final UserCacheRecord userCacheRecord, final CSVPrinter csvPrinter) 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<>();
        csvRow.add(userCacheRecord.getUserDN());
        csvRow.add(userCacheRecord.getLdapProfile());
        csvRow.add(userCacheRecord.getUsername());
        csvRow.add(userCacheRecord.getEmail());
        csvRow.add(userCacheRecord.getUserGUID());
        csvRow.add(userCacheRecord.getLastLoginTime() == null ? naField
                : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getLastLoginTime()));
        csvRow.add(userCacheRecord.getPasswordExpirationTime() == null ? naField
                : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getPasswordExpirationTime()));
        csvRow.add(userCacheRecord.getPasswordChangeTime() == null ? naField
                : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getPasswordChangeTime()));
        csvRow.add(userCacheRecord.getResponseSetTime() == null ? naField
                : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getResponseSetTime()));
        csvRow.add(userCacheRecord.isHasResponses() ? trueField : falseField);
        csvRow.add(userCacheRecord.isHasHelpdeskResponses() ? trueField : falseField);
        csvRow.add(userCacheRecord.getResponseStorageMethod() == null ? naField
                : userCacheRecord.getResponseStorageMethod().toString());
        csvRow.add(userCacheRecord.getPasswordStatus().isExpired() ? trueField : falseField);
        csvRow.add(userCacheRecord.getPasswordStatus().isPreExpired() ? trueField : falseField);
        csvRow.add(userCacheRecord.getPasswordStatus().isViolatesPolicy() ? trueField : falseField);
        csvRow.add(userCacheRecord.getPasswordStatus().isWarnPeriod() ? trueField : falseField);
        csvRow.add(userCacheRecord.isRequiresPasswordUpdate() ? trueField : falseField);
        csvRow.add(userCacheRecord.isRequiresResponseUpdate() ? trueField : falseField);
        csvRow.add(userCacheRecord.isRequiresProfileUpdate() ? trueField : falseField);
        csvRow.add(userCacheRecord.getCacheTimestamp() == null ? naField
                : PwmConstants.DEFAULT_DATETIME_FORMAT.format(userCacheRecord.getCacheTimestamp()));

        csvPrinter.printRecord(csvRow);
    }

    public ReportSummaryData getSummaryData() {
        return summaryData;
    }

    private class DredgeTask extends TimerTask {
        @Override
        public void run() {
            try {
                updateCacheFromLdap();
            } catch (Exception e) {
                if (e instanceof PwmException) {
                    if (((PwmException) e).getErrorInformation()
                            .getError() == PwmError.ERROR_DIRECTORY_UNAVAILABLE) {
                        if (timer != null) {
                            timer.schedule(new DredgeTask(), 10 * 60 * 1000);
                        }
                    }
                }
                LOGGER.warn(PwmConstants.REPORTING_SESSION_LABEL, "unable to dredge ldap for ");
            }
        }
    }

    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 RolloverTask extends TimerTask {
        public void run() {
            summaryData = ReportSummaryData.newSummaryData(settings.getTrackDays());
            updateRestingCacheData();
        }
    }

    public int recordsInCache() {
        if (userCacheService != null && userCacheService.status() == STATUS.OPEN) {
            try {
                return userCacheService.size();
            } catch (LocalDBException e) {
                LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,
                        "error reading user cache service size: " + e.getMessage());
            }
        }
        return 0;
    }

}