/*
* 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.context;
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.List;
import org.apache.log4j.Logger;
/**
* BatchContainerDirectory knows the path to the directory for the BatchContainerStep and BatchStepTrigger semaphore files.
* It also handles much of the logic for writing, reading, and removing those files.
*
* BatchContainerDirectory adds a ConsoleAppender to its Logger if one hasn't been configured.
*
*/
public class BatchContainerDirectory {
private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BatchContainerDirectory.class);
private static final String BATCH_CONTAINER_SEMAPHORE_EXTENSION = "runlock";
private File directory;
/**
* Creates a reference for the path to the batch container. Verifies that the directory exists and throws an error if it does not.
*
* @param batchContainerPath the path to the directory that contains the semaphore files
*/
public BatchContainerDirectory(String batchContainerPath) {
directory = new File(batchContainerPath);
if(!directory.exists()) {
throw new RuntimeException(batchContainerPath + " does not exist. Please verify and run again");
}
else {
LOG.info("batchContainerDirectory=" + batchContainerPath);
}
}
/**
* Writes the semaphore file which indicates that the batch container is running
*
* @param jobName the name of the job that is starting the batch container
* @param stepName the name of the step which starts the batch container
*/
public void writeBatchContainerSemaphore(String jobName, String stepName) {
if (!isExistsBatchContainerSemaphore()) {
BatchStepFileDescriptor batchStepFile = new BatchStepFileDescriptor(jobName, stepName, BATCH_CONTAINER_SEMAPHORE_EXTENSION);
writeBatchStepFileToSystem(batchStepFile, null);
}
}
/**
* Removes the semaphore file which indicates that the batch container is running
*/
public void removeBatchContainerSemaphore() {
FilenameFilter semaphoreExtensionFilter = new FileExtensionFileFilter(BATCH_CONTAINER_SEMAPHORE_EXTENSION);
File[] filteredFiles = directory.listFiles(semaphoreExtensionFilter);
for(File semaphore: filteredFiles) {
semaphore.delete();
}
}
/**
* Checks whether the batch container is currently running (by checking for the presence of the batch container semaphore file)
*
* @return true if the batch container is running (if the semaphore file exists), false otherwise
*/
public boolean isBatchContainerRunning() {
return isExistsBatchContainerSemaphore();
}
/**
* @return true if the batch container semaphore file exists in the batch container, false otherwise
*/
private boolean isExistsBatchContainerSemaphore() {
FilenameFilter semaphoreExtensionFilter = new FileExtensionFileFilter(BATCH_CONTAINER_SEMAPHORE_EXTENSION);
File[] filteredFiles = directory.listFiles(semaphoreExtensionFilter);
if (filteredFiles != null && filteredFiles.length > 0) {
return true;
}
else {
return false;
}
}
/**
* Returns a list of files in the batch container with the run extension
*
* @return a list of step .run files
*/
public File[] getStepRunFiles() {
// Find all the step .run files for all the jobs.
FilenameFilter stepStartFilter = new FileExtensionFileFilter(BatchStepFileDescriptor.getFileExtensionRun());
File[] stepStartFiles = directory.listFiles(stepStartFilter);
// No .run files found
if (stepStartFiles == null || stepStartFiles.length == 0) {
return null;
}
return stepStartFiles;
}
/**
* Writes the run file for the Step specified in the descriptor
*
* @param batchStepFile the step for which a .run file will be written
* @param stepIndex the index of the step in the job
*/
public void writeBatchStepRunFile(BatchStepFileDescriptor batchStepFile, int stepIndex) {
BatchStepFileDescriptor runFile = getCopyWithNewExtension(batchStepFile, BatchStepFileDescriptor.getFileExtensionRun());
// If successFile already exists then container will think the Step completed before it actually did
File successFile = new File(getDirectoryPath() + runFile.getNameNoExtension() + BatchStepFileDescriptor.STEP_FILE_EXTENSION_SEPARATOR + BatchStepFileDescriptor.getFileExtensionSuccess());
if (successFile.exists()) {
throw new RuntimeException(successFile.getName() + " shouldn't exist yet before Step is run");
}
writeBatchStepFileToSystem(runFile, new Integer(stepIndex));
}
/**
* Writes the success file for the Step specified in the descriptor
*
* @param batchStepFile the step for which a .success file will be written
*/
public void writeBatchStepSuccessfulResultFile(BatchStepFileDescriptor batchStepFile) {
BatchStepFileDescriptor successFile = getCopyWithNewExtension(batchStepFile, BatchStepFileDescriptor.getFileExtensionSuccess());
writeBatchStepFileToSystem(successFile, null);
}
/**
* Writes the error file for the Step specified in the descriptor
*
* @param batchStepFile the step for which a .error file will be written
*/
public void writeBatchStepErrorResultFile(BatchStepFileDescriptor batchStepFile) {
BatchStepFileDescriptor errorFile = getCopyWithNewExtension(batchStepFile, BatchStepFileDescriptor.getFileExtensionError());
writeBatchStepFileToSystem(errorFile, null);
}
/**
* Writes the error file for the Step specified in the descriptor. The stack trace in the Throwable will be written to the file.
*
* @param batchStepFile the step for which a .error file will be written
* @param error the error to write in the error file
*/
public void writeBatchStepErrorResultFile(BatchStepFileDescriptor batchStepFile, Throwable error) {
BatchStepFileDescriptor errorFile = getCopyWithNewExtension(batchStepFile, BatchStepFileDescriptor.getFileExtensionError());
writeBatchStepFileToSystem(errorFile, error);
}
/**
* Returns a copy of the descriptor passed into the method, but with the extension specified.
*
* @param batchStepFile the Step for which a new descriptor is needed
* @param extension the extension to use for the new descriptor
* @return a copy of the descriptor passed in, but with the specified extension
*/
private BatchStepFileDescriptor getCopyWithNewExtension(BatchStepFileDescriptor batchStepFile, String extension) {
return new BatchStepFileDescriptor(batchStepFile.getJobName(), batchStepFile.getStepName(), extension);
}
/**
* Writes a batch step semaphore file to the directory; Writes the details to the file.
* Throws a RuntimeException if the file already exists.
*
* @param batchStepFile the step for which a semaphore file will be written
* @param details additional details to add to the semaphore file
*/
private void writeBatchStepFileToSystem(BatchStepFileDescriptor batchStepFile, Object details) {
LOG.debug("Writing "+ batchStepFile.getExtension() +" file for "+ batchStepFile);
String fileName = getDirectoryPath() + batchStepFile.getName();
File file = new File(fileName);
if (!file.exists()) {
try {
LOG.info("Creating new "+ batchStepFile.getExtension() +" file: "+ file.getName());
file.createNewFile();
//if further details exist write them to the file
if (details != null) {
if (details instanceof Throwable) {
writeErrorMessageToFile(file, (Throwable)details);
}
else if (details instanceof Integer) {
writeStepIndexToFile(file, (Integer)details);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
else {
throw new RuntimeException("Step "+ batchStepFile.getExtension() +" file: "+ fileName +" already exists");
}
}
/**
* Removes the semaphore file for the specified step
*
* @param batchStepFile the step for which the semaphore file should be removed
*/
public void removeBatchStepFileFromSystem(BatchStepFileDescriptor batchStepFile) {
LOG.info("Removing "+ batchStepFile.getExtension() +" file for "+ batchStepFile);
String fileName = getDirectoryPath() + batchStepFile.getName();
File file = new File(fileName);
if (file != null && file.exists()) {
boolean successfulDelete = file.delete();
if (!successfulDelete) {
LOG.error("Failed to delete "+ fileName +" from the system.");
} else if (file.exists()) {
// Some file systems use caching which can cause odd container behavior particularly
// if SEMAPHORE_PROCESSING_INTERVAL is set low. This statement could be enhanced to
// loop until the file disappears
LOG.warn("Deleted "+ fileName +" from the system but file still exists. File system slow or cached?");
}
}
else {
//don't need to throw an exception if the file doesn't exist- but note it in the logs
LOG.warn("Step "+ batchStepFile.getExtension() +" file: "+ fileName +" doesn't exist");
}
}
/**
* Returns the result file (either a success file or an error file) if one exists for the specified step. Otherwise null is returned.
*
* @param batchStepFile the step for which a result file is requested
* @return the descriptor of the result file if one exists, null otherwise
*/
public BatchStepFileDescriptor getResultFile(BatchStepFileDescriptor batchStepFile) {
LOG.debug("Looking for a result file for "+ batchStepFile);
String successFileName = getDirectoryPath() + batchStepFile.getNameNoExtension() +"."+ BatchStepFileDescriptor.getFileExtensionSuccess();
File successFile = new File(successFileName);
if (successFile != null && successFile.exists()) {
LOG.info("Found .success result file for "+ batchStepFile);
return new BatchStepFileDescriptor(successFile);
}
String errorFileName = getDirectoryPath() + batchStepFile.getNameNoExtension() +"."+ BatchStepFileDescriptor.getFileExtensionError();
File errorFile = new File(errorFileName);
if (errorFile != null && errorFile.exists()) {
LOG.info("Found .error result file for "+ batchStepFile);
return new BatchStepFileDescriptor(errorFile);
}
return null;
}
/**
* Checks whether the step's semaphore file in the descriptor is null and if not whether the step's semaphore file in the directory is empty
*
* @param batchStepFile the descriptor for which a semaphore file existence check is requested
* @return true if the step's semaphore file is empty, false otherwise. Throws a RuntimeException if the file does not exist.
*/
public boolean isFileEmpty(BatchStepFileDescriptor batchStepFile) {
File resultFile = batchStepFile.getStepFile();
if (resultFile == null) {
throw new RuntimeException(batchStepFile + BatchStepFileDescriptor.STEP_FILE_EXTENSION_SEPARATOR + batchStepFile.getExtension() +" does not exist");
}
return isFileEmpty(resultFile);
}
/**
* Returns the index of the step contained in the file
*
* @param batchStepFile the file from which to retrieve the step index
* @return the step index, or -1 if there was no content in the file
*/
public int getStepIndexFromFile(BatchStepFileDescriptor batchStepFile) {
File runFile = batchStepFile.getStepFile();
if (runFile != null) {
List<String> contents = getFileContents(runFile);
if (contents.size() > 0) {
return Integer.parseInt(contents.get(0));
}
}
return -1;
}
/**
* Reads the contents of the semaphore file (normally the error file), and writes the contents to the requested Logger.
*
* @param batchStepFile the descriptor whose semaphore file's contents should be written to the Logger
* @param log the log to write the file contents to
*/
public void logFileContents(BatchStepFileDescriptor batchStepFile, Logger log) {
File resultFile = batchStepFile.getStepFile();
if (resultFile != null) {
List<String> contents = getFileContents(resultFile);
String toLog = "";
for (String line : contents) {
toLog += line + "\n";
}
log.error("Exception found in "+ resultFile.getName() +"\n"+ toLog);
}
}
/**
* Returns the Exception contained in an error result file
*
* @param errorResultFile
* @return the exception
*/
public String getExceptionFromFile(BatchStepFileDescriptor errorResultFile) {
if (errorResultFile.isStepFileAnErrorResultFile()) {
File resultFile = errorResultFile.getStepFile();
if (resultFile != null) {
List<String> contents = getFileContents(resultFile);
String toLog = "";
for (String line : contents) {
toLog += line + "\n";
}
return toLog;
}
}
return "";
}
/**
* Creates a shutdown hook and adds it to the local Runtime so that if the container's VM is shut down,
* the semaphore files will be removed
*/
public void addShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
removeBatchContainerSemaphore();
if (isBatchContainerRunning()) {
LOG.info("the batch container was not shut down successfully; .runlock still exists.");
}
}
});
}
/**
* Returns a List<String> containing each line in the file
*
* @param file the file whose contents we want to retrieve
* @return a String array containing each line in the file
*/
private List<String> getFileContents(File file) {
ArrayList<String> results = new ArrayList<String>();
try {
FileReader fileReader = new FileReader(file.getAbsolutePath());
String line = "";
BufferedReader inputBufferedReader = new BufferedReader(fileReader);
String currentLine = inputBufferedReader.readLine();
while (currentLine != null) {
results.add(currentLine);
currentLine = inputBufferedReader.readLine();
}
inputBufferedReader.close();
return results;
} catch (IOException e) {
throw new RuntimeException("getFileContents() : " + e.getMessage(), e);
}
}
/**
* Returns the absolute path to the directory
*
* @return the absolute path to the batch container directory
*/
private String getDirectoryPath() {
return directory.getAbsolutePath() + File.separator;
}
/**
* Checks whether the specified File is empty.
*
* @param file the file to check
* @return true if the file is empty, false otherwise
*/
private boolean isFileEmpty(File file) {
return file.length() == 0;
}
private void writeMessageToFile(File runFile , String message) {
PrintStream printStream = initializePrintStream(runFile);
//write index
printStream.print(message);
printStream.flush();
destroyPrintStream(printStream);
}
/**
* Writes the index of the step to the File
*
* @param runFile the file to write to
* @param stepIndex the step index to write
*/
private void writeStepIndexToFile(File runFile, Integer stepIndex) {
PrintStream printStream = initializePrintStream(runFile);
//write index
printStream.print(stepIndex.intValue());
printStream.flush();
destroyPrintStream(printStream);
}
/**
* Writes the stack trace of Throwable to the File
*
* @param errorFile the file to write to
* @param error the error to write
*/
private void writeErrorMessageToFile(File errorFile, Throwable error) {
PrintStream printStream = initializePrintStream(errorFile);
//write error
error.printStackTrace(printStream);
//write contents of the batch container directory
printStream.println("BatchContainerDirectory contents:");
File[] batchContainerFiles = directory.listFiles();
if (batchContainerFiles == null || batchContainerFiles.length == 0) {
printStream.println("empty");
}
else {
for(File batchContainerFile : batchContainerFiles) {
printStream.println(batchContainerFile.getAbsolutePath());
}
}
printStream.flush();
destroyPrintStream(printStream);
}
/**
* Initializes a PrintStream to use for writing to.
*
* @param errorFile the file to write to
* @return a new PrintStream to use to write to the file
*/
private PrintStream initializePrintStream(File errorFile) {
//initialize PrintStream
PrintStream printStream;
try {
printStream = new PrintStream(errorFile);
}
catch (FileNotFoundException e) {
LOG.error(e);
throw new RuntimeException(e);
}
return printStream;
}
/**
* Closes the PrintStream
*
* @param printStream the print stream to close
*/
private void destroyPrintStream(PrintStream printStream) {
//close PrintStream
if(printStream != null) {
printStream.close();
printStream = null;
}
}
}