/*
* (c) Rob Gordon 2005.
*/
package org.oddjob.io;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.oddjob.framework.HardReset;
import org.oddjob.framework.SoftReset;
/**
* @oddjob.description Delete a file or directory, or files
* and directories.
* <p>
* Unless the force property is set, this job will cause an
* exception if an attempt is made to delete a non empty directory.
*
* @oddjob.example
*
* Delete all files from a directory. The directory is the first of
* Oddjob's arguments.
*
* {@oddjob.xml.resource org/oddjob/io/DeleteFilesExample.xml}
*
* @author Rob Gordon
*/
public class DeleteJob implements Callable<Integer>, Serializable {
private static final long serialVersionUID = 200601172014032400L;
private static final Logger logger = Logger.getLogger(DeleteJob.class);
/**
* @oddjob.property
* @oddjob.description A name, can be any text.
* @oddjob.required No.
*/
private volatile String name;
/**
* @oddjob.property
* @oddjob.description The file, directory, or files and directories
* to delete. Note the files must be valid file name, they can not
* contain wildcard characters. This will be the case by default if
* the {@link FilesType} is used to specify the files.
* @oddjob.required Yes.
*/
private volatile File[] files;
/**
* @oddjob.property
* @oddjob.description Forceably delete non empty directories.
* @oddjob.required No, defaults to false.
*/
private volatile boolean force;
/**
* @oddjob.property
* @oddjob.description Logs the number of files and directories deleted
* every n number of items. If this property is 1 then the file or
* directory path is logged every delete. If this property is less than
* one then the counts are logged only at the end.
* @oddjob.required No, defaults to 0.
*/
private volatile int logEvery;
/**
* @oddjob.property
* @oddjob.description Flag to indicate that it is the intention to
* delete files at the root level. This is to catch the situation
* where variable substitution is used to specify the file path but
* the variable doesn't exists - e.g. The file specification is
* <code>${some.dir}/*</code> but <code>some.dir</code> has not been
* defined.
* @oddjob.required No, defaults to false.
*/
private volatile boolean reallyRoot;
/**
* @oddjob.property
* @oddjob.description The maximum number of errors to allow before
* failing. Sometimes when deleting a large number of files, it is not
* desirable to have one or two locked files from stopping all the other
* files from being deleted.
* @oddjob.required No, defaults to 0.
*/
private volatile int maxErrors;
/**
* @oddjob.property
* @oddjob.description Count of the files deleted.
*/
private final AtomicInteger fileCount = new AtomicInteger();
/**
* @oddjob.property
* @oddjob.description Count of the directories deleted.
*/
private final AtomicInteger dirCount = new AtomicInteger();
/**
* @oddjob.property
* @oddjob.description Count of the errors.
*/
private final AtomicInteger errorCount = new AtomicInteger();
/*
* (non-Javadoc)
* @see java.util.concurrent.Callable#call()
*/
@Override
public Integer call() throws IOException, InterruptedException {
if (files == null) {
throw new IllegalStateException("Files must be specified.");
}
File[] toDelete = files;
try {
for (int i = 0; i < toDelete.length; ++i) {
if (Thread.interrupted()) {
throw new InterruptedException("Delete interrupted.");
}
File fileToDelete = toDelete[i];
if (!fileToDelete.exists()) {
logger.debug("Ignoring " + fileToDelete +
", it does not exist.");
continue;
}
deleteFile(fileToDelete);
}
}
finally {
logCounts();
}
return new Integer(0);
}
protected void deleteFile(File fileToDelete) throws IOException {
try {
if (isRoot(fileToDelete) && !reallyRoot) {
throw new IllegalStateException(
"You can not delete root (/*) files " +
"without setting the reallyRoot property to true.");
}
boolean isDirectory = fileToDelete.isDirectory();
if (force) {
FileUtils.forceDelete(fileToDelete);
}
else {
if (!fileToDelete.delete()) {
throw new IOException("Failed to delete " + fileToDelete);
}
}
if (isDirectory) {
dirCount.incrementAndGet();
}
else {
fileCount.incrementAndGet();
}
if (logEvery == 1) {
logger.info("Deleted " + fileToDelete);
}
else {
if (logEvery > 0 &&
(dirCount.get() + fileCount.get()) % logEvery == 0) {
logCounts();
}
if (logger.isDebugEnabled()) {
logger.debug("Deleted " + fileToDelete);
}
}
}
catch (IOException|RuntimeException e) {
if (errorCount.incrementAndGet() >= maxErrors &&
maxErrors >= 0) {
if (maxErrors > 0) {
logger.info("Max error count of " + maxErrors + " exceeded.");
}
throw e;
}
else {
logger.info(e.getMessage());
}
}
}
protected boolean isRoot(File fileToDelete) throws IOException {
File canonicalFile = fileToDelete.getCanonicalFile();
return canonicalFile.getParentFile() == null ||
canonicalFile.getParentFile().getParentFile() == null;
}
/**
* Log counts.
*
*/
private void logCounts() {
int fileCount = this.fileCount.get();
int dirCount = this.dirCount.get();
int errorCount = this.errorCount.get();
StringBuilder message = new StringBuilder();
message.append("Deleted ");
if (fileCount > 0) {
message.append(fileCount);
message.append(" file");
if (fileCount > 1) {
message.append("s");
}
if (dirCount > 0) {
message.append(" and ");
}
}
if (dirCount > 0) {
message.append(dirCount);
message.append(" director");
if (dirCount == 1) {
message.append("y");
}
else {
message.append("ies");
}
}
if (fileCount == 0 && dirCount == 0) {
message.append("nothing");
}
if (errorCount > 0) {
message.append(" (" + errorCount + " error");
if (errorCount > 1) {
message.append("s");
}
message.append(")");
}
message.append(".");
logger.info(message.toString());
}
@HardReset
@SoftReset
public void reset() {
fileCount.set(0);
dirCount.set(0);
errorCount.set(0);
}
/**
* Get the name.
*
* @return The name.
*/
public String getName() {
return name;
}
/**
* Set the name
*
* @param name The name.
*/
public void setName(String name) {
this.name = name;
}
/**
* Get the files.
*
* @return The files.
*/
public File[] getFiles() {
return files;
}
/**
* Set the files.
*
* @param The files.
*/
public void setFiles(File[] files) {
this.files = files;
}
/**
* Getter for force property.
*
* @return The force property.
*/
public boolean isForce() {
return force;
}
/**
* Setter for force property.
*
* @param force The force property.
*/
public void setForce(boolean force) {
this.force = force;
}
public void setLogEvery(int logEvery) {
this.logEvery = logEvery;
}
public int getLogEvery() {
return logEvery;
}
public void setReallyRoot(boolean reallyRoot) {
this.reallyRoot = reallyRoot;
}
public boolean isReallyRoot() {
return reallyRoot;
}
public int getFileCount() {
return fileCount.get();
}
public int getDirCount() {
return dirCount.get();
}
public int getErrorCount() {
return errorCount.get();
}
public int getMaxErrors() {
return maxErrors;
}
public void setMaxErrors(int maxErrors) {
this.maxErrors = maxErrors;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
if (name == null) {
return getClass().getSimpleName();
}
else {
return name;
}
}
}