/*
* The Kuali Financial System, a comprehensive financial management system for higher education.
*
* Copyright 2005-2014 The Kuali Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kuali.kfs.sys.batch.service.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.batch.dataaccess.FinancialSystemDocumentHeaderPopulationDao;
import org.kuali.kfs.sys.batch.service.FinancialSystemDocumentHeaderPopulationService;
import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeader;
import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeaderMissingFromWorkflow;
import org.kuali.kfs.sys.service.NonTransactional;
import org.kuali.rice.kew.api.document.Document;
import org.kuali.rice.kew.api.document.DocumentStatus;
import org.kuali.rice.kew.api.document.WorkflowDocumentService;
import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
import org.kuali.rice.kim.api.identity.IdentityService;
import org.kuali.rice.kim.api.identity.principal.Principal;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.springframework.transaction.annotation.Transactional;
/**
* The base implementation of the FinancialSystemDocumentHeaderPopulationService
*/
public class FinancialSystemDocumentHeaderPopulationServiceImpl implements FinancialSystemDocumentHeaderPopulationService {
org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(FinancialSystemDocumentHeaderPopulationServiceImpl.class);
protected WorkflowDocumentService workflowDocumentService;
protected BusinessObjectService businessObjectService;
protected IdentityService identityService;
protected FinancialSystemDocumentHeaderPopulationDao financialSystemDocumentHeaderPopulationDao;
protected volatile String systemUserPrincipalId;
/**
* Populates financial system document header records, at a count of batchSize at a time, until the jobRunSize number of records have been processed, skipping document headers that
* are included in documentStatusesToPopulate if the given Set has any members at all
* @see org.kuali.kfs.sys.batch.service.FinancialSystemDocumentHeaderPopulationService#populateFinancialSystemDocumentHeadersFromKew(int, int, Set<DocumentStatus>)
*/
@Override
@NonTransactional
public void populateFinancialSystemDocumentHeadersFromKew(int batchSize, Integer jobRunSize, Set<DocumentStatus> documentStatusesToPopulate) {
final long startTime = System.currentTimeMillis();
for (Collection<FinancialSystemDocumentHeader> documentHeaderBatch : getFinancialSystemDocumentHeaderBatchIterable(batchSize, jobRunSize)) {
Map<String, FinancialSystemDocumentHeader> documentHeaderMap = convertDocumentHeaderBatchToMap(documentHeaderBatch);
handleBatch(documentHeaderMap, documentStatusesToPopulate);
}
final long endTime = System.currentTimeMillis();
final double runTimeSeconds = (endTime - startTime) / 1000.0;
LOG.info("Run time: "+runTimeSeconds);
}
/**
* Reads in the matching KEW document headers for the given batch of FinancialSystemDocumentHeader records and updates
* @see org.kuali.kfs.sys.batch.service.FinancialSystemDocumentHeaderPopulationService#handleBatch(java.util.Map, Set<DocumentStatus>)
*/
@Override
@Transactional
public void handleBatch(Map<String, FinancialSystemDocumentHeader> documentHeaders, Set<DocumentStatus> documentStatusesToPopulate) {
List<Document> workflowDocuments = getWorkflowDocuments(documentHeaders, documentStatusesToPopulate);
List<FinancialSystemDocumentHeader> documentHeadersToSave = new ArrayList<FinancialSystemDocumentHeader>();
for (Document kewDocHeader : workflowDocuments) {
final FinancialSystemDocumentHeader fsDocHeader = documentHeaders.get(kewDocHeader.getDocumentId());
if (fsDocHeader != null) {
updateDocumentHeader(fsDocHeader, kewDocHeader);
documentHeadersToSave.add(fsDocHeader);
} else {
// how would this even happen????
LOG.error("Document ID: "+kewDocHeader.getDocumentId()+" was returned from search but no financial system document header could be found in the map. And it's freaking me out, man!");
}
}
// save the changes
getBusinessObjectService().save(documentHeadersToSave);
}
/**
* Returns a List of KEW document headers to match the given FinancialSystemDocumentHeader records. If a workflow document header cannot be found
* for a financial system document header, the document number will be saved as a FinancialSystemDocumentHeaderMissingFromWorkflow record and skipped
* from subsequent runs of the job
* @param documentHeaders a Map of FS document headers
* @param documentStatusesToPopulate if the given Set has any members, only documents in the given statuses will have their FinancialSystemDocumentHeader records populated
* @return a List of matching workflow document header records, skipping any records included in the documentStatusesToPopulate Set if there are any members in it at all
*/
protected List<Document> getWorkflowDocuments(Map<String, FinancialSystemDocumentHeader> documentHeaders, Set<DocumentStatus> documentStatusesToPopulate) {
List<Document> workflowDocuments = new ArrayList<Document>();
List<FinancialSystemDocumentHeaderMissingFromWorkflow> missingWorkflowHeaders = new ArrayList<FinancialSystemDocumentHeaderMissingFromWorkflow>();
for (String documentNumber : documentHeaders.keySet()) {
final Document workflowDoc = getWorkflowDocumentService().getDocument(documentNumber);
if (workflowDoc != null && (documentStatusesToPopulate.isEmpty() || documentStatusesToPopulate.contains(workflowDoc.getStatus()))) {
workflowDocuments.add(workflowDoc);
} else if (workflowDoc == null) { // only record the error if we weren't supposed to skip the record...ie, if the workflow document is null
LOG.error("Could not find a workflow document record for financial system document header #"+documentNumber);
FinancialSystemDocumentHeaderMissingFromWorkflow missingWorkflowHeader = new FinancialSystemDocumentHeaderMissingFromWorkflow();
missingWorkflowHeader.setDocumentNumber(documentNumber);
missingWorkflowHeaders.add(missingWorkflowHeader);
}
}
if (!missingWorkflowHeaders.isEmpty()) {
getBusinessObjectService().save(missingWorkflowHeaders);
}
return workflowDocuments;
}
/**
* Joins the given Set of document numbers with a pipe "|" character
* @param documentIds the document numbers to join
* @return the joined document numbers, ready to be handed to the document search
*/
protected String pipeDocumentIds(Set<String> documentIds) {
return StringUtils.join(documentIds,'|');
}
/**
* Creates a DocumentSearchCriteria which will look up the documents identified by the ids piped into the documentIdForSearch
* @return a DocumentSearchCriteria to look up the document search results
*/
protected DocumentSearchCriteria buildDocumentSearchCriteria(String documentIdForSearch) {
DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create();
criteria.setDocumentId(documentIdForSearch);
return criteria.build();
}
/**
* @return the principal id of the system user
*/
protected String getSystemUserPrincipalId() {
if (StringUtils.isBlank(systemUserPrincipalId)) {
final Principal principal = getIdentityService().getPrincipalByPrincipalName(KFSConstants.SYSTEM_USER);
systemUserPrincipalId = principal.getPrincipalId();
}
return systemUserPrincipalId;
}
/**
* Writes to the passed in log a message detailing the changes which will occur when the job is run outside of log mode,
* ie what the updated document status, document type, initiator principal id, and application document status will be updated to
* @param financialSystemDocumentHeader the financial system document header which would have been updated
* @param kewDocumentHeader the workflow document header with the information to update with
* @param log the log to write to
*/
protected void logChanges(FinancialSystemDocumentHeader financialSystemDocumentHeader, Document kewDocumentHeader, Logger log) {
log.info("Financial System Document Header "+financialSystemDocumentHeader.getDocumentNumber()+" KEW document header "+kewDocumentHeader.getDocumentId()+
" Initiator Principal Id: "+kewDocumentHeader.getInitiatorPrincipalId()+" Document Type Name: "+kewDocumentHeader.getDocumentTypeName()+
" Document Status: "+kewDocumentHeader.getStatus().getLabel()+" Application Document Status: "+kewDocumentHeader.getApplicationDocumentStatus());
}
/**
* Updates the financial system document header with values from the workflow document header
* @param financialSystemDocumentHeader the financial system document header to update
* @param kewDocumentHeader the workflow document header with values to update the financial system document header with
*/
protected void updateDocumentHeader(FinancialSystemDocumentHeader financialSystemDocumentHeader, Document kewDocumentHeader) {
financialSystemDocumentHeader.setInitiatorPrincipalId(kewDocumentHeader.getInitiatorPrincipalId());
financialSystemDocumentHeader.setWorkflowDocumentTypeName(kewDocumentHeader.getDocumentTypeName());
financialSystemDocumentHeader.setWorkflowDocumentStatusCode(kewDocumentHeader.getStatus().getCode());
financialSystemDocumentHeader.setApplicationDocumentStatus(kewDocumentHeader.getApplicationDocumentStatus());
financialSystemDocumentHeader.setWorkflowCreateDate(new java.sql.Timestamp(kewDocumentHeader.getDateCreated().getMillis()));
}
/**
* Convenience iterator to get batches of FinancialSystemDocumentHeader by batch size
*/
protected class FinancialSystemDocumentHeaderBatchIterator implements Iterator<Collection<FinancialSystemDocumentHeader>> {
protected int batchSize;
protected int currentStartIndex = 1;
protected int documentHeaderCount;
protected FinancialSystemDocumentHeaderBatchIterator(int batchSize, Integer jobRunSize) {
this.batchSize = batchSize;
Map<String, Object> fieldValues = new HashMap<String, Object>(); // there's no "countAll" so we'll just pass in an empty Map for the count
this.documentHeaderCount = getFinancialSystemDocumentHeaderCount();
if (jobRunSize != null && jobRunSize.intValue() > 0 && jobRunSize.intValue() < this.documentHeaderCount) {
this.documentHeaderCount = jobRunSize; // use jobRunSize to limit
}
}
@Override
public boolean hasNext() {
return currentStartIndex <= documentHeaderCount;
}
@Override
public Collection<FinancialSystemDocumentHeader> next() {
int endIndex = currentStartIndex + batchSize - 1;
if (endIndex > documentHeaderCount) {
endIndex = documentHeaderCount;
}
// nota bene: it was discussed that it might be helpful to have a parameter with a specific list of document numbers to read in and convert.
// if such a parameter were implemented, this would likely be a good place to use that logic.... The DAO shouldn't read the parameter directly, I think....
Collection<FinancialSystemDocumentHeader> docHeaderBatch = readBatchOfFinancialSystemDocumentHeaders(currentStartIndex, endIndex);
currentStartIndex = endIndex + 1;
return docHeaderBatch;
}
@Override
public void remove() {
throw new UnsupportedOperationException("This iterator is read only; remove should not be called against it");
}
}
/**
* Counts the number of Financial System Document Header records without initiator principal id's set
* @see org.kuali.kfs.sys.batch.service.FinancialSystemDocumentHeaderPopulationService#getFinancialSystemDocumentHeaderCount()
*/
@Transactional
@Override
public int getFinancialSystemDocumentHeaderCount() {
return getFinancialSystemDocumentHeaderPopulationDao().countTotalFinancialSystemDocumentHeadersToProcess();
}
/**
* Uses the DAO to retrieve the specified batch
* @see org.kuali.kfs.sys.batch.service.FinancialSystemDocumentHeaderPopulationService#readBatchOfFinancialSystemDocumentHeaders(int, int)
*/
@Transactional
@Override
public Collection<FinancialSystemDocumentHeader> readBatchOfFinancialSystemDocumentHeaders(int startIndex, int endIndex) {
return getFinancialSystemDocumentHeaderPopulationDao().getFinancialSystemDocumentHeadersForBatch(startIndex, endIndex);
}
/**
* Returns a new Iterable to iterate over batches of FinancialSystemDocumentHeaders
* @param batchSize the size of the batches to build
* @param jobRunSize the number of records
* @return the newly created Iterable
*/
protected Iterable<Collection<FinancialSystemDocumentHeader>> getFinancialSystemDocumentHeaderBatchIterable(final int batchSize, final Integer jobRunSize) {
return new Iterable<Collection<FinancialSystemDocumentHeader>>() {
@Override
public Iterator<Collection<FinancialSystemDocumentHeader>> iterator() {
return new FinancialSystemDocumentHeaderBatchIterator(batchSize, jobRunSize);
}
};
}
/**
* Converts a Collection of FinancialSystemDocumentHeader records into a Map keyed by the document number
* @param documentHeaderBatch a Collection of FinancialSystemDocumentHeader records
* @return the Map of FinancialSystemDocumentHeader records keyed by document number
*/
protected Map<String, FinancialSystemDocumentHeader> convertDocumentHeaderBatchToMap(Collection<FinancialSystemDocumentHeader> documentHeaderBatch) {
Map<String, FinancialSystemDocumentHeader> documentHeaderMap = new HashMap<String, FinancialSystemDocumentHeader>();
for (FinancialSystemDocumentHeader docHeader : documentHeaderBatch) {
documentHeaderMap.put(docHeader.getDocumentNumber(), docHeader);
}
return documentHeaderMap;
}
@NonTransactional
public WorkflowDocumentService getWorkflowDocumentService() {
return workflowDocumentService;
}
@NonTransactional
public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) {
this.workflowDocumentService = workflowDocumentService;
}
@NonTransactional
public BusinessObjectService getBusinessObjectService() {
return businessObjectService;
}
@NonTransactional
public void setBusinessObjectService(BusinessObjectService businessObjectService) {
this.businessObjectService = businessObjectService;
}
@NonTransactional
public IdentityService getIdentityService() {
return identityService;
}
@NonTransactional
public void setIdentityService(IdentityService identityService) {
this.identityService = identityService;
}
@NonTransactional
public FinancialSystemDocumentHeaderPopulationDao getFinancialSystemDocumentHeaderPopulationDao() {
return financialSystemDocumentHeaderPopulationDao;
}
@NonTransactional
public void setFinancialSystemDocumentHeaderPopulationDao(FinancialSystemDocumentHeaderPopulationDao financialSystemDocumentHeaderPopulationDao) {
this.financialSystemDocumentHeaderPopulationDao = financialSystemDocumentHeaderPopulationDao;
}
}