/* * 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.gl.businessobject; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FilenameFilter; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.coa.businessobject.OffsetDefinition; import org.kuali.kfs.gl.GeneralLedgerConstants; import org.kuali.kfs.gl.batch.service.AccountingCycleCachingService; import org.kuali.kfs.gl.service.OriginEntryService; import org.kuali.kfs.sys.ConfigureContext; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.context.KualiTestBase; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.kfs.sys.context.TestUtils; import org.kuali.kfs.sys.dataaccess.UnitTestSqlDao; import org.kuali.kfs.sys.service.ConfigurableDateService; import org.kuali.rice.core.api.config.property.ConfigurationService; import org.kuali.rice.krad.service.PersistenceService; /** * OriginEntryTestBase...the uberpowerful base of a lot of GL tests. Basically, this class provides * many convenience methods for writing tests that test against large batches of origin entries. */ @ConfigureContext public class OriginEntryTestBase extends KualiTestBase { private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(OriginEntryTestBase.class); protected ConfigurableDateService dateTimeService; protected PersistenceService persistenceService; protected UnitTestSqlDao unitTestSqlDao = null; protected ConfigurationService kualiConfigurationService = null; protected OriginEntryService originEntryService = null; protected AccountingCycleCachingService accountingCycleCachingService = null; protected Date date; protected String batchDirectory; protected String builtDirectory; protected String testingYear; protected String testingPeriodCode; /** * Constructs a OriginEntryTestBase instance */ public OriginEntryTestBase() { super(); } /** * Sets up this test base; that means getting some services from Spring and reseting the * enhancement flags. * @see junit.framework.TestCase#setUp() */ @Override protected void setUp() throws Exception { super.setUp(); if (LOG.isDebugEnabled()) { LOG.debug("setUp() starting"); } dateTimeService = SpringContext.getBean(ConfigurableDateService.class); date = dateTimeService.getCurrentDate(); // Other objects needed for the tests persistenceService = SpringContext.getBean(PersistenceService.class, "persistenceServiceOjb"); unitTestSqlDao = SpringContext.getBean(UnitTestSqlDao.class); kualiConfigurationService = SpringContext.getBean(ConfigurationService.class); originEntryService = SpringContext.getBean(OriginEntryService.class); batchDirectory = this.getBatchDirectoryName(); buildBatchDirectory(batchDirectory); accountingCycleCachingService = SpringContext.getBean(AccountingCycleCachingService.class); accountingCycleCachingService.initialize(); // Set all enhancements to off resetAllEnhancementFlags(); testingYear = TestUtils.getFiscalYearForTesting().toString(); testingPeriodCode = TestUtils.getPeriodCodeForTesting(); } /** * Removes any build batch directory * @see junit.framework.TestCase#tearDown() */ @Override protected void tearDown() throws Exception { removeBatchDirectory(batchDirectory); if (accountingCycleCachingService != null) { accountingCycleCachingService.destroy(); } } /** * get the name of the batch directory * @return the name of the batch directory */ protected String getBatchDirectoryName() { return SpringContext.getBean(ConfigurationService.class).getPropertyValueAsString("staging.directory")+"/gl/test_directory/originEntry"; } /** * Recursively ensures that the whole of the path of the batch directory exists */ protected void buildBatchDirectory(String batchDirectory) { String[] directoryPieces = batchDirectory.split("/"); StringBuilder builtDirectorySoFar = new StringBuilder(); StringBuilder directoryToRemoveSoFar = new StringBuilder(); for (String directoryPiece : directoryPieces) { if (!StringUtils.isBlank(directoryPiece)) { builtDirectorySoFar.append('/'); builtDirectorySoFar.append(directoryPiece); File dir = new File(builtDirectorySoFar.toString()); if (!dir.exists()) { directoryToRemoveSoFar.append('/'); directoryToRemoveSoFar.append(directoryPiece); dir.mkdir(); } } } builtDirectory = directoryToRemoveSoFar.toString(); } /** * Removes any directories added as part of building the batch directory */ protected void removeBatchDirectory(String batchDirectory) { String unbuiltDirectory = batchDirectory.substring(0, batchDirectory.length()-builtDirectory.length()); String pathToUnbuild = new String(batchDirectory); while (!unbuiltDirectory.equals(pathToUnbuild)) { File pathToUnbuildFile = new File(pathToUnbuild); clearAllFilesInDirectory(pathToUnbuildFile); pathToUnbuildFile.delete(); int lastSeperator = pathToUnbuild.lastIndexOf('/'); pathToUnbuild = pathToUnbuild.substring(0, lastSeperator); } } /** * Removes all the files within the given directory * @param dir the directory to delete files from */ protected void clearAllFilesInDirectory(File dir) { for (File f : dir.listFiles()) { f.delete(); } } /** * An inner class to point to a specific entry in a group */ protected class EntryHolder { private String baseFileName; private String transactionLine; /** * Constructs a OriginEntryTestBase.EntryHolder * @param baseFileName the group that the entry to point to is in * @param transactionLine the line number of the entry */ public EntryHolder(String baseFileName, String transactionLine) { this.baseFileName = baseFileName; this.transactionLine = transactionLine; } /** * @return the base file name */ public String getBaseFileName() { return this.baseFileName; } /** * @return the transaction line */ public String getTransactionLine() { return this.transactionLine; } } /** * An inner class to filter only the files for this batch run */ class BatchFilenameFilter implements FilenameFilter { /** * @see java.io.FilenameFilter#accept(java.io.File, java.lang.String) */ @Override public boolean accept(File dir, String name) { return name.endsWith(GeneralLedgerConstants.BatchFileSystem.EXTENSION); } } /** * Given a type code and a bunch of transactions, creates a new backup file and adds all * the transactions to that file * * @param baseFileName the type code of the new file * @param transactions an array of String-formatted entries to save into the file */ protected void loadInputTransactions(String fileName, String[] transactions) { final String fullFileName = batchDirectory + "/" + fileName + GeneralLedgerConstants.BatchFileSystem.EXTENSION; try { PrintStream ps = new PrintStream(fullFileName); for (int i = 0; i < transactions.length; i++) { ps.println(transactions[i]); } ps.close(); } catch (FileNotFoundException fnfe) { throw new RuntimeException("Could not open file "+fullFileName); } } /** * Deletes everything in the expenditure transaction table */ protected void clearExpenditureTable() { unitTestSqlDao.sqlCommand("delete from GL_EXPEND_TRN_MT"); } /** * Deletes everything in the sufficient fund balance table */ protected void clearSufficientFundBalanceTable() { unitTestSqlDao.sqlCommand("delete from GL_SF_BALANCES_T"); } /** * Deletes all entries in the entry table with the given chart code and account number * * @param fin_coa_cd the chart code of entries to delete * @param account_nbr the account number of entries to delete */ protected void clearGlEntryTable(String fin_coa_cd, String account_nbr) { unitTestSqlDao.sqlCommand("delete from GL_ENTRY_T where fin_coa_cd = '" + fin_coa_cd + "' and account_nbr = '" + account_nbr + "'"); } /** * Deletes everything in the gl reversal table */ protected void clearReversalTable() { unitTestSqlDao.sqlCommand("delete from GL_REVERSAL_T"); } /** * Deletes everything in the gl balance table */ protected void clearGlBalanceTable() { unitTestSqlDao.sqlCommand("delete from GL_BALANCE_T"); } /** * Deletes everything in the gl encumbrance table. */ protected void clearEncumbranceTable() { unitTestSqlDao.sqlCommand("delete from GL_ENCUMBRANCE_T"); } /** * Deletes everything in the gl account balance table */ protected void clearGlAccountBalanceTable() { unitTestSqlDao.sqlCommand("delete from GL_ACCT_BALANCES_T"); } /** * Deletes all files in the batch test directory */ protected void clearBatchFiles() { //I didn't see a deleteAll method, so... for (File file : new File(batchDirectory).listFiles(new BatchFilenameFilter())) { file.delete(); } } /** * Check all the entries in the file against the data passed in EntryHolder[]. If any of them are different, assert an * error. * * @param fileCount the expected number of files * @param requiredEntries an array of expected String-formatted entries to check against */ protected void assertOriginEntries(int fileCount, EntryHolder[] requiredEntries) { //we do not need to call clearCache() since no dao and jdbc calls mixted in this method. //refer to KFSMI-7637 // persistenceService.clearCache(); File[] files = new File(batchDirectory).listFiles(new BatchFilenameFilter()); assertEquals("Number of groups is wrong: " + Arrays.toString(files), fileCount, files.length); List<EntryHolder> sortedEntryTransactions = new ArrayList<EntryHolder>(); for (File file : files) { BufferedReader br = null; try { br = new BufferedReader(new FileReader(file)); String type = file.getName().replaceFirst(GeneralLedgerConstants.BatchFileSystem.EXTENSION, ""); // FIXME have to do something to bring types and filenames into sync, and probably have to remove separator character too String line = null; while ((line = br.readLine()) != null) { sortedEntryTransactions.add(new EntryHolder(type,line)); } } catch (FileNotFoundException fnfe) { throw new RuntimeException(fnfe); } catch (IOException ioe) { throw new RuntimeException(ioe); } } // now, sort the lines here to avoid any DB sorting issues //in the origin entry group version, this would sort the groups in order of creation, but in the files version, //it will sort the files in alphabetical order by type; entries within the groups (files) should be sorted the same //I don't foresee a problem with this... Comparator<EntryHolder> entryHolderComparator = new Comparator<EntryHolder>() { @Override public int compare(EntryHolder o1, EntryHolder o2) { int groupCompareResult = o1.baseFileName.compareTo(o2.baseFileName); if (groupCompareResult == 0) { return o1.transactionLine.compareTo(o2.transactionLine); } else { return groupCompareResult; } } }; Collections.sort(sortedEntryTransactions, entryHolderComparator); Arrays.sort(requiredEntries, entryHolderComparator); // This is for debugging purposes - change to true for output System.err.println("Files:"); for (File file : files) { System.err.println("F:" + file.getName()); } System.err.println("Transactions:"); for (EntryHolder element : sortedEntryTransactions) { System.err.println("L:" + element.baseFileName + " " + element.transactionLine); } System.err.println("Expected Transactions:"); for (EntryHolder element : requiredEntries) { System.err.println("L:" + element.baseFileName + " " + element.transactionLine); } assertEquals("Wrong number of transactions in Origin Entry\nExpected: " + requiredEntries + "\nActual: " + sortedEntryTransactions, requiredEntries.length, sortedEntryTransactions.size()); int count = 0; for (EntryHolder foundTransaction : sortedEntryTransactions) { // Check file type assertEquals("Group for transaction " + count + " is wrong", requiredEntries[count].baseFileName, foundTransaction.baseFileName); // Check transaction - this is done this way so that Anthill prints the two transactions to make // resolving the issue easier. String expected = requiredEntries[count].transactionLine.substring(0, 173);// trim(); String found = foundTransaction.transactionLine.substring(0, 173);// trim(); if (!found.equals(expected)) { System.err.println("Expected transaction: " + expected); System.err.println("Found transaction: " + found); assertEquals("Transaction " + count + " doesn't match expected output", expected, found); } count++; } } protected static Object[] FLEXIBLE_OFFSET_ENABLED_FLAG = { OffsetDefinition.class, KFSConstants.SystemGroupParameterNames.FLEXIBLE_OFFSET_ENABLED_FLAG }; /** * Resets the flexible offset and flexible claim on cash parameters, so that processes running as unit tests have consistent behaviors * @throws Exception if the parameters could not be reset for some reason */ protected void resetAllEnhancementFlags() throws Exception { setApplicationConfigurationFlag((Class<?>) FLEXIBLE_OFFSET_ENABLED_FLAG[0], (String) FLEXIBLE_OFFSET_ENABLED_FLAG[1], false); } /** * Resets a parameter for the sake of the unit test * * @param componentClass the module class of the parameter * @param name the name of the parameter to reset * @param value the new value for the parameter * @throws Exception thrown if some vague thing goes wrong */ protected void setApplicationConfigurationFlag(Class<?> componentClass, String name, boolean value) throws Exception { TestUtils.setSystemParameter(componentClass, name, value ? "Y" : "N"); } }