diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunner.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunner.java
new file mode 100644
index 000000000..b724ed8c2
--- /dev/null
+++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunner.java
@@ -0,0 +1,167 @@
+/* $This file is distributed under the terms of the license in /doc/license.txt$ */
+
+package edu.cornell.mannlib.vitro.utilities.testrunner;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+ * A harness that runs a system-level command.
+ *
+ *
+ * No provision is made for standard input.
+ *
+ *
+ * The standard output and standard error streams are asynchronously read, so
+ * the sub-process will not block on full buffers. Warning: if either of these
+ * streams contain more data than can fit into a String, then we will have a
+ * problem.
+ *
+ *
+ * @author jblake
+ */
+public class CommandRunner {
+
+ private int returnCode;
+ private String stdOut = "";
+ private String stdErr = "";
+ private File workingDirectory;
+
+ /* Gets informed of output as it arrives. Never null. */
+ private final Logger logger;
+
+ private final Map environmentAdditions = new HashMap();
+
+ public CommandRunner(SeleniumRunnerParameters parms) {
+ this.logger = parms.getLogger();
+ }
+
+ /** Set the directory that the command will run in. */
+ public void setWorkingDirectory(File workingDirectory) {
+ this.workingDirectory = workingDirectory;
+ }
+
+ /** Add (or replace) any environment variable. */
+ public void setEnvironmentVariable(String key, String value) {
+ this.environmentAdditions.put(key, value);
+ }
+
+ /**
+ * Run the command.
+ *
+ * @param command
+ * a list containing the operating system program and its
+ * arguments. See
+ * {@link java.lang.ProcessBuilder#ProcessBuilder(List)}.
+ */
+ public void run(List command) throws CommandRunnerException {
+ logger.subProcessStart(command);
+ try {
+ ProcessBuilder builder = new ProcessBuilder(command);
+
+ if (workingDirectory != null) {
+ builder.directory(workingDirectory);
+ }
+
+ if (!environmentAdditions.isEmpty()) {
+ builder.environment().putAll(this.environmentAdditions);
+ }
+
+ Process process = builder.start();
+ StreamEater outputEater = new StreamEater(process.getInputStream(),
+ false);
+ StreamEater errorEater = new StreamEater(process.getErrorStream(),
+ true);
+
+ this.returnCode = process.waitFor();
+
+ outputEater.join();
+ this.stdOut = outputEater.getContents();
+
+ errorEater.join();
+ this.stdErr = errorEater.getContents();
+ } catch (IOException e) {
+ throw new CommandRunnerException(
+ "Exception when handling sub-process:", e);
+ } catch (InterruptedException e) {
+ throw new CommandRunnerException(
+ "Exception when handling sub-process:", e);
+ }
+ logger.subProcessStop(command);
+ }
+
+ public int getReturnCode() {
+ return returnCode;
+ }
+
+ public String getStdErr() {
+ return stdErr;
+ }
+
+ public String getStdOut() {
+ return stdOut;
+ }
+
+ /**
+ * A thread that reads an InputStream until it reaches end of file, then
+ * closes the stream. Designated as error stream or not, so it can tell the
+ * logger.
+ */
+ private class StreamEater extends Thread {
+ private final InputStream stream;
+ private final boolean isError;
+
+ private final StringWriter contents = new StringWriter();
+
+ private final byte[] buffer = new byte[4096];
+
+ public StreamEater(InputStream stream, boolean isError) {
+ this.stream = stream;
+ this.isError = isError;
+ this.start();
+ }
+
+ @Override
+ public void run() {
+ try {
+ int howMany = 0;
+ while (true) {
+ howMany = stream.read(buffer);
+ if (howMany > 0) {
+ String string = new String(buffer, 0, howMany);
+ contents.write(string);
+
+ if (isError) {
+ logger.subProcessErrout(string);
+ } else {
+ logger.subProcessStdout(string);
+ }
+ } else if (howMany == 0) {
+ Thread.yield();
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public String getContents() {
+ return contents.toString();
+ }
+ }
+
+}
diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunnerException.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunnerException.java
new file mode 100644
index 000000000..8550abbb7
--- /dev/null
+++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunnerException.java
@@ -0,0 +1,26 @@
+/* $This file is distributed under the terms of the license in /doc/license.txt$ */
+
+package edu.cornell.mannlib.vitro.utilities.testrunner;
+
+/**
+ * Indicates a problem with the attempt to run a command in a sub-process.
+ */
+public class CommandRunnerException extends Exception {
+
+ public CommandRunnerException() {
+ super();
+ }
+
+ public CommandRunnerException(String message) {
+ super(message);
+ }
+
+ public CommandRunnerException(Throwable cause) {
+ super(cause);
+ }
+
+ public CommandRunnerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Logger.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Logger.java
new file mode 100644
index 000000000..9212cf447
--- /dev/null
+++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Logger.java
@@ -0,0 +1,117 @@
+/* $This file is distributed under the terms of the license in /doc/license.txt$ */
+
+package edu.cornell.mannlib.vitro.utilities.testrunner;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * TODO
+ */
+public class Logger {
+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(
+ "yyyy-MM-dd HH:mm:ss.SSS");
+
+ private final Writer writer;
+
+ public Logger(PrintStream out) {
+ this.writer = new OutputStreamWriter(out);
+ }
+
+ public Logger(File logFile) throws IOException {
+ this.writer = new FileWriter(logFile);
+ }
+
+ public void runStarted() {
+ log("Run started.");
+ }
+
+ public void runStopped() {
+ log("Run stopped.");
+ }
+
+ public void subProcessStart(List command) {
+ log("Subprocess started: " + command);
+ }
+
+ public void subProcessStdout(String string) {
+ logRawText(string);
+ }
+
+ public void subProcessErrout(String string) {
+ logRawText(string);
+ }
+
+ public void subProcessStop(List command) {
+ log("Subprocess stopped: " + command);
+ }
+
+ public void suiteStarted(File suiteDir) {
+ log("Suite started: " + suiteDir.getName());
+ }
+
+ public void suiteFailed(File suiteDir, IOException e) {
+ log("Suite failed: " + suiteDir.getName());
+ log(e);
+ }
+
+ public void suiteStopped(File suiteDir) {
+ log("Suite stopped: " + suiteDir.getName());
+ }
+
+ public void cleanUploadStart(File uploadDirectory) {
+ log("Upload cleaning started: " + uploadDirectory.getPath());
+ }
+
+ public void cleanUploadFailed(File uploadDirectory, IOException e) {
+ log("Upload cleaning failed: " + uploadDirectory.getPath());
+ log(e);
+ }
+
+ public void cleanUploadStop(File uploadDirectory) {
+ log("Upload cleaning stopped: " + uploadDirectory.getPath());
+ }
+
+ private void logRawText(String rawText) {
+ try {
+ writer.write(rawText);
+ writer.flush();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void log(String message) {
+ try {
+ writer.write(timeStamp() + " " + message + "\n");
+ writer.flush();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void log(Throwable t) {
+ try {
+ t.printStackTrace(new PrintWriter(writer));
+ writer.flush();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Convert the current date and time to a string for the log.
+ */
+ private String timeStamp() {
+ return DATE_FORMAT.format(new Date());
+ }
+
+}
diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleaner.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleaner.java
new file mode 100644
index 000000000..c6bffa09f
--- /dev/null
+++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleaner.java
@@ -0,0 +1,26 @@
+/* $This file is distributed under the terms of the license in /doc/license.txt$ */
+
+package edu.cornell.mannlib.vitro.utilities.testrunner;
+
+/**
+ * TODO
+ */
+public class ModelCleaner {
+
+ /**
+ * @param parms
+ */
+ public ModelCleaner(SeleniumRunnerParameters parms) {
+ // TODO Auto-generated constructor stub
+ throw new RuntimeException("ModelCleaner Constructor not implemented.");
+ }
+
+ /**
+ *
+ */
+ public void clean() {
+ // TODO Auto-generated method stub
+ throw new RuntimeException("ModelCleaner.clean() not implemented.");
+ }
+
+}
diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunner.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunner.java
new file mode 100644
index 000000000..b6aaf6b15
--- /dev/null
+++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunner.java
@@ -0,0 +1,101 @@
+/* $This file is distributed under the terms of the license in /doc/license.txt$ */
+
+package edu.cornell.mannlib.vitro.utilities.testrunner;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Run the Selenium test suites. Provide the properties file and perhaps an
+ * "interactive" flag.
+ */
+public class SeleniumRunner {
+ private final SeleniumRunnerParameters parms;
+ private final Logger logger;
+ private final UploadAreaCleaner uploadCleaner;
+ private final ModelCleaner modelCleaner;
+ private final SuiteRunner suiteRunner;
+
+ public SeleniumRunner(SeleniumRunnerParameters parms) {
+ this.parms = parms;
+ this.logger = parms.getLogger();
+ this.uploadCleaner = new UploadAreaCleaner(parms);
+ this.modelCleaner = new ModelCleaner(parms);
+ this.suiteRunner = new SuiteRunner(parms);
+ }
+
+ public void runSelectedSuites() {
+ logger.runStarted();
+ for (File suiteDir : parms.getSelectedSuites()) {
+ logger.suiteStarted(suiteDir);
+ try {
+ if (parms.isCleanModel()) {
+ modelCleaner.clean();
+ }
+ if (parms.isCleanUploads()) {
+ uploadCleaner.clean();
+ }
+ suiteRunner.runSuite(suiteDir);
+ } catch (IOException e) {
+ logger.suiteFailed(suiteDir, e);
+ }
+ logger.suiteStopped(suiteDir);
+ }
+ logger.runStopped();
+ }
+
+ private static void selectAllSuites(SeleniumRunnerParameters parms) {
+ List suites = new ArrayList();
+ for (File parentDir : parms.getSuiteParentDirectories()) {
+ suites.addAll(parms.findSuiteDirs(parentDir));
+ }
+ parms.setSelectedSuites(suites);
+ }
+
+ /**
+ * @param args
+ */
+ public static void main(String[] args) {
+ SeleniumRunnerParameters parms = null;
+ boolean interactive = false;
+
+ if ((args.length != 1) && (args.length != 2)) {
+ usage("Wrong number of arguments.");
+ }
+
+ if (args.length == 2) {
+ if (!"interactive".equalsIgnoreCase(args[1])) {
+ usage("Invalid argument '" + args[1] + "'");
+ }
+ interactive = true;
+ }
+
+ try {
+ parms = new SeleniumRunnerParameters(args[0]);
+ } catch (IOException e) {
+ usage("Can't read properties file: " + e);
+ }
+
+ if (interactive) {
+ // TODO hook up the GUI.
+ throw new RuntimeException("interactive mode not implemented.");
+ } else {
+ // Run all of the suites.
+ // For each suite, clean the model and the upload area.
+ selectAllSuites(parms);
+ parms.setCleanModel(true);
+ parms.setCleanUploads(true);
+ SeleniumRunner runner = new SeleniumRunner(parms);
+ runner.runSelectedSuites();
+ }
+ }
+
+ private static void usage(String message) {
+ System.out.println(message);
+ System.out.println("Usage is: SeleniumRunner "
+ + "[\"interactive\"]");
+ System.exit(1);
+ }
+}
diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunnerParameters.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunnerParameters.java
new file mode 100644
index 000000000..54359a8d9
--- /dev/null
+++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunnerParameters.java
@@ -0,0 +1,205 @@
+/* $This file is distributed under the terms of the license in /doc/license.txt$ */
+
+package edu.cornell.mannlib.vitro.utilities.testrunner;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * Holds the runtime parameters that are read from the properties file, perhaps
+ * with modifications from the GUI if we are running interactively.
+ */
+public class SeleniumRunnerParameters {
+ private static final String PROP_OUTPUT_DIRECTORY = "output_directory";
+ private static final String PROP_UPLOAD_DIRECTORY = "upload_directory";
+ private static final String PROP_SUITE_DIRECTORIES = "suite_parent_directories";
+
+ private static final String LOGFILE_NAME = "log_file.txt";
+
+ private final File uploadDirectory;
+ private final File outputDirectory;
+ private final File logFile;
+
+ private final Collection suiteParentDirectories;
+
+ private Collection selectedSuites = Collections.emptySet();
+ private boolean cleanModel = true;
+ private boolean cleanUploads = true;
+ private Logger logger = new Logger(System.out);
+
+ /**
+ * Read the required properties from the property file, and do some checks
+ * on them.
+ */
+ public SeleniumRunnerParameters(String propertiesFilepath)
+ throws IOException {
+ Reader propsReader = null;
+ try {
+ propsReader = new FileReader(new File(propertiesFilepath));
+ Properties props = new Properties();
+ props.load(propsReader);
+
+ this.uploadDirectory = checkReadWriteDirectory(props,
+ PROP_UPLOAD_DIRECTORY);
+ this.outputDirectory = checkReadWriteDirectory(props,
+ PROP_OUTPUT_DIRECTORY);
+ this.logFile = new File(this.outputDirectory, LOGFILE_NAME);
+ this.logger = new Logger(this.logFile);
+
+ this.suiteParentDirectories = checkSuiteParentDirectories(props);
+
+ } finally {
+ if (propsReader != null) {
+ try {
+ propsReader.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Check that there is a property for the output directory, and that it
+ * points to a valid directory.
+ */
+ private File checkReadWriteDirectory(Properties props, String key) {
+ String value = getRequiredProperty(props, key);
+
+ File dir = new File(value);
+
+ if (!dir.exists()) {
+ throw new IllegalArgumentException("Directory " + key + " '"
+ + value + "' does not exist.");
+ }
+
+ if (!dir.isDirectory()) {
+ throw new IllegalArgumentException("Directory " + key + " '"
+ + value + "' is not a directory.");
+ }
+
+ if (!dir.canRead()) {
+ throw new IllegalArgumentException("Directory " + key + " '"
+ + value + "' is not readable.");
+ }
+
+ if (!dir.canWrite()) {
+ throw new IllegalArgumentException("Directory " + key + " '"
+ + value + "' is not writeable.");
+ }
+ return dir;
+ }
+
+ /**
+ * Get the property for the suite directories and ensure that each one is
+ * indeed a readable directory.
+ */
+ private Collection checkSuiteParentDirectories(Properties props) {
+ String value = getRequiredProperty(props, PROP_SUITE_DIRECTORIES);
+
+ List dirs = new ArrayList();
+ String[] paths = value.split("[:;]");
+ for (String path : paths) {
+ File dir = new File(path.trim());
+
+ if (!dir.exists()) {
+ throw new IllegalArgumentException("Suite directory '"
+ + dir.getPath() + "' does not exist.");
+ }
+ if (!dir.isDirectory()) {
+ throw new IllegalArgumentException("Suite directory '"
+ + dir.getPath() + "' is not a directory.");
+ }
+ if (!dir.canRead()) {
+ throw new IllegalArgumentException("Suite directory '"
+ + dir.getPath() + "' is not readable.");
+ }
+ }
+ return dirs;
+ }
+
+ /**
+ * Get the value for this property. If there isn't one, or if it's empty,
+ * complain.
+ */
+ private String getRequiredProperty(Properties props, String key) {
+ String value = props.getProperty(key);
+ if ((value == null) || (value.trim().length() == 0)) {
+ throw new IllegalArgumentException(
+ "Property file must provide a value for '" + key + "'");
+ }
+ return value;
+ }
+
+ public Logger getLogger() {
+ return logger;
+ }
+
+ public void setLogger(Logger logger) {
+ this.logger = logger;
+ }
+
+ public File getUploadDirectory() {
+ return uploadDirectory;
+ }
+
+ public Collection getSuiteParentDirectories() {
+ return suiteParentDirectories;
+ }
+
+ public void setSelectedSuites(Collection selectedSuites) {
+ this.selectedSuites = selectedSuites;
+ }
+
+ public Collection getSelectedSuites() {
+ return new ArrayList(this.selectedSuites);
+ }
+
+ public boolean isCleanModel() {
+ return cleanModel;
+ }
+
+ public void setCleanModel(boolean cleanModel) {
+ this.cleanModel = cleanModel;
+ }
+
+ public boolean isCleanUploads() {
+ return cleanUploads;
+ }
+
+ public void setCleanUploads(boolean cleanUploads) {
+ this.cleanUploads = cleanUploads;
+ }
+
+ /**
+ * Look inside this parent directory and find any suite directories. You can
+ * recognize a suite directory because it contains a file named Suite.html.
+ */
+ public Collection findSuiteDirs(File parentDir) {
+ return Arrays.asList(parentDir.listFiles(new FileFilter() {
+ public boolean accept(File pathname) {
+ if (!pathname.isDirectory()) {
+ return false;
+ }
+ File suiteFile = new File(pathname, "Suite.html");
+ if (suiteFile.exists()) {
+ return true;
+ } else {
+ logger.subProcessErrout("Warning: suite file '" + suiteFile.getPath()
+ + "' does not exist.\n");
+ return false;
+ }
+ }
+ }));
+ }
+
+}
diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SuiteRunner.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SuiteRunner.java
new file mode 100644
index 000000000..c5d39e13f
--- /dev/null
+++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SuiteRunner.java
@@ -0,0 +1,28 @@
+/* $This file is distributed under the terms of the license in /doc/license.txt$ */
+
+package edu.cornell.mannlib.vitro.utilities.testrunner;
+
+import java.io.File;
+
+/**
+ * TODO
+ */
+public class SuiteRunner {
+
+ /**
+ * @param parms
+ */
+ public SuiteRunner(SeleniumRunnerParameters parms) {
+ // TODO Auto-generated constructor stub
+ throw new RuntimeException("SuiteRunner Constructor not implemented.");
+ }
+
+ /**
+ * @param suiteDir
+ */
+ public void runSuite(File suiteDir) {
+ // TODO Auto-generated method stub
+ throw new RuntimeException("SuiteRunner.runSuite() not implemented.");
+ }
+
+}
diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/UploadAreaCleaner.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/UploadAreaCleaner.java
new file mode 100644
index 000000000..4ab73a435
--- /dev/null
+++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/UploadAreaCleaner.java
@@ -0,0 +1,99 @@
+/* $This file is distributed under the terms of the license in /doc/license.txt$ */
+
+package edu.cornell.mannlib.vitro.utilities.testrunner;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+
+/**
+ * Clean out the file upload area, so the next suite will start with no uploads.
+ */
+public class UploadAreaCleaner {
+ private final SeleniumRunnerParameters parms;
+ private final Logger logger;
+
+ public UploadAreaCleaner(SeleniumRunnerParameters parms) {
+ this.parms = parms;
+ this.logger = parms.getLogger();
+ }
+
+ /**
+ * Delete all of the directories and files in the upload directory. Don't
+ * delete the upload directory itself.
+ */
+ public void clean() throws IOException {
+ File uploadDirectory = parms.getUploadDirectory();
+ if (!uploadDirectory.isDirectory()) {
+ throw new IllegalArgumentException("'" + uploadDirectory.getPath()
+ + "' is not a directory.");
+ }
+
+ logger.cleanUploadStart(uploadDirectory);
+
+ try {
+ for (File file : uploadDirectory.listFiles()) {
+ if (file.isFile()) {
+ deleteFile(file);
+ } else {
+ purgeDirectoryRecursively(uploadDirectory);
+ }
+ }
+ } catch (IOException e) {
+ logger.cleanUploadFailed(uploadDirectory, e);
+ throw e;
+ } finally {
+ logger.cleanUploadStop(uploadDirectory);
+ }
+ }
+
+ /**
+ * Delete all of the files in a directory, any sub-directories, and the
+ * directory itself.
+ */
+ protected static void purgeDirectoryRecursively(File directory)
+ throws IOException {
+ File[] files = directory.listFiles();
+ for (File file : files) {
+ if (file.isDirectory()) {
+ purgeDirectoryRecursively(file);
+ } else {
+ deleteFile(file);
+ }
+ }
+ deleteFile(directory);
+ }
+
+ /**
+ * Delete a file, either before or after the test. If it can't be deleted,
+ * complain.
+ */
+ protected static void deleteFile(File file) throws IOException {
+ if (file.exists()) {
+ file.delete();
+ }
+ if (!file.exists()) {
+ return;
+ }
+
+ /*
+ * If we were unable to delete the file, is it because it's a non-empty
+ * directory?
+ */
+ if (!file.isDirectory()) {
+ final StringBuffer message = new StringBuffer(
+ "Can't delete directory '" + file.getPath() + "'\n");
+ file.listFiles(new FileFilter() {
+ public boolean accept(File pathname) {
+ message.append(" contains file '" + pathname + "'\n");
+ return true;
+ }
+ });
+ throw new IOException(message.toString().trim());
+ } else {
+ throw new IOException("Unable to delete file '" + file.getPath()
+ + "'");
+ }
+ }
+
+}