[ https://issues.apache.org/jira/browse/FINERACT-65?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=15405690#comment-15405690 ]
ASF GitHub Bot commented on FINERACT-65: ---------------------------------------- Github user nazeer1100126 commented on a diff in the pull request: https://github.com/apache/incubator-fineract/pull/191#discussion_r73314684 --- Diff: fineract-provider/src/main/java/org/apache/fineract/infrastructure/reportmailingjob/service/ReportMailingJobWritePlatformServiceImpl.java --- @@ -0,0 +1,495 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.reportmailingjob.service; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.dataqueries.domain.Report; +import org.apache.fineract.infrastructure.dataqueries.domain.ReportRepositoryWrapper; +import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService; +import org.apache.fineract.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository; +import org.apache.fineract.infrastructure.jobs.annotation.CronTarget; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.infrastructure.report.provider.ReportingProcessServiceProvider; +import org.apache.fineract.infrastructure.report.service.ReportingProcessService; +import org.apache.fineract.infrastructure.reportmailingjob.ReportMailingJobConstants; +import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobEmailAttachmentFileFormat; +import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobEmailData; +import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobPreviousRunStatus; +import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobStretchyReportParamDateOption; +import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJob; +import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRepository; +import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRepositoryWrapper; +import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRunHistory; +import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRunHistoryRepository; +import org.apache.fineract.infrastructure.reportmailingjob.util.ReportMailingJobDateUtil; +import org.apache.fineract.infrastructure.reportmailingjob.validation.ReportMailingJobValidator; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.calendar.service.CalendarUtils; +import org.apache.fineract.useradministration.domain.AppUser; +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.sun.jersey.core.util.MultivaluedMapImpl; + +@Service +public class ReportMailingJobWritePlatformServiceImpl implements ReportMailingJobWritePlatformService { + + private final static Logger logger = LoggerFactory.getLogger(ReportMailingJobWritePlatformServiceImpl.class); + private final ReportRepositoryWrapper reportRepositoryWrapper; + private final ReportMailingJobValidator reportMailingJobValidator; + private final ReportMailingJobRepositoryWrapper reportMailingJobRepositoryWrapper; + private final ReportMailingJobRepository reportMailingJobRepository; + private final PlatformSecurityContext platformSecurityContext; + private final ReportMailingJobEmailService reportMailingJobEmailService; + private final ReadReportingService readReportingService; + private final ReportingProcessServiceProvider reportingProcessServiceProvider; + private final ReportMailingJobRunHistoryRepository reportMailingJobRunHistoryRepository; + private final static String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + @Autowired + public ReportMailingJobWritePlatformServiceImpl(final ReportRepositoryWrapper reportRepositoryWrapper, + final ReportMailingJobValidator reportMailingJobValidator, + final ReportMailingJobRepositoryWrapper reportMailingJobRepositoryWrapper, + final ReportMailingJobRepository reportMailingJobRepository, + final PlatformSecurityContext platformSecurityContext, + final ReportMailingJobEmailService reportMailingJobEmailService, + final ReadReportingService readReportingService, + final ReportMailingJobRunHistoryRepository reportMailingJobRunHistoryRepository, + final ReportingProcessServiceProvider reportingProcessServiceProvider) { + this.reportRepositoryWrapper = reportRepositoryWrapper; + this.reportMailingJobValidator = reportMailingJobValidator; + this.reportMailingJobRepositoryWrapper = reportMailingJobRepositoryWrapper; + this.reportMailingJobRepository = reportMailingJobRepositoryWrapper.getReportMailingJobRepository(); + this.platformSecurityContext = platformSecurityContext; + this.reportMailingJobEmailService = reportMailingJobEmailService; + this.readReportingService = readReportingService; + this.reportMailingJobRunHistoryRepository = reportMailingJobRunHistoryRepository; + this.reportingProcessServiceProvider = reportingProcessServiceProvider; + } + + @Override + @Transactional + public CommandProcessingResult createReportMailingJob(JsonCommand jsonCommand) { + try { + // validate the create request + this.reportMailingJobValidator.validateCreateRequest(jsonCommand); + + final AppUser appUser = this.platformSecurityContext.authenticatedUser(); + + // get the stretchy Report object + final Report stretchyReport = this.reportRepositoryWrapper.findOneThrowExceptionIfNotFound(jsonCommand.longValueOfParameterNamed( + ReportMailingJobConstants.STRETCHY_REPORT_ID_PARAM_NAME)); + + // create an instance of ReportMailingJob class from the JsonCommand object + final ReportMailingJob reportMailingJob = ReportMailingJob.newInstance(jsonCommand, stretchyReport, new LocalDate(), appUser, appUser); + + // save entity + this.reportMailingJobRepository.save(reportMailingJob); + + return new CommandProcessingResultBuilder().withCommandId(jsonCommand.commandId()). + withEntityId(reportMailingJob.getId()).build(); + } catch (final DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve); + + return CommandProcessingResult.empty(); + } + } + + @Override + @Transactional + public CommandProcessingResult updateReportMailingJob(Long reportMailingJobId, JsonCommand jsonCommand) { + try { + // validate the update request + this.reportMailingJobValidator.validateUpdateRequest(jsonCommand); + + // retrieve the ReportMailingJob object from the database + final ReportMailingJob reportMailingJob = this.reportMailingJobRepositoryWrapper.findOneThrowExceptionIfNotFound(reportMailingJobId); + + final Map<String, Object> changes = reportMailingJob.update(jsonCommand); + + // get the recurrence rule string + final String recurrence = reportMailingJob.getRecurrence(); + + // get the next run DateTime from the ReportMailingJob entity + DateTime nextRunDateTime = reportMailingJob.getNextRunDateTime(); + + // check if the stretchy report id was updated + if (changes.containsKey(ReportMailingJobConstants.STRETCHY_REPORT_ID_PARAM_NAME)) { + final Long stretchyReportId = (Long) changes.get(ReportMailingJobConstants.STRETCHY_REPORT_ID_PARAM_NAME); + final Report stretchyReport = this.reportRepositoryWrapper.findOneThrowExceptionIfNotFound(stretchyReportId); + + // update the stretchy report + reportMailingJob.update(stretchyReport); + } + + // check if the recurrence was updated + if (changes.containsKey(ReportMailingJobConstants.RECURRENCE_PARAM_NAME)) { + + // go ahead if the recurrence is not null + if (StringUtils.isNotBlank(recurrence)) { + // set the start DateTime to the current tenant date time + DateTime startDateTime = DateUtils.getLocalDateTimeOfTenant().toDateTime(); + + // check if the start DateTime was updated + if (changes.containsKey(ReportMailingJobConstants.START_DATE_TIME_PARAM_NAME)) { + // get the updated start DateTime + startDateTime = reportMailingJob.getStartDateTime(); + } + + startDateTime = reportMailingJob.getStartDateTime(); + + // get the next recurring DateTime + final DateTime nextRecurringDateTime = this.createNextRecurringDateTime(recurrence, startDateTime); + + // update the next run time property + reportMailingJob.updateNextRunDateTime(nextRecurringDateTime); + + // check if the next run DateTime is not empty and the recurrence is empty + } else if (StringUtils.isBlank(recurrence) && (nextRunDateTime != null)) { + // the next run DateTime should be set to null + reportMailingJob.updateNextRunDateTime(null); + } + } + + if (changes.containsKey(ReportMailingJobConstants.START_DATE_TIME_PARAM_NAME)) { + final DateTime startDateTime = reportMailingJob.getStartDateTime(); + + // initially set the next recurring date time to the new start date time + DateTime nextRecurringDateTime = startDateTime; + + // ensure that the recurrence pattern string is not empty + if (StringUtils.isNotBlank(recurrence)) { + // get the next recurring DateTime + nextRecurringDateTime = this.createNextRecurringDateTime(recurrence, startDateTime); + } + + // update the next run time property + reportMailingJob.updateNextRunDateTime(nextRecurringDateTime); + } + + if (!changes.isEmpty()) { + // save and flush immediately so any data integrity exception can be handled in the "catch" block + this.reportMailingJobRepository.saveAndFlush(reportMailingJob); + } + + return new CommandProcessingResultBuilder(). + withCommandId(jsonCommand.commandId()). + withEntityId(reportMailingJob.getId()). + with(changes). + build(); + } catch (final DataIntegrityViolationException dve) { + handleDataIntegrityIssues(jsonCommand, dve); + + return CommandProcessingResult.empty(); + } + } + + @Override + @Transactional + public CommandProcessingResult deleteReportMailingJob(Long reportMailingJobId) { + // retrieve the ReportMailingJob object from the database + final ReportMailingJob reportMailingJob = this.reportMailingJobRepositoryWrapper.findOneThrowExceptionIfNotFound(reportMailingJobId); + + // delete the report mailing job by setting the isDeleted property to 1 and altering the name + reportMailingJob.delete(); + + // save the report mailing job entity + this.reportMailingJobRepository.save(reportMailingJob); + + return new CommandProcessingResultBuilder().withEntityId(reportMailingJobId).build(); + } + + @Override + @CronTarget(jobName = JobName.EXECUTE_REPORT_MAILING_JOBS) + public void executeReportMailingJobs() throws JobExecutionException { + final Collection<ReportMailingJob> reportMailingJobCollection = this.reportMailingJobRepository.findByIsActiveTrueAndIsDeletedFalse(); + + for (ReportMailingJob reportMailingJob : reportMailingJobCollection) { + // get the tenant's date as a DateTime object + final DateTime localDateTimeOftenant = DateUtils.getLocalDateTimeOfTenant().toDateTime(); + final DateTime nextRunDateTime = reportMailingJob.getNextRunDateTime(); + + if (nextRunDateTime != null && nextRunDateTime.isBefore(localDateTimeOftenant)) { + // get the emailAttachmentFileFormat enum object + final ReportMailingJobEmailAttachmentFileFormat emailAttachmentFileFormat = ReportMailingJobEmailAttachmentFileFormat. + newInstance(reportMailingJob.getEmailAttachmentFileFormat()); + + if (emailAttachmentFileFormat != null && emailAttachmentFileFormat.isValid()) { + final Report stretchyReport = reportMailingJob.getStretchyReport(); + final String reportName = (stretchyReport != null) ? stretchyReport.getReportName() : null; + final StringBuilder errorLog = new StringBuilder(); + final Map<String, String> validateStretchyReportParamMap = this.reportMailingJobValidator. + validateStretchyReportParamMap(reportMailingJob.getStretchyReportParamMap()); + MultivaluedMap<String, String> reportParams = new MultivaluedMapImpl(); + + if (validateStretchyReportParamMap != null) { + Iterator<Map.Entry<String, String>> validateStretchyReportParamMapEntries = validateStretchyReportParamMap.entrySet().iterator(); + + while (validateStretchyReportParamMapEntries.hasNext()) { + Map.Entry<String, String> validateStretchyReportParamMapEntry = validateStretchyReportParamMapEntries.next(); + String key = validateStretchyReportParamMapEntry.getKey(); + String value = validateStretchyReportParamMapEntry.getValue(); + + if (StringUtils.containsIgnoreCase(key, "date")) { + ReportMailingJobStretchyReportParamDateOption reportMailingJobStretchyReportParamDateOption = + ReportMailingJobStretchyReportParamDateOption.newInstance(value); + + if (reportMailingJobStretchyReportParamDateOption.isValid()) { + value = ReportMailingJobDateUtil.getDateAsString(reportMailingJobStretchyReportParamDateOption); + } + } + + reportParams.add(key, value); + } + } + + // generate the report output stream, method in turn call another that sends the file to the email recipients + this.generateReportOutputStream(reportMailingJob, emailAttachmentFileFormat, reportParams, reportName, errorLog); + + // update the previous run time, next run time, status, error log properties + this.updateReportMailingJobAfterJobExecution(reportMailingJob, errorLog, localDateTimeOftenant); + } + } + } + } + + /** + * update the report mailing job entity after job execution + * + * @param reportMailingJob -- the report mailing job entity + * @param errorLog -- StringBuilder object containing the error log if any + * @param jobStartDateTime -- the start DateTime of the job + * @return None + **/ + private void updateReportMailingJobAfterJobExecution(final ReportMailingJob reportMailingJob, final StringBuilder errorLog, + final DateTime jobStartDateTime) { + final String recurrence = reportMailingJob.getRecurrence(); + final DateTime nextRunDateTime = reportMailingJob.getNextRunDateTime(); + ReportMailingJobPreviousRunStatus reportMailingJobPreviousRunStatus = ReportMailingJobPreviousRunStatus.SUCCESS; + + reportMailingJob.updatePreviousRunErrorLog(null); + + if (errorLog != null && errorLog.length() > 0) { + reportMailingJobPreviousRunStatus = ReportMailingJobPreviousRunStatus.ERROR; + reportMailingJob.updatePreviousRunErrorLog(errorLog.toString()); + } + + reportMailingJob.increaseNumberOfRunsByOne(); + reportMailingJob.updatePreviousRunStatus(reportMailingJobPreviousRunStatus.getValue()); + reportMailingJob.updatePreviousRunDateTime(reportMailingJob.getNextRunDateTime()); + + // check if the job has a recurrence pattern, if not deactivate the job. The job will only run once + if (StringUtils.isEmpty(recurrence)) { + // deactivate job + reportMailingJob.deactivate(); + + // job will only run once, no next run time + reportMailingJob.updateNextRunDateTime(null); + } else if (nextRunDateTime != null) { + final DateTime nextRecurringDateTime = this.createNextRecurringDateTime(recurrence, nextRunDateTime); + + // finally update the next run date time property + reportMailingJob.updateNextRunDateTime(nextRecurringDateTime); + } + + // save the ReportMailingJob entity + this.reportMailingJobRepository.save(reportMailingJob); + + // create a new report mailing job run history entity + this.createReportMailingJobRunHistroryAfterJobExecution(reportMailingJob, errorLog, jobStartDateTime, + reportMailingJobPreviousRunStatus.getValue()); + } + + /** + * create the next recurring DateTime from recurrence pattern, start DateTime and current DateTime + * + * @param recurrencePattern + * @param startDateTime + * @return DateTime object + */ + private DateTime createNextRecurringDateTime(final String recurrencePattern, final DateTime startDateTime) { + DateTime nextRecurringDateTime = null; + + // the recurrence pattern/rule cannot be empty + if (StringUtils.isNotBlank(recurrencePattern) && startDateTime != null) { + final LocalDate nextDayLocalDate = startDateTime.plus(1).toLocalDate(); + final LocalDate nextRecurringLocalDate = CalendarUtils.getNextRecurringDate(recurrencePattern, startDateTime.toLocalDate(), + nextDayLocalDate); + final String nextDateTimeString = nextRecurringLocalDate + " " + startDateTime.getHourOfDay() + ":" + startDateTime.getMinuteOfHour() + + ":" + startDateTime.getSecondOfMinute(); + final DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(DATETIME_FORMAT); + + nextRecurringDateTime = DateTime.parse(nextDateTimeString, dateTimeFormatter); + } + + return nextRecurringDateTime; + } + + /** + * create a new report mailing job run history entity after job execution + * + * @param reportMailingJob -- the report mailing job entity + * @param errorLog -- StringBuilder object containing the error log if any + * @param jobStartDateTime -- the start DateTime of the job + * @param jobRunStatus -- the status of the job (success/error) + * @return None + **/ + private void createReportMailingJobRunHistroryAfterJobExecution(final ReportMailingJob reportMailingJob, final StringBuilder errorLog, + final DateTime jobStartDateTime, final String jobRunStatus) { + final DateTime jobEndDateTime = DateUtils.getLocalDateTimeOfTenant().toDateTime(); + final String errorLogToString = (errorLog != null) ? errorLog.toString() : null; + final ReportMailingJobRunHistory reportMailingJobRunHistory = ReportMailingJobRunHistory.newInstance(reportMailingJob, jobStartDateTime, + jobEndDateTime, jobRunStatus, null, errorLogToString); + + this.reportMailingJobRunHistoryRepository.save(reportMailingJobRunHistory); + } + + /** + * Handle any SQL data integrity issue + * + * @param jsonCommand -- JsonCommand object + * @param dve -- data integrity exception object + * @return None + **/ + private void handleDataIntegrityIssues(final JsonCommand jsonCommand, final DataIntegrityViolationException dve) { + final Throwable realCause = dve.getMostSpecificCause(); + + if (realCause.getMessage().contains(ReportMailingJobConstants.NAME_PARAM_NAME)) { + final String name = jsonCommand.stringValueOfParameterNamed(ReportMailingJobConstants.NAME_PARAM_NAME); + throw new PlatformDataIntegrityException("error.msg.report.mailing.job.duplicate.name", "Report mailing job with name `" + name + "` already exists", + ReportMailingJobConstants.NAME_PARAM_NAME, name); + } + + logger.error(dve.getMessage(), dve); + + throw new PlatformDataIntegrityException("error.msg.charge.unknown.data.integrity.issue", + "Unknown data integrity issue with resource: " + realCause.getMessage()); + } + + /** + * generate the report output stream + * + * @param reportMailingJob + * @param emailAttachmentFileFormat + * @param reportParams + * @param reportName + * @param errorLog + * @return the error log StringBuilder object + */ + private StringBuilder generateReportOutputStream(final ReportMailingJob reportMailingJob, final ReportMailingJobEmailAttachmentFileFormat emailAttachmentFileFormat, + final MultivaluedMap<String, String> reportParams, final String reportName, final StringBuilder errorLog) { + + try { + final String reportType = this.readReportingService.getReportType(reportName); + final ReportingProcessService reportingProcessService = this.reportingProcessServiceProvider.findReportingProcessService(reportType); + + if (reportingProcessService != null) { + final Response processReport = reportingProcessService.processRequest(reportName, reportParams); + final Object reponseObject = (processReport != null) ? processReport.getEntity() : null; + + if (reponseObject != null && reponseObject.getClass().equals(ByteArrayOutputStream.class)) { + final ByteArrayOutputStream byteArrayOutputStream = ByteArrayOutputStream.class.cast(reponseObject); + final String fileLocation = FileSystemContentRepository.FINERACT_BASE_DIR + File.separator + ""; + final String fileNameWithoutExtension = fileLocation + File.separator + reportName; + + // check if file directory exists, if not create directory + if (!new File(fileLocation).isDirectory()) { + new File(fileLocation).mkdirs(); + } + + if ((byteArrayOutputStream == null) || byteArrayOutputStream.size() == 0) { + errorLog.append("Report processing failed, empty output stream created"); + } else if ((errorLog != null && errorLog.length() == 0) && (byteArrayOutputStream.size() > 0)) { + final String fileName = fileNameWithoutExtension + "." + emailAttachmentFileFormat.getValue(); + + // send the file to email recipients + this.sendReportFileToEmailRecipients(reportMailingJob, fileName, byteArrayOutputStream, errorLog); + } + } else { + errorLog.append("Response object entity is not equal to ByteArrayOutputStream ---------- "); + } + } else { + errorLog.append("ReportingProcessService object is null ---------- "); + } + } catch (Exception e) { + errorLog.append("The ReportMailingJobWritePlatformServiceImpl.generateReportOutputStream method threw an Exception: " + + e + " ---------- "); + } + + return errorLog; + } + + /** + * send report file to email recipients + * + * @param reportMailingJob + * @param fileName + * @param byteArrayOutputStream + * @param errorLog + */ + private void sendReportFileToEmailRecipients(final ReportMailingJob reportMailingJob, final String fileName, + final ByteArrayOutputStream byteArrayOutputStream, final StringBuilder errorLog) { + final Set<String> emailRecipients = this.reportMailingJobValidator.validateEmailRecipients(reportMailingJob.getEmailRecipients()); + + try { + final File file = new File(fileName); --- End diff -- Don't you need to close this file instance once mail is sent? > Implement ability to schedule & e-mail reports > ---------------------------------------------- > > Key: FINERACT-65 > URL: https://issues.apache.org/jira/browse/FINERACT-65 > Project: Apache Fineract > Issue Type: New Feature > Reporter: Emmanuel Nnaa > Assignee: Markus Geiss > Priority: Minor > > Some reports take a bit longer to run, therefore we should be able to allow > our users to schedule them overnight to be e-mailed. > The way this should work is: > - Add e-mailaddress to users and make it mandatory (possibly already in > MifosX) > - Allow users to configure schedule entries which consist of: > - One or multiple target users (e-mailaddresses) > - Set the preferred subject and content of the e-mail (Look at user generated > documents API in MifosX for replacing certain variables) > - Choose the run frequency of the report (daily, weekly, every x'th day of > the month etc, cron-like flexibility with a UI) > - Pick the preferred output format (PDF/XLS/CSV) > These reports should be picked up by a scheduled job overnight and then run > in serial to avoid performance hits and sent out to the users. -- This message was sent by Atlassian JIRA (v6.3.4#6332)