From c072937fcb725b6e374d5fcb2f436c1af06c1ac3 Mon Sep 17 00:00:00 2001 From: tpylak <tpylak> Date: Fri, 12 Oct 2007 15:12:49 +0000 Subject: [PATCH] file abstraction for incoming data store SVN: 2144 --- .../utilities/DirectoryScanningTimerTask.java | 188 +++++++++++++----- .../cisd/common/utilities/FileUtilities.java | 6 + .../cisd/common/utilities/IStoreHandler.java | 27 +++ .../cisd/common/utilities/StoreItem.java | 58 ++++++ .../DirectoryScanningTimerTaskTest.java | 73 +------ .../common/utilities/FileUtilitiesTest.java | 56 +++++- 6 files changed, 284 insertions(+), 124 deletions(-) create mode 100644 common/source/java/ch/systemsx/cisd/common/utilities/IStoreHandler.java create mode 100644 common/source/java/ch/systemsx/cisd/common/utilities/StoreItem.java diff --git a/common/source/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTask.java b/common/source/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTask.java index 9363d1fd3f7..bd27877a4fd 100644 --- a/common/source/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTask.java +++ b/common/source/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTask.java @@ -18,7 +18,6 @@ package ch.systemsx.cisd.common.utilities; import java.io.File; import java.io.FileFilter; -import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.TimerTask; @@ -26,7 +25,6 @@ import java.util.TimerTask; import org.apache.log4j.Level; import org.apache.log4j.Logger; -import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; import ch.systemsx.cisd.common.logging.ISimpleLogger; import ch.systemsx.cisd.common.logging.LogCategory; import ch.systemsx.cisd.common.logging.LogFactory; @@ -41,10 +39,10 @@ import ch.systemsx.cisd.common.logging.LogFactory; * * @author Bernd Rinn */ -public final class DirectoryScanningTimerTask extends TimerTask implements ISelfTestable +public final class DirectoryScanningTimerTask extends TimerTask { - public static final String FAULTY_PATH_FILENAME = ".faulty_paths"; + static final String FAULTY_PATH_FILENAME = ".faulty_paths"; private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION, DirectoryScanningTimerTask.class); @@ -52,26 +50,33 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf private static final Logger notificationLog = LogFactory.getLogger(LogCategory.NOTIFY, DirectoryScanningTimerTask.class); - private static final IFromStringConverter<File> FILE_CONVERTER = new IFromStringConverter<File>() - { - public File fromString(String value) - { - return new File(value); - } - }; + public static interface IScannedStore + { + /** + * List items in the scanned store in order in which they should be handled. + */ + StoreItem[] tryListSortedReadyToProcess(ISimpleLogger loggerOrNull); - private final IPathHandler handler; + boolean exists(StoreItem item); - private final File sourceDirectory; + /** + * returned description should give the user the idea about file location. You should not use the result for + * something else than printing it for user. It should not be especially assumed that the result is the path + * which could be used in java.io.File constructor. + */ + String getLocationDescription(StoreItem item); + } + + private final IStoreHandler handler; + + private final IScannedStore sourceDirectory; /** The number of consecutive errors of reading a directory that need to occur before the event is logged. */ private final int ignoredErrorCount; private int errorCountReadingDirectory; - private final FileFilter filter; - - private final Set<File> faultyPaths; + private final Set<String> faultyPaths; private final File faultyPathsFile; @@ -102,20 +107,101 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf public DirectoryScanningTimerTask(File sourceDirectory, FileFilter filter, IPathHandler handler, int ignoredErrorCount) { - assert sourceDirectory != null; - assert filter != null; + this(asScannedStore(sourceDirectory, filter), sourceDirectory, asScanningHandler(sourceDirectory, handler), + ignoredErrorCount); + } + + /** + * Creates a <var>DirectoryScanningTimerTask</var>. + * + * @param scannedStore The store which is scan for entries. + * @param faultyPathDirectory The directory in which file with faulty paths is should be stored. + * @param handler The handler that is used for treating the matching paths. + * @param ignoredErrorCount The number of consecutive errors of reading the directory that need to occur before the + * next error is logged (can be used to suppress error when the directory is on a remote share and the + * server is flaky sometimes) + */ + public DirectoryScanningTimerTask(IScannedStore scannedStore, File faultyPathDirectory, IStoreHandler handler, + int ignoredErrorCount) + { + assert scannedStore != null; assert handler != null; assert ignoredErrorCount >= 0; this.ignoredErrorCount = ignoredErrorCount; - this.sourceDirectory = sourceDirectory; - this.filter = filter; + this.sourceDirectory = scannedStore; this.handler = handler; - this.faultyPaths = new HashSet<File>(); - this.faultyPathsFile = new File(sourceDirectory, FAULTY_PATH_FILENAME); + this.faultyPaths = new HashSet<String>(); + this.faultyPathsFile = new File(faultyPathDirectory, FAULTY_PATH_FILENAME); faultyPathsFile.delete(); } + private static IStoreHandler asScanningHandler(final File directory, final IPathHandler handler) + { + return new IStoreHandler() + { + public void handle(StoreItem item) + { + File path = asFile(directory, item); + handler.handle(path); + } + }; + } + + private static IScannedStore asScannedStore(final File directory, final FileFilter filter) + { + return new IScannedStore() + { + public String getLocationDescription(StoreItem item) + { + return DirectoryScanningTimerTask.getLocationDescription(asFile(item)); + } + + public boolean exists(StoreItem item) + { + return asFile(item).exists(); + } + + public StoreItem[] tryListSortedReadyToProcess(ISimpleLogger loggerOrNull) + { + File[] files = FileUtilities.tryListFiles(directory, filter, loggerOrNull); + if (files != null) + { + FileUtilities.sortByLastModified(files); + return asItems(files); + } else + { + return null; + } + } + + private StoreItem[] asItems(File[] files) + { + StoreItem[] items = new StoreItem[files.length]; + for (int i = 0; i < items.length; i++) + { + items[i] = new StoreItem(files[i].getName()); + } + return items; + } + + private File asFile(StoreItem item) + { + return DirectoryScanningTimerTask.asFile(directory, item); + } + }; + } + + private static String getLocationDescription(File file) + { + return file.getPath(); + } + + private static File asFile(File parentDirectory, StoreItem item) + { + return new File(parentDirectory, item.getName()); + } + /** * Handles all entries in the source directory that are picked by the filter. */ @@ -129,13 +215,10 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf operationLog.trace("Start scanning directory " + sourceDirectory + "."); } checkForFaultyPathsFileChanged(); - final File[] paths = listFiles(); - // Sort in order of "oldest first" in order to move older items before newer items. This becomes important - // when doing online quality control of measurements. - Arrays.sort(paths, FileComparator.BY_LAST_MODIFIED); - for (File path : paths) + final StoreItem[] paths = listFiles(); + for (StoreItem path : paths) { - if (faultyPathsFile.equals(path)) // Never touch the faultyPathsFile. + if (isFaultyPathsFile(path)) // Never touch the faultyPathsFile. { continue; } @@ -151,6 +234,13 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf } } + private boolean isFaultyPathsFile(StoreItem item) + { + String itemLocation = sourceDirectory.getLocationDescription(item); + String faultyPathsLocation = getLocationDescription(faultyPathsFile); + return itemLocation.equals(faultyPathsLocation); + } + private void checkForFaultyPathsFileChanged() { if (faultyPathsFile.exists()) @@ -158,12 +248,12 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf if (faultyPathsFile.lastModified() > faultyPathsLastChanged) // Handles manual manipulation. { faultyPaths.clear(); - CollectionIO.readCollection(faultyPathsFile, faultyPaths, FILE_CONVERTER); + CollectionIO.readCollection(faultyPathsFile, faultyPaths); faultyPathsLastChanged = faultyPathsFile.lastModified(); if (operationLog.isInfoEnabled()) { operationLog.info(String.format("Reread faulty paths file (%s), new set contains %d entries", - faultyPathsFile.getPath(), faultyPaths.size())); + getLocationDescription(faultyPathsFile), faultyPaths.size())); } } } else @@ -173,7 +263,7 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf } } - private File[] listFiles() + private StoreItem[] listFiles() { final boolean logNotifyError = (errorCountReadingDirectory == ignoredErrorCount); // Avoid mailbox flooding. final boolean logOperationError = (errorCountReadingDirectory < ignoredErrorCount); @@ -181,7 +271,7 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf logNotifyError ? createSimpleErrorLogger(LogCategory.NOTIFY) : (logOperationError ? createSimpleErrorLogger(LogCategory.OPERATION) : null); - final File[] paths = FileUtilities.tryListFiles(sourceDirectory, filter, errorLogger); + final StoreItem[] paths = sourceDirectory.tryListSortedReadyToProcess(errorLogger); if (errorCountReadingDirectory > ignoredErrorCount && paths != null) { if (notificationLog.isInfoEnabled()) @@ -196,7 +286,7 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf { errorCountReadingDirectory = 0; } - return (paths == null) ? new File[0] : paths; + return (paths == null) ? new StoreItem[0] : paths; } private ISimpleLogger createSimpleErrorLogger(final LogCategory category) @@ -221,41 +311,35 @@ public final class DirectoryScanningTimerTask extends TimerTask implements ISelf }; } - private void handle(File path) + private void handle(StoreItem item) { - if (faultyPaths.contains(path)) + if (isFaultyPath(item)) { // Guard: skip faulty paths. return; } try { - handler.handle(path); + handler.handle(item); } finally { - if (path.exists()) + if (sourceDirectory.exists(item)) { - addToFaultyPaths(path); + addToFaultyPaths(item); } } } - private void addToFaultyPaths(File path) + private boolean isFaultyPath(StoreItem item) { - faultyPaths.add(path); - CollectionIO.writeIterable(faultyPathsFile, faultyPaths); - faultyPathsLastChanged = faultyPathsFile.lastModified(); + String path = sourceDirectory.getLocationDescription(item); + return faultyPaths.contains(path); } - public void check() throws ConfigurationFailureException + private void addToFaultyPaths(StoreItem item) { - if (operationLog.isDebugEnabled()) - { - operationLog.debug("Checking source directory '" + sourceDirectory.getAbsolutePath() + "'."); - } - final String errorMessage = FileUtilities.checkDirectoryFullyAccessible(sourceDirectory, "source"); - if (errorMessage != null) - { - throw new ConfigurationFailureException(errorMessage); - } + String path = sourceDirectory.getLocationDescription(item); + faultyPaths.add(path); + CollectionIO.writeIterable(faultyPathsFile, faultyPaths); + faultyPathsLastChanged = faultyPathsFile.lastModified(); } } diff --git a/common/source/java/ch/systemsx/cisd/common/utilities/FileUtilities.java b/common/source/java/ch/systemsx/cisd/common/utilities/FileUtilities.java index ce557ab13cd..8432fc111eb 100644 --- a/common/source/java/ch/systemsx/cisd/common/utilities/FileUtilities.java +++ b/common/source/java/ch/systemsx/cisd/common/utilities/FileUtilities.java @@ -30,6 +30,7 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -633,6 +634,11 @@ public final class FileUtilities return paths; } + public static void sortByLastModified(File[] files) + { + Arrays.sort(files, FileComparator.BY_LAST_MODIFIED); + } + private static void logFailureInDirectoryListing(RuntimeException exOrNull, File directory, ISimpleLogger logger) { if (exOrNull == null) diff --git a/common/source/java/ch/systemsx/cisd/common/utilities/IStoreHandler.java b/common/source/java/ch/systemsx/cisd/common/utilities/IStoreHandler.java new file mode 100644 index 00000000000..7918fa7537e --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/utilities/IStoreHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.common.utilities; + +/** + * Handles items in the file store + * + * @author Tomasz Pylak on Oct 9, 2007 + */ +public interface IStoreHandler +{ + void handle(StoreItem item); +} diff --git a/common/source/java/ch/systemsx/cisd/common/utilities/StoreItem.java b/common/source/java/ch/systemsx/cisd/common/utilities/StoreItem.java new file mode 100644 index 00000000000..b2d95b86bbe --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/utilities/StoreItem.java @@ -0,0 +1,58 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.systemsx.cisd.common.utilities; + +/** + * Represents one entry (file or directory) in a file store + * + * @author Tomasz Pylak on Oct 8, 2007 + */ +public class StoreItem +{ + public static final StoreItem[] EMPTY_ARRAY = new StoreItem[0]; + + private final String name; + + public StoreItem(String name) + { + this.name = name; + } + + /** Should not be used for logging. Use toString() instead. */ + public String getName() + { + return name; + } + + @Override + public String toString() + { + return name; + } + + @Override + public boolean equals(Object obj) + { + return obj != null && obj instanceof StoreItem && name.equals(((StoreItem) obj).name); + } + + @Override + public int hashCode() + { + return name.hashCode(); + } +} diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTaskTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTaskTest.java index 969ec93914a..13494e863b3 100644 --- a/common/sourceTest/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTaskTest.java +++ b/common/sourceTest/java/ch/systemsx/cisd/common/utilities/DirectoryScanningTimerTaskTest.java @@ -16,8 +16,8 @@ package ch.systemsx.cisd.common.utilities; -import static org.testng.AssertJUnit.assertEquals; import static ch.systemsx.cisd.common.utilities.FileUtilities.ACCEPT_ALL_FILTER; +import static org.testng.AssertJUnit.assertEquals; import java.io.File; import java.io.FileFilter; @@ -31,14 +31,9 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException; import ch.systemsx.cisd.common.logging.LogCategory; import ch.systemsx.cisd.common.logging.LogInitializer; import ch.systemsx.cisd.common.logging.LogMonitoringAppender; -import ch.systemsx.cisd.common.utilities.CollectionIO; -import ch.systemsx.cisd.common.utilities.DirectoryScanningTimerTask; -import ch.systemsx.cisd.common.utilities.FileUtilities; -import ch.systemsx.cisd.common.utilities.StoringUncaughtExceptionHandler; /** * Test cases for the {@link DirectoryScanningTimerTask}. @@ -119,60 +114,6 @@ public class DirectoryScanningTimerTaskTest exceptionHandler.checkAndRethrowException(); } - @Test(expectedExceptions = - { ConfigurationFailureException.class }) - public void testFailedConstructionNonExistent() - { - final File nonExistentFile = new File(unitTestRootDirectory, "non-existent"); - nonExistentFile.delete(); - final DirectoryScanningTimerTask task = - new DirectoryScanningTimerTask(nonExistentFile, ACCEPT_ALL_FILTER, mockPathHandler); - task.check(); - } - - @Test(expectedExceptions = - { ConfigurationFailureException.class }) - public void testFailedConstructionFileInsteadOfDirectory() throws IOException - { - final File file = new File(unitTestRootDirectory, "existent_file"); - file.delete(); - file.deleteOnExit(); - file.createNewFile(); - final DirectoryScanningTimerTask task = - new DirectoryScanningTimerTask(file, ACCEPT_ALL_FILTER, mockPathHandler); - task.check(); - } - - @Test(groups = - { "requires_unix" }, expectedExceptions = - { ConfigurationFailureException.class }) - public void testFailedConstructionReadOnly() throws IOException, InterruptedException - { - final File readOnlyDirectory = new File(unitTestRootDirectory, "read_only_directory"); - readOnlyDirectory.delete(); - readOnlyDirectory.mkdir(); - readOnlyDirectory.deleteOnExit(); - assert readOnlyDirectory.setReadOnly(); - - try - { - // Here we should get an AssertationError - final DirectoryScanningTimerTask task = - new DirectoryScanningTimerTask(readOnlyDirectory, ACCEPT_ALL_FILTER, mockPathHandler); - task.check(); - } finally - { - // Unfortunately, with JDK 5 there is no portable way to set a file or directory read/write, once - // it has been set read-only, thus this test 'requires_unix' for the time being. - Runtime.getRuntime().exec(String.format("/bin/chmod u+w %s", readOnlyDirectory.getPath())).waitFor(); - if (readOnlyDirectory.canWrite() == false) - { - // Can't use assert here since we expect an AssertationError - throw new IllegalStateException(); - } - } - } - @Test public void testFaultyPathsDeletion() { @@ -219,7 +160,8 @@ public class DirectoryScanningTimerTaskTest assert someFile.exists(); final DirectoryScanningTimerTask scanner = new DirectoryScanningTimerTask(workingDirectory, ACCEPT_ALL_FILTER, mockPathHandler); - CollectionIO.writeIterable(faultyPaths, Collections.singleton(someFile)); + String fileLocation = someFile.getPath(); + CollectionIO.writeIterable(faultyPaths, Collections.singleton(fileLocation)); scanner.run(); assertEquals(0, mockPathHandler.handledPaths.size()); } @@ -340,8 +282,7 @@ public class DirectoryScanningTimerTaskTest scanner.run(); appender.verifyLogHasHappened(); dir.delete(); - } - finally + } finally { LogMonitoringAppender.removeAppender(appender); } @@ -385,9 +326,9 @@ public class DirectoryScanningTimerTaskTest final LogMonitoringAppender appenderNotifyError = LogMonitoringAppender.addAppender(LogCategory.NOTIFY, "Failed to get listing of directory"); final LogMonitoringAppender appenderOperationError = - LogMonitoringAppender.addAppender(LogCategory.OPERATION, "Failed to get listing of directory"); + LogMonitoringAppender.addAppender(LogCategory.OPERATION, "Failed to get listing of directory"); final LogMonitoringAppender appenderOK = - LogMonitoringAppender.addAppender(LogCategory.NOTIFY, "' is available again"); + LogMonitoringAppender.addAppender(LogCategory.NOTIFY, "' is available again"); try { final int numberOfErrorsToIgnore = 2; @@ -435,7 +376,7 @@ public class DirectoryScanningTimerTaskTest final int numberOfErrorsToIgnore = 2; // The directory needs to exist when the scanner is created, otherwise the self-test will fail. final DirectoryScanningTimerTask scanner = - new DirectoryScanningTimerTask(dir, ACCEPT_ALL_FILTER, mockPathHandler, numberOfErrorsToIgnore); + new DirectoryScanningTimerTask(dir, ACCEPT_ALL_FILTER, mockPathHandler, numberOfErrorsToIgnore); dir.delete(); assert dir.exists() == false; // First error -> ignored diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/utilities/FileUtilitiesTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/utilities/FileUtilitiesTest.java index 5b32c76aaf5..b976ee2d8c5 100644 --- a/common/sourceTest/java/ch/systemsx/cisd/common/utilities/FileUtilitiesTest.java +++ b/common/sourceTest/java/ch/systemsx/cisd/common/utilities/FileUtilitiesTest.java @@ -64,7 +64,51 @@ public class FileUtilitiesTest { FileUtils.cleanDirectory(workingDirectory); } - + + @Test + public void testFailedConstructionNonExistent() + { + final File nonExistentFile = new File(workingDirectory, "non-existent"); + nonExistentFile.delete(); + String errorMsg = FileUtilities.checkDirectoryFullyAccessible(nonExistentFile, "test"); + assertNotNull(errorMsg); + } + + @Test + public void testFailedConstructionFileInsteadOfDirectory() throws IOException + { + final File file = new File(workingDirectory, "existent_file"); + file.delete(); + file.deleteOnExit(); + file.createNewFile(); + String errorMsg = FileUtilities.checkDirectoryFullyAccessible(file, "test"); + assertNotNull(errorMsg); + } + + @Test(groups = + { "requires_unix" }) + public void testFailedConstructionReadOnly() throws IOException, InterruptedException + { + final File readOnlyDirectory = new File(workingDirectory, "read_only_directory"); + readOnlyDirectory.delete(); + readOnlyDirectory.mkdir(); + readOnlyDirectory.deleteOnExit(); + assert readOnlyDirectory.setReadOnly(); + + String errorMsg = FileUtilities.checkDirectoryFullyAccessible(readOnlyDirectory, "test"); + + // --- clean before checking results + // Unfortunately, with JDK 5 there is no portable way to set a file or directory read/write, once + // it has been set read-only, thus this test 'requires_unix' for the time being. + Runtime.getRuntime().exec(String.format("/bin/chmod u+w %s", readOnlyDirectory.getPath())).waitFor(); + if (readOnlyDirectory.canWrite() == false) + { + // Can't use assert here since we expect an AssertationError + throw new IllegalStateException(); + } + assertNotNull(errorMsg); + } + @Test public void testCopyFile() throws Exception { @@ -77,7 +121,7 @@ public class FileUtilitiesTest assertEquals(FileUtilities.loadToString(sourceFile), FileUtilities.loadToString(destinationFile)); assertEquals(47110000, destinationFile.lastModified()); } - + @Test public void testWriteToFile() throws Exception { @@ -94,7 +138,7 @@ public class FileUtilitiesTest FileUtilities.writeToFile(file, "Hello world"); assertEquals("Hello world\n", FileUtilities.loadToString(file)); } - + @Test public void testWriteToExistingDirectory() throws Exception { @@ -112,7 +156,7 @@ public class FileUtilitiesTest assertTrue("Exception message not as expected: " + message, cause.getMessage().startsWith(dir.toString())); } } - + @Test public void testWriteToExistingReadOnlyFile() throws Exception { @@ -134,7 +178,7 @@ public class FileUtilitiesTest assert file.delete(); } } - + @Test public void testLoadToStringFile() throws Exception { @@ -271,7 +315,7 @@ public class FileUtilitiesTest newFile = FileUtilities.createNextNumberedFile(file, pattern, "12abc_[1]"); assertEquals(new File(workingDirectory, "12abc_[13]"), newFile); newFile = FileUtilities.createNextNumberedFile(file, Pattern.compile("xxx(\\d+)xxx"), "12abc_[1]"); - assertEquals(new File(workingDirectory, "12abc_[1]"), newFile); + assertEquals(new File(workingDirectory, "12abc_[1]"), newFile); } @Test -- GitLab