diff --git a/src/main/java/mil/dds/anet/AnetApplication.java b/src/main/java/mil/dds/anet/AnetApplication.java index fd71e0a09d..3ed7c78f6f 100644 --- a/src/main/java/mil/dds/anet/AnetApplication.java +++ b/src/main/java/mil/dds/anet/AnetApplication.java @@ -278,9 +278,10 @@ public void run(AnetConfiguration configuration, Environment environment) } private void runAccountDeactivationWorker(final AnetConfiguration configuration, - final ScheduledExecutorService scheduler, final AnetObjectEngine engine) - throws IllegalArgumentException { - // Check whether the application is configured to auto-check for account deactivation + final ScheduledExecutorService scheduler, final AnetObjectEngine engine) { + // Check whether the application is configured to auto-check for account deactivation. + // NOTE: if you change this, reloading the dictionary from the admin interface is *not* + // sufficient, you will have to restart ANET for this change to be reflected if (configuration.getDictionaryEntry("automaticallyInactivateUsers") != null) { // Check for any accounts which are scheduled to be deactivated as they reach the end-of-tour // date. @@ -289,7 +290,7 @@ private void runAccountDeactivationWorker(final AnetConfiguration configuration, final AccountDeactivationWorker deactivationWarningWorker = new AccountDeactivationWorker( configuration, engine.getPersonDao(), accountDeactivationWarningInterval); - // Run the email deactivation worker at the set interval. In development run it every minute. + // Run the email deactivation worker at the set interval scheduler.scheduleAtFixedRate(deactivationWarningWorker, accountDeactivationWarningInterval, accountDeactivationWarningInterval, TimeUnit.SECONDS); diff --git a/src/main/java/mil/dds/anet/resources/ReportResource.java b/src/main/java/mil/dds/anet/resources/ReportResource.java index 2a6503f60f..45f902be0c 100644 --- a/src/main/java/mil/dds/anet/resources/ReportResource.java +++ b/src/main/java/mil/dds/anet/resources/ReportResource.java @@ -79,28 +79,10 @@ public class ReportResource { private final AnetObjectEngine engine; private final AnetConfiguration config; - private final RollupGraphComparator rollupGraphComparator; - private final DateTimeFormatter dtf; - private final boolean engagementsIncludeTimeAndDuration; - private final DateTimeFormatter edtf; - public ReportResource(AnetObjectEngine engine, AnetConfiguration config) { this.engine = engine; this.dao = engine.getReportDao(); this.config = config; - this.dtf = DateTimeFormatter - .ofPattern((String) this.config.getDictionaryEntry("dateFormats.email.date")) - .withZone(DaoUtils.getDefaultZoneId()); - engagementsIncludeTimeAndDuration = Boolean.TRUE - .equals((Boolean) this.config.getDictionaryEntry("engagementsIncludeTimeAndDuration")); - final String edtfPattern = (String) this.config - .getDictionaryEntry(engagementsIncludeTimeAndDuration ? "dateFormats.email.withTime" - : "dateFormats.email.date"); - this.edtf = DateTimeFormatter.ofPattern(edtfPattern).withZone(DaoUtils.getDefaultZoneId()); - @SuppressWarnings("unchecked") - List pinnedOrgNames = (List) this.config.getDictionaryEntry("pinned_ORGs"); - this.rollupGraphComparator = new RollupGraphComparator(pinnedOrgNames); - } @GraphQLQuery(name = "report") @@ -736,7 +718,7 @@ public List getDailyRollupGraph(@GraphQLArgument(name = "startDate" dailyRollupGraph = dao.getDailyRollupGraph(startDate, endDate, orgType, nonReportingOrgs); } - Collections.sort(dailyRollupGraph, rollupGraphComparator); + Collections.sort(dailyRollupGraph, getRollupGraphComparator()); return dailyRollupGraph; } @@ -776,10 +758,7 @@ public String showRollupEmail(@GraphQLArgument(name = "startDate") Long start, action.setAdvisorOrganizationUuid(advisorOrgUuid); action.setPrincipalOrganizationUuid(principalOrgUuid); - @SuppressWarnings("unchecked") - final Map fields = (Map) config.getDictionaryEntry("fields"); - - Map context = new HashMap(); + final Map context = new HashMap(); context.put("context", engine.getContext()); context.put("serverUrl", config.getServerUrl()); context.put(AdminSettingKeys.SECURITY_BANNER_TEXT.name(), @@ -787,10 +766,7 @@ public String showRollupEmail(@GraphQLArgument(name = "startDate") Long start, context.put(AdminSettingKeys.SECURITY_BANNER_COLOR.name(), engine.getAdminSetting(AdminSettingKeys.SECURITY_BANNER_COLOR)); context.put(DailyRollupEmail.SHOW_REPORT_TEXT_FLAG, showReportText); - context.put("dateFormatter", dtf); - context.put("engagementsIncludeTimeAndDuration", engagementsIncludeTimeAndDuration); - context.put("engagementDateFormatter", edtf); - context.put("fields", fields); + addConfigToContext(context); try { Configuration freemarkerConfig = new Configuration(FREEMARKER_VERSION); @@ -974,4 +950,27 @@ private void updateAssessment(Note newAssessment, Note oldAssessment) { noteDao.update(newAssessment); } } + + private void addConfigToContext(Map context) { + context.put("dateFormatter", + DateTimeFormatter.ofPattern((String) config.getDictionaryEntry("dateFormats.email.date")) + .withZone(DaoUtils.getDefaultZoneId())); + context.put("engagementsIncludeTimeAndDuration", Boolean.TRUE + .equals((Boolean) config.getDictionaryEntry("engagementsIncludeTimeAndDuration"))); + final String edtfPattern = (String) config.getDictionaryEntry(Boolean.TRUE + .equals((Boolean) config.getDictionaryEntry("engagementsIncludeTimeAndDuration")) + ? "dateFormats.email.withTime" + : "dateFormats.email.date"); + context.put("engagementDateFormatter", + DateTimeFormatter.ofPattern(edtfPattern).withZone(DaoUtils.getDefaultZoneId())); + @SuppressWarnings("unchecked") + final Map fields = (Map) config.getDictionaryEntry("fields"); + context.put("fields", fields); + } + + private RollupGraphComparator getRollupGraphComparator() { + @SuppressWarnings("unchecked") + final List pinnedOrgNames = (List) config.getDictionaryEntry("pinned_ORGs"); + return new RollupGraphComparator(pinnedOrgNames); + } } diff --git a/src/main/java/mil/dds/anet/resources/TaskResource.java b/src/main/java/mil/dds/anet/resources/TaskResource.java index e3c38f9343..c070f6f98c 100644 --- a/src/main/java/mil/dds/anet/resources/TaskResource.java +++ b/src/main/java/mil/dds/anet/resources/TaskResource.java @@ -31,13 +31,12 @@ public class TaskResource { private final AnetObjectEngine engine; private final TaskDao dao; - private final String duplicateTaskShortName; + private final AnetConfiguration config; public TaskResource(AnetObjectEngine engine, AnetConfiguration config) { this.engine = engine; this.dao = engine.getTaskDao(); - final String taskShortLabel = (String) config.getDictionaryEntry("fields.task.shortLabel"); - duplicateTaskShortName = String.format("Duplicate %s number", taskShortLabel); + this.config = config; } @GraphQLQuery(name = "task") @@ -59,7 +58,7 @@ public Task createTask(@GraphQLRootContext Map context, try { created = dao.insert(t); } catch (UnableToExecuteStatementException e) { - throw ResponseUtils.handleSqlException(e, duplicateTaskShortName); + throw createDuplicateException(e); } if (t.getPlanningApprovalSteps() != null) { // Create the planning approval steps @@ -156,7 +155,7 @@ public Integer updateTask(@GraphQLRootContext Map context, // GraphQL mutations *have* to return something, so we return the number of updated rows return numRows; } catch (UnableToExecuteStatementException e) { - throw ResponseUtils.handleSqlException(e, duplicateTaskShortName); + throw createDuplicateException(e); } } @@ -166,4 +165,10 @@ public AnetBeanList search(@GraphQLRootContext Map context query.setUser(DaoUtils.getUserFromContext(context)); return dao.search(query); } + + private WebApplicationException createDuplicateException(UnableToExecuteStatementException e) { + final String taskShortLabel = (String) config.getDictionaryEntry("fields.task.shortLabel"); + return ResponseUtils.handleSqlException(e, + String.format("Duplicate %s number", taskShortLabel)); + } } diff --git a/src/main/java/mil/dds/anet/threads/AccountDeactivationWorker.java b/src/main/java/mil/dds/anet/threads/AccountDeactivationWorker.java index 167aa584fc..d841a804b0 100644 --- a/src/main/java/mil/dds/anet/threads/AccountDeactivationWorker.java +++ b/src/main/java/mil/dds/anet/threads/AccountDeactivationWorker.java @@ -22,10 +22,6 @@ public class AccountDeactivationWorker extends AbstractWorker { private final PersonDao dao; - - private final List daysTillEndOfTourWarnings; - private final List ignoredDomainNames; - private final int warningIntervalInSecs; public AccountDeactivationWorker(AnetConfiguration config, PersonDao dao, @@ -33,25 +29,14 @@ public AccountDeactivationWorker(AnetConfiguration config, PersonDao dao, super(config, "Deactivation Warning Worker waking up to check for Future Account Deactivations"); this.dao = dao; - - @SuppressWarnings("unchecked") - final List daysTillWarning = (List) config - .getDictionaryEntry("automaticallyInactivateUsers.emailRemindersDaysPrior"); - this.daysTillEndOfTourWarnings = - daysTillWarning.stream().filter(i -> i > 0).collect(Collectors.toList()); - - @SuppressWarnings("unchecked") - List domainNamesToIgnore = - (List) config.getDictionaryEntry("automaticallyInactivateUsers.ignoredDomainNames"); - this.ignoredDomainNames = domainNamesToIgnore == null ? domainNamesToIgnore - : domainNamesToIgnore.stream().map(x -> x.trim()).collect(Collectors.toList()); - this.warningIntervalInSecs = warningIntervalInSecs; } @Override protected void runInternal(Instant now, JobHistory jobHistory) { // Make sure the mechanism will be triggered, so account deactivation checking can take place + final List ignoredDomainNames = getDomainNamesToIgnore(); + final List daysTillEndOfTourWarnings = getDaysTillEndOfTourWarnings(); final List warningDays = (daysTillEndOfTourWarnings == null || daysTillEndOfTourWarnings.isEmpty()) ? new ArrayList<>(0) @@ -79,15 +64,32 @@ protected void runInternal(Instant now, JobHistory jobHistory) { final Integer warning = warningDays.get(i); final Integer nextWarning = i == warningDays.size() - 1 ? null : warningDays.get(i + 1); checkDeactivationStatus(p, warning, nextWarning, now, - jobHistory == null ? null : jobHistory.getLastRun()); + jobHistory == null ? null : jobHistory.getLastRun(), ignoredDomainNames, + warningIntervalInSecs); } }); } + private List getDaysTillEndOfTourWarnings() { + @SuppressWarnings("unchecked") + final List daysTillWarning = (List) config + .getDictionaryEntry("automaticallyInactivateUsers.emailRemindersDaysPrior"); + return daysTillWarning.stream().filter(i -> i > 0).collect(Collectors.toList()); + } + + private List getDomainNamesToIgnore() { + @SuppressWarnings("unchecked") + final List domainNamesToIgnore = + (List) config.getDictionaryEntry("automaticallyInactivateUsers.ignoredDomainNames"); + return domainNamesToIgnore == null ? null + : domainNamesToIgnore.stream().map(x -> x.trim()).collect(Collectors.toList()); + } + private void checkDeactivationStatus(final Person person, final Integer daysBeforeWarning, - final Integer nextWarning, final Instant now, final Instant lastRun) { + final Integer nextWarning, final Instant now, final Instant lastRun, + final List ignoredDomainNames, final Integer warningIntervalInSecs) { if (person.getStatus() == Person.Status.INACTIVE - || Utils.isDomainUserNameIgnored(person.getDomainUsername(), this.ignoredDomainNames)) { + || Utils.isDomainUserNameIgnored(person.getDomainUsername(), ignoredDomainNames)) { // Skip inactive ANET users or users from ignored domains return; } @@ -100,7 +102,7 @@ private void checkDeactivationStatus(final Person person, final Integer daysBefo final Instant warningDate = now.plus(daysBeforeWarning, ChronoUnit.DAYS); final Instant prevWarningDate = - (lastRun == null) ? warningDate.minus(this.warningIntervalInSecs, ChronoUnit.SECONDS) + (lastRun == null) ? warningDate.minus(warningIntervalInSecs, ChronoUnit.SECONDS) : lastRun.plus(daysBeforeWarning, ChronoUnit.DAYS); if (person.getEndOfTourDate().isBefore(warningDate) && person.getEndOfTourDate().isAfter(prevWarningDate)) { diff --git a/src/main/java/mil/dds/anet/threads/AnetEmailWorker.java b/src/main/java/mil/dds/anet/threads/AnetEmailWorker.java index ecdb1d1a7d..970b47b47a 100644 --- a/src/main/java/mil/dds/anet/threads/AnetEmailWorker.java +++ b/src/main/java/mil/dds/anet/threads/AnetEmailWorker.java @@ -46,69 +46,27 @@ public class AnetEmailWorker extends AbstractWorker { private static AnetEmailWorker instance; - private EmailDao dao; - private ObjectMapper mapper; - private Properties props; - private Authenticator auth; - private String fromAddr; - private String serverUrl; - private final Map fields; - private Configuration freemarkerConfig; - private final String supportEmailAddr; - private final DateTimeFormatter dtf; - private final DateTimeFormatter dttf; - private final boolean engagementsIncludeTimeAndDuration; - private final DateTimeFormatter edtf; - private final Integer nbOfHoursForStaleEmails; - private final boolean disabled; - private final List activeDomainNames; - - @SuppressWarnings("unchecked") + private final EmailDao dao; + private final ObjectMapper mapper; + private final String fromAddr; + private final String serverUrl; + private final SmtpConfiguration smtpConfig; + private final Properties smtpProps; + private final Authenticator smtpAuth; + private final Configuration freemarkerConfig; + public AnetEmailWorker(AnetConfiguration config, EmailDao dao) { super(config, "AnetEmailWorker waking up to send emails!"); this.dao = dao; this.mapper = MapperUtils.getDefaultMapper(); - this.fromAddr = config.getEmailFromAddr(); - this.serverUrl = config.getServerUrl(); - this.supportEmailAddr = (String) config.getDictionaryEntry("SUPPORT_EMAIL_ADDR"); - this.dtf = - DateTimeFormatter.ofPattern((String) config.getDictionaryEntry("dateFormats.email.date")) - .withZone(DaoUtils.getDefaultZoneId()); - this.dttf = DateTimeFormatter - .ofPattern((String) config.getDictionaryEntry("dateFormats.email.withTime")) - .withZone(DaoUtils.getDefaultZoneId()); - engagementsIncludeTimeAndDuration = Boolean.TRUE - .equals((Boolean) config.getDictionaryEntry("engagementsIncludeTimeAndDuration")); - final String edtfPattern = (String) config - .getDictionaryEntry(engagementsIncludeTimeAndDuration ? "dateFormats.email.withTime" - : "dateFormats.email.date"); - this.edtf = DateTimeFormatter.ofPattern(edtfPattern).withZone(DaoUtils.getDefaultZoneId()); - this.fields = (Map) config.getDictionaryEntry("fields"); - this.activeDomainNames = ((List) config.getDictionaryEntry("activeDomainNames")) - .stream().map(String::toLowerCase).collect(Collectors.toList()); setInstance(this); - SmtpConfiguration smtpConfig = config.getSmtp(); - props = new Properties(); - props.put("mail.smtp.ssl.trust", smtpConfig.getSslTrust()); - props.put("mail.smtp.starttls.enable", smtpConfig.getStartTls().toString()); - props.put("mail.smtp.host", smtpConfig.getHostname()); - props.put("mail.smtp.port", smtpConfig.getPort().toString()); - auth = null; - this.nbOfHoursForStaleEmails = smtpConfig.getNbOfHoursForStaleEmails(); - - if (smtpConfig.getUsername() != null && smtpConfig.getUsername().trim().length() > 0) { - props.put("mail.smtp.auth", "true"); - auth = new javax.mail.Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(smtpConfig.getUsername(), smtpConfig.getPassword()); - } - }; - } - - disabled = smtpConfig.isDisabled(); + this.fromAddr = config.getEmailFromAddr(); + this.serverUrl = config.getServerUrl(); + this.smtpConfig = config.getSmtp(); + this.smtpProps = getSmtpProps(smtpConfig); + this.smtpAuth = getSmtpAuth(smtpConfig); freemarkerConfig = new Configuration(FREEMARKER_VERSION); // auto-escape HTML in our .ftlh templates @@ -126,6 +84,11 @@ public static void setInstance(AnetEmailWorker instance) { @Override protected void runInternal(Instant now, JobHistory jobHistory) { + @SuppressWarnings("unchecked") + final List activeDomainNames = + ((List) config.getDictionaryEntry("activeDomainNames")).stream() + .map(String::toLowerCase).collect(Collectors.toList()); + // check the database for any emails we need to send. final List emails = dao.getAll(); @@ -135,13 +98,13 @@ protected void runInternal(Instant now, JobHistory jobHistory) { Map context = null; try { - context = buildContext(email); + context = buildContext(email, smtpConfig); if (context != null) { - logger.info("{} Sending email to {} re: {}", disabled ? "[Disabled] " : "", + logger.info("{} Sending email to {} re: {}", smtpConfig.isDisabled() ? "[Disabled] " : "", email.getToAddresses(), email.getAction().getSubject(context)); - if (!disabled) { - sendEmail(email, context); + if (!smtpConfig.isDisabled()) { + sendEmail(email, context, smtpProps, smtpAuth, activeDomainNames); } } processedEmails.add(email.getId()); @@ -149,8 +112,9 @@ protected void runInternal(Instant now, JobHistory jobHistory) { logger.error("Error sending email", t); // Process stale emails - if (this.nbOfHoursForStaleEmails != null) { - final Instant staleTime = now.minus(nbOfHoursForStaleEmails, ChronoUnit.HOURS); + if (smtpConfig.getNbOfHoursForStaleEmails() != null) { + final Instant staleTime = + now.minus(smtpConfig.getNbOfHoursForStaleEmails(), ChronoUnit.HOURS); if (email.getCreatedAt().isBefore(staleTime)) { String message = "Purging stale email to "; try { @@ -169,7 +133,8 @@ protected void runInternal(Instant now, JobHistory jobHistory) { dao.deletePendingEmails(processedEmails); } - private Map buildContext(final AnetEmail email) { + private Map buildContext(final AnetEmail email, + final SmtpConfiguration smtpConfig) { AnetObjectEngine engine = AnetObjectEngine.getInstance(); Map context = new HashMap(); context.put("context", engine.getContext()); @@ -178,17 +143,32 @@ private Map buildContext(final AnetEmail email) { engine.getAdminSetting(AdminSettingKeys.SECURITY_BANNER_TEXT)); context.put(AdminSettingKeys.SECURITY_BANNER_COLOR.name(), engine.getAdminSetting(AdminSettingKeys.SECURITY_BANNER_COLOR)); - context.put("SUPPORT_EMAIL_ADDR", supportEmailAddr); - context.put("dateFormatter", dtf); - context.put("dateTimeFormatter", dttf); + context.put("SUPPORT_EMAIL_ADDR", config.getDictionaryEntry("SUPPORT_EMAIL_ADDR")); + context.put("dateFormatter", + DateTimeFormatter.ofPattern((String) config.getDictionaryEntry("dateFormats.email.date")) + .withZone(DaoUtils.getDefaultZoneId())); + context.put("dateTimeFormatter", + DateTimeFormatter + .ofPattern((String) config.getDictionaryEntry("dateFormats.email.withTime")) + .withZone(DaoUtils.getDefaultZoneId())); + final boolean engagementsIncludeTimeAndDuration = Boolean.TRUE + .equals((Boolean) config.getDictionaryEntry("engagementsIncludeTimeAndDuration")); context.put("engagementsIncludeTimeAndDuration", engagementsIncludeTimeAndDuration); - context.put("engagementDateFormatter", edtf); + final String edtfPattern = (String) config + .getDictionaryEntry(engagementsIncludeTimeAndDuration ? "dateFormats.email.withTime" + : "dateFormats.email.date"); + context.put("engagementDateFormatter", + DateTimeFormatter.ofPattern(edtfPattern).withZone(DaoUtils.getDefaultZoneId())); + @SuppressWarnings("unchecked") + final Map fields = (Map) config.getDictionaryEntry("fields"); context.put("fields", fields); return email.getAction().buildContext(context); } - private void sendEmail(final AnetEmail email, final Map context) + private void sendEmail(final AnetEmail email, final Map context, + final Properties smtpProps, final Authenticator smtpAuth, + final List activeDomainNames) throws MessagingException, IOException, TemplateException { // Remove any null email addresses email.getToAddresses().removeIf(s -> Objects.equals(s, null)); @@ -206,7 +186,7 @@ private void sendEmail(final AnetEmail email, final Map context) // scan:ignore — false positive, we know which template we are processing temp.process(context, writer); - final Session session = Session.getInstance(props, auth); + final Session session = Session.getInstance(smtpProps, smtpAuth); final Email mail = EmailBuilder.startingBlank().from(new InternetAddress(fromAddr)) .toMultiple(email.getToAddresses()).withSubject(email.getAction().getSubject(context)) .withHTMLText(writer.toString()).buildEmail(); @@ -239,4 +219,32 @@ private synchronized void internal_sendEmailAsync(AnetEmail email) { throw new WebApplicationException(jsonError); } } + + private Properties getSmtpProps(SmtpConfiguration smtpConfig) { + final Properties props = new Properties(); + props.put("mail.smtp.ssl.trust", smtpConfig.getSslTrust()); + props.put("mail.smtp.starttls.enable", smtpConfig.getStartTls().toString()); + props.put("mail.smtp.host", smtpConfig.getHostname()); + props.put("mail.smtp.port", smtpConfig.getPort().toString()); + if (hasUsername(smtpConfig)) { + props.put("mail.smtp.auth", "true"); + } + return props; + } + + private Authenticator getSmtpAuth(SmtpConfiguration smtpConfig) { + if (hasUsername(smtpConfig)) { + return new javax.mail.Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(smtpConfig.getUsername(), smtpConfig.getPassword()); + } + }; + } + return null; + } + + private boolean hasUsername(SmtpConfiguration smtpConfig) { + return smtpConfig.getUsername() != null && smtpConfig.getUsername().trim().length() > 0; + } } diff --git a/src/main/java/mil/dds/anet/threads/ReportApprovalWorker.java b/src/main/java/mil/dds/anet/threads/ReportApprovalWorker.java index fd07a2d55d..39405a246b 100644 --- a/src/main/java/mil/dds/anet/threads/ReportApprovalWorker.java +++ b/src/main/java/mil/dds/anet/threads/ReportApprovalWorker.java @@ -20,18 +20,17 @@ public class ReportApprovalWorker extends AbstractWorker { private final ReportDao dao; - private final Integer nbOfHoursApprovalTimeout; public ReportApprovalWorker(AnetConfiguration config, ReportDao dao) { super(config, "Report Approval Worker waking up to check for reports to be approved"); this.dao = dao; - this.nbOfHoursApprovalTimeout = - (Integer) config.getDictionaryEntry("reportWorkflow.nbOfHoursApprovalTimeout"); } @Override protected void runInternal(Instant now, JobHistory jobHistory) { - final Instant approvalTimeout = now.minus(nbOfHoursApprovalTimeout, ChronoUnit.HOURS); + final Instant approvalTimeout = + now.minus((Integer) config.getDictionaryEntry("reportWorkflow.nbOfHoursApprovalTimeout"), + ChronoUnit.HOURS); // Get a list of all PENDING_APPROVAL reports final ReportSearchQuery query = new ReportSearchQuery(); query.setPageSize(0); diff --git a/src/main/java/mil/dds/anet/threads/ReportPublicationWorker.java b/src/main/java/mil/dds/anet/threads/ReportPublicationWorker.java index 634614f78d..86f65f2ec4 100644 --- a/src/main/java/mil/dds/anet/threads/ReportPublicationWorker.java +++ b/src/main/java/mil/dds/anet/threads/ReportPublicationWorker.java @@ -18,19 +18,17 @@ public class ReportPublicationWorker extends AbstractWorker { private final ReportDao dao; - private final Integer nbOfHoursQuarantineApproved; public ReportPublicationWorker(AnetConfiguration config, ReportDao dao) { super(config, "Report Publication Worker waking up to check for reports to be published"); this.dao = dao; - this.nbOfHoursQuarantineApproved = - (Integer) config.getDictionaryEntry("reportWorkflow.nbOfHoursQuarantineApproved"); } @Override protected void runInternal(Instant now, JobHistory jobHistory) { final Instant quarantineApproval = - now.minus(this.nbOfHoursQuarantineApproved, ChronoUnit.HOURS); + now.minus((Integer) config.getDictionaryEntry("reportWorkflow.nbOfHoursQuarantineApproved"), + ChronoUnit.HOURS); // Get a list of all APPROVED reports final ReportSearchQuery query = new ReportSearchQuery(); query.setPageSize(0);