diff --git a/pom.xml b/pom.xml index bb37321..c81ba1f 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,45 @@ nl.knaw.dans dans-java-utils + + junit + junit + RELEASE + test + + + org.mockito + mockito-core + 5.8.0 + test + + + org.junit.jupiter + junit-jupiter + test + + + io.dropwizard + dropwizard-testing + 3.0.5 + test + + + com.h2database + h2 + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-junit-jupiter + 5.4.0 + test + diff --git a/src/main/assembly/dist/bin/dd-manage-deposit b/src/main/assembly/dist/bin/dd-manage-deposit index fe00670..0625e07 100644 --- a/src/main/assembly/dist/bin/dd-manage-deposit +++ b/src/main/assembly/dist/bin/dd-manage-deposit @@ -5,4 +5,4 @@ BINPATH=$(command readlink -f $0 2> /dev/null || command grealpath $0 2> /dev/nu APPHOME=$(dirname $(dirname $BINPATH)) CONFIG_PATH=/etc/opt/dans.knaw.nl/$SCRIPTNAME/config.yml -java -Ddans.default.config=$CONFIG_PATH -jar $APPHOME/bin/$SCRIPTNAME.jar "$@" +java $DANS_JAVA_OPTS -Ddans.default.config=$CONFIG_PATH -jar $APPHOME/bin/$SCRIPTNAME.jar "$@" diff --git a/src/main/assembly/dist/cfg/config.yml b/src/main/assembly/dist/cfg/config.yml index e941205..cec3712 100644 --- a/src/main/assembly/dist/cfg/config.yml +++ b/src/main/assembly/dist/cfg/config.yml @@ -18,7 +18,7 @@ depositBoxes: - /var/opt/dans.knaw.nl/tmp/auto-ingest/outbox/failed - /var/opt/dans.knaw.nl/tmp/sword2-uploads -pollingInterval: 5000 +pollingInterval: 500 depositPropertiesDatabase: driverClass: org.postgresql.Driver diff --git a/src/main/java/nl/knaw/dans/managedeposit/DdManageDepositApplication.java b/src/main/java/nl/knaw/dans/managedeposit/DdManageDepositApplication.java index e5428f2..9326387 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/DdManageDepositApplication.java +++ b/src/main/java/nl/knaw/dans/managedeposit/DdManageDepositApplication.java @@ -17,11 +17,11 @@ package nl.knaw.dans.managedeposit; import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import io.dropwizard.db.DataSourceFactory; import io.dropwizard.hibernate.HibernateBundle; import io.dropwizard.hibernate.UnitOfWorkAwareProxyFactory; -import io.dropwizard.core.setup.Bootstrap; -import io.dropwizard.core.setup.Environment; import nl.knaw.dans.managedeposit.core.CsvMessageBodyWriter; import nl.knaw.dans.managedeposit.core.DepositProperties; import nl.knaw.dans.managedeposit.core.service.DepositStatusUpdater; @@ -34,10 +34,6 @@ public class DdManageDepositApplication extends Application { - public static void main(final String[] args) throws Exception { - new DdManageDepositApplication().run(args); - } - private final HibernateBundle depositPropertiesHibernate = new HibernateBundle<>(DepositProperties.class) { @@ -47,6 +43,10 @@ public DataSourceFactory getDataSourceFactory(DdManageDepositConfiguration confi } }; + public static void main(final String[] args) throws Exception { + new DdManageDepositApplication().run(args); + } + @Override public String getName() { return "Dd Manage Deposit"; @@ -70,13 +70,10 @@ public void run(final DdManageDepositConfiguration configuration, final Environm final UnitOfWorkAwareProxyFactory proxyFactory = new UnitOfWorkAwareProxyFactory(depositPropertiesHibernate); DepositStatusUpdater depositStatusUpdater = proxyFactory.create( - DepositStatusUpdater.class, - new Class[] { DepositPropertiesDAO.class }, - new Object[] { depositPropertiesDAO }); + DepositStatusUpdater.class, DepositPropertiesDAO.class, depositPropertiesDAO); final IngestPathMonitor ingestPathMonitor = new IngestPathMonitor(configuration.getDepositBoxes(), depositStatusUpdater, configuration.getPollingInterval()); environment.lifecycle().manage(ingestPathMonitor); - } } diff --git a/src/main/java/nl/knaw/dans/managedeposit/DdManageDepositConfiguration.java b/src/main/java/nl/knaw/dans/managedeposit/DdManageDepositConfiguration.java index 70fc225..e492e40 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/DdManageDepositConfiguration.java +++ b/src/main/java/nl/knaw/dans/managedeposit/DdManageDepositConfiguration.java @@ -18,7 +18,6 @@ import io.dropwizard.core.Configuration; import io.dropwizard.db.DataSourceFactory; -import nl.knaw.dans.managedeposit.core.service.TextTruncation; import javax.validation.Valid; import javax.validation.constraints.NotNull; @@ -26,7 +25,9 @@ import java.util.ArrayList; import java.util.List; +@SuppressWarnings("unused") public class DdManageDepositConfiguration extends Configuration { + private static final long DEFAULT_POLLING_INTERVAL = 500; @Valid @NotNull private DataSourceFactory database = new DataSourceFactory(); @@ -52,7 +53,7 @@ public void setDepositPropertiesDatabase(DataSourceFactory database) { } public long getPollingInterval() { - return pollingInterval > 0 ? pollingInterval : TextTruncation.pollingInterval; + return pollingInterval > 0 ? pollingInterval : DEFAULT_POLLING_INTERVAL; } public void setPollingInterval(long pollingInterval) { diff --git a/src/main/java/nl/knaw/dans/managedeposit/core/CsvMessageBodyWriter.java b/src/main/java/nl/knaw/dans/managedeposit/core/CsvMessageBodyWriter.java index 3236949..43ae7bb 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/core/CsvMessageBodyWriter.java +++ b/src/main/java/nl/knaw/dans/managedeposit/core/CsvMessageBodyWriter.java @@ -45,14 +45,16 @@ public boolean isWriteable(Class type, Type genericType, Annotation[] annotation @Override public void writeTo(List data, Class aClass, Type type, Annotation[] annotations, MediaType mediaType, MultivaluedMap multivaluedMap, OutputStream outputStream) throws IOException, WebApplicationException { - if (data != null && data.size() > 0) { + if (data != null && !data.isEmpty()) { // TODO: pass the mapper in at configuration time CsvMapper mapper = new CsvMapper(); Object o = data.get(0); CsvSchema schema = mapper.schemaFor(o.getClass()) .withHeader() .sortedBy("depositor", "depositId", "bagName", "depositState", "depositCreationTimestamp", "depositUpdateTimestamp", "description", "location", "storageInBytes", "deleted") - .rebuild().build(); + .rebuild() + .setNullValue("undefined") + .build(); mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); mapper.registerModule(new JavaTimeModule()); diff --git a/src/main/java/nl/knaw/dans/managedeposit/core/DepositProperties.java b/src/main/java/nl/knaw/dans/managedeposit/core/DepositProperties.java index 177416a..941e074 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/core/DepositProperties.java +++ b/src/main/java/nl/knaw/dans/managedeposit/core/DepositProperties.java @@ -30,6 +30,8 @@ name = "showAll", query = "SELECT dp FROM DepositProperties dp" ) + +@SuppressWarnings("unused") public class DepositProperties { @Column(name = "depositor", nullable = false) // depositor.userId @@ -49,10 +51,10 @@ public class DepositProperties { @Column(name = "deposit_update_timestamp") // modified timestamp of deposit.properties private OffsetDateTime depositUpdateTimestamp; - @Column(name = "description", length = TextTruncation.maxDescriptionLength) // state.description + @Column(name = "description", length = TextTruncation.MAX_DESCRIPTION_LENGTH) // state.description private String description; - @Column(name = "location", length = TextTruncation.maxDirectoryLength) // full parent-path on disk + @Column(name = "location", length = TextTruncation.MAX_DIRECTORY_LENGTH) // full parent-path on disk private String location; @Column(name = "storage_in_bytes") // Total storage of deposit directory @@ -60,15 +62,12 @@ public class DepositProperties { @Column(name = "deleted") // deposit is deleted from inbox - archived private boolean deleted; - public String getDepositId() { - return depositId; - } public DepositProperties() { } public DepositProperties(String depositId, String depositor, String bagName, String depositState, - String description, OffsetDateTime depositCreationTimestamp, String location, long storageInBytes) { + String description, OffsetDateTime depositCreationTimestamp, String location, long storageInBytes, OffsetDateTime depositUpdateTimestamp) { this.depositId = depositId; this.depositor = depositor; this.bagName = bagName; @@ -77,6 +76,11 @@ public DepositProperties(String depositId, String depositor, String bagName, Str this.depositCreationTimestamp = depositCreationTimestamp; this.location = location; this.storageInBytes = storageInBytes; + this.depositUpdateTimestamp = depositUpdateTimestamp; + } + + public String getDepositId() { + return depositId; } public String getDepositor() { @@ -169,4 +173,20 @@ public boolean equals(Object o) { public int hashCode() { return 31 * depositId.hashCode() + depositor.hashCode(); } -} \ No newline at end of file + + @Override + public String toString() { + return "DepositProperties{" + + "depositor='" + depositor + "'" + + ", depositId='" + depositId + '\'' + + ", bagName='" + bagName + '\'' + + ", depositState='" + depositState + '\'' + + ", depositCreationTimestamp=" + depositCreationTimestamp + + ", depositUpdateTimestamp=" + depositUpdateTimestamp + + ", description='" + description + '\'' + + ", location='" + location + '\'' + + ", storageInBytes=" + storageInBytes + + ", deleted=" + deleted + + '}'; + } +} diff --git a/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositPropertiesAssembler.java b/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositPropertiesAssembler.java index 1a18fb7..13e4fbc 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositPropertiesAssembler.java +++ b/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositPropertiesAssembler.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.DateTimeException; import java.time.OffsetDateTime; import java.util.Optional; @@ -34,49 +35,46 @@ class DepositPropertiesAssembler { DepositPropertiesAssembler() { } - Optional assembleObject(File depositPropertiesFile, boolean updateModificationDateTime) { - + Optional assembleObject(File depositPropertiesFile, long CalculatedFolderSize) { Path depositPath = depositPropertiesFile.getParentFile().toPath(); log.debug("assembleObject(depositPropertiesPath:Path): '{}'", depositPropertiesFile.getAbsolutePath()); - DepositProperties dp; // = null + DepositProperties dp = null; Configuration configuration; try { configuration = DepositPropertiesFileReader.readDepositProperties(depositPropertiesFile); + String creationTime = configuration.getString("creation.timestamp"); dp = new DepositProperties(depositPath.getFileName().toString(), configuration.getString("depositor.userId", ""), configuration.getString("bag-store.bag-name", ""), configuration.getString("state.label", ""), - TextTruncation.stripEnd(configuration.getString("state.description", ""), TextTruncation.maxDescriptionLength), - OffsetDateTime.parse(configuration.getString("creation.timestamp", OffsetDateTime.now().toString())), - TextTruncation.stripBegin(depositPropertiesFile.getParentFile().getParentFile().getAbsolutePath(), TextTruncation.maxDirectoryLength), - calculateFolderSize(depositPath)); - - if (updateModificationDateTime) { - dp.setDepositUpdateTimestamp(OffsetDateTime.now()); - } - else { - dp.setDepositUpdateTimestamp(dp.getDepositCreationTimestamp()); - } - + TextTruncation.stripEnd(configuration.getString("state.description", ""), TextTruncation.MAX_DESCRIPTION_LENGTH), + (creationTime == null || creationTime.isEmpty()) ? null : OffsetDateTime.parse(creationTime), + TextTruncation.stripBegin(depositPropertiesFile.getParentFile().getParentFile().getAbsolutePath(), TextTruncation.MAX_DIRECTORY_LENGTH), + CalculatedFolderSize == 0 ? calculateFolderSize(depositPath) : CalculatedFolderSize, + OffsetDateTime.now()); } - catch (ConfigurationException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); + catch (ConfigurationException | DateTimeException e) { + log.error("Error reading deposit.properties file: {}", e.getMessage()); } - return Optional.of(dp); + catch (RuntimeException e) { + log.error("Error accessing deposit files : {}", e.getMessage()); + } + return Optional.ofNullable(dp); } private long calculateFolderSize(Path path) { - long size; - try (var pathStream = Files.walk(path)) { - size = pathStream - .filter(p -> p.toFile().isFile()) - .mapToLong(p -> p.toFile().length()) - .sum(); - } - catch (IOException e) { - throw new RuntimeException(e); + long size = 0; + if (Files.exists(path)) { + try (var pathStream = Files.walk(path)) { + size = pathStream + .filter(p -> p.toFile().isFile()) + .mapToLong(p -> p.toFile().length()) + .sum(); + } + catch (IOException e) { + throw new RuntimeException(e); + } } return size; diff --git a/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositPropertiesFileReader.java b/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositPropertiesFileReader.java index cfe8402..14a89ee 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositPropertiesFileReader.java +++ b/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositPropertiesFileReader.java @@ -26,7 +26,7 @@ import java.io.File; class DepositPropertiesFileReader { - private static final Logger log = LoggerFactory.getLogger(DepositPropertiesAssembler.class); + private static final Logger log = LoggerFactory.getLogger(DepositPropertiesFileReader.class); static public Configuration readDepositProperties(File propertiesFile) throws ConfigurationException { log.debug("readDepositProperties: '{}'", propertiesFile.toString()); diff --git a/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositStatusUpdater.java b/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositStatusUpdater.java index 936fa79..aeb5807 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositStatusUpdater.java +++ b/src/main/java/nl/knaw/dans/managedeposit/core/service/DepositStatusUpdater.java @@ -28,7 +28,7 @@ import java.util.Optional; public class DepositStatusUpdater { - private static final Logger log = LoggerFactory.getLogger(DepositPropertiesAssembler.class); + private static final Logger log = LoggerFactory.getLogger(DepositStatusUpdater.class); private final DepositPropertiesDAO depositPropertiesDAO; private final DepositPropertiesAssembler depositPropertiesAssembler; @@ -38,47 +38,69 @@ public DepositStatusUpdater(DepositPropertiesDAO depositPropertiesDAO) { } @UnitOfWork - public void onCreateDeposit(File depositPropertiesFile) { - Optional dp = depositPropertiesDAO.findById(depositPropertiesFile.getParentFile().getName()); + public void onDepositCreate(File depositPropertiesFile) { + // Note: the 'move deposit' action is processed by the system in two steps: 1. `create` deposit in the new location; 2. `delete` it from the old location. + Optional record = depositPropertiesDAO.findById(depositPropertiesFile.getParentFile().getName()); - if (dp.isPresent()) { - // The 'move deposit' action is processed in two steps: 1. `create` deposit in the new location; 2. `delete` it from the old location. - // Update the location column. - Path depositLocationFolder = Path.of(depositPropertiesFile.getParentFile().getParentFile().getAbsolutePath()); - Optional updatedNumber = depositPropertiesDAO.updateDepositLocation(dp.get().getDepositId(), depositLocationFolder); - if (updatedNumber.isPresent()) - log.debug("onCreateDeposit - `location` of deposit '{}' has been updated to '{}' ", dp.get().getDepositId(), depositLocationFolder); + if (record.isPresent()) { + // Update the location column and other fields + Optional dpObject = depositPropertiesAssembler.assembleObject(depositPropertiesFile, record.get().getStorageInBytes()); + if (dpObject.isPresent()) { + depositPropertiesDAO.merge(dpObject.get()); + log.debug("onDepositCreate - The deposit '{}' location and/or state has been updated to '{}' ", record.get().getDepositId(), depositPropertiesFile.getParentFile().getAbsolutePath()); + } + else { + writeErrorMsg(depositPropertiesFile.getParentFile().getAbsolutePath()); + } } else { - Optional dpObject = depositPropertiesAssembler.assembleObject(depositPropertiesFile, false); - dpObject.ifPresent(depositPropertiesDAO::save); - log.debug("onCreateDeposit: A new deposit has been registered `{}`", depositPropertiesFile.getParentFile().getAbsolutePath()); + Optional dpObject = depositPropertiesAssembler.assembleObject(depositPropertiesFile, 0); + if (dpObject.isPresent()) { + depositPropertiesDAO.save(dpObject.get()); + log.debug("onDepositCreate: A new deposit has been registered '{}'", depositPropertiesFile.getParentFile().getAbsolutePath()); + } + else { + writeErrorMsg(depositPropertiesFile.getParentFile().getAbsolutePath()); + } } } @UnitOfWork - public void onChangeDeposit(File depositPropertiesFile) { - Optional dpObject = depositPropertiesAssembler.assembleObject(depositPropertiesFile, true); - dpObject.ifPresent(depositPropertiesDAO::save); - log.debug("onChangeDeposit: deposit.properties has been changed `{}`", depositPropertiesFile.getParentFile().getAbsolutePath()); + public void onDepositChange(File depositPropertiesFile) { + Optional record = depositPropertiesDAO.findById(depositPropertiesFile.getParentFile().getName()); + long folder_size = record.map(DepositProperties::getStorageInBytes).orElse(0L); + Optional dpObject = depositPropertiesAssembler.assembleObject(depositPropertiesFile, folder_size); + if (dpObject.isPresent()) { + depositPropertiesDAO.merge(dpObject.get()); + log.debug("onDepositChange: deposit.properties has been changed '{}'", depositPropertiesFile.getParentFile().getAbsolutePath()); + } + else { + writeErrorMsg(depositPropertiesFile.getParentFile().getAbsolutePath()); + } } @UnitOfWork - public void onDeleteDeposit(File depositPropertiesFile) { - // At this stage, the deposit.properties file's handle is present but the content is null (impossible to read data of the file) - Optional dp = depositPropertiesDAO.findById(depositPropertiesFile.getParentFile().getName()); + public void onDepositDelete(File depositPropertiesFile) { + // Note: if delete notify is part of a folder moving, then at this stage, the deposit.properties file's handle is present but the content is null (impossible to read data of the file) + Optional record = depositPropertiesDAO.findById(depositPropertiesFile.getParentFile().getName()); - try { - // The 'move deposit' action is processed in two steps: 1. `create` deposit in the new location; 2. `delete` it from the old location. Ignore delete step - if (dp.isPresent() && Files.isSameFile(Path.of(dp.get().getLocation()), Path.of(depositPropertiesFile.getParentFile().getParentFile().getAbsolutePath()))) { - String depositId = depositPropertiesFile.getParentFile().getName(); - Optional deletedNumber = depositPropertiesDAO.updateDeleteFlag(depositId, true); - log.debug("onDeleteDeposit - 'deleted' mark has been set to '{}' for deposit.properties from '{}' ", deletedNumber.isPresent(), depositId); + // The 'move deposit' action is processed in two steps: 1. `create` deposit in the new location; 2. `delete` it from the old location. Ignore delete step + if (record.isPresent()) { + try { + if (Files.isSameFile(Path.of(record.get().getLocation()), Path.of(depositPropertiesFile.getParentFile().getParentFile().getAbsolutePath())) && !depositPropertiesFile.exists()) { + String depositId = depositPropertiesFile.getParentFile().getName(); + Optional deletedNumber = depositPropertiesDAO.updateDeleteFlag(depositId, true); + log.debug("onDepositDelete - 'deleted' mark has been set to '{}' for deposit.properties from '{}' ", deletedNumber.isPresent() && deletedNumber.get() > 0, depositId); + } + } + catch (IOException e) { + writeErrorMsg(e.getMessage()); } } - catch (IOException e) { - log.debug("The 'move deposit' action is processed in two steps: 1. `create` deposit in the new location; 2. `delete` it from the old location. Ignore delete step for {}", depositPropertiesFile.getParentFile().getAbsolutePath()); - } + } + + private void writeErrorMsg(String additionalInfo) { + log.error("Error creating / Updating deposit record: '{}'", additionalInfo); } } \ No newline at end of file diff --git a/src/main/java/nl/knaw/dans/managedeposit/core/service/DepthFileFilter.java b/src/main/java/nl/knaw/dans/managedeposit/core/service/DepthFileFilter.java new file mode 100644 index 0000000..b200053 --- /dev/null +++ b/src/main/java/nl/knaw/dans/managedeposit/core/service/DepthFileFilter.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 DANS - Data Archiving and Networked Services (info@dans.knaw.nl) + * + * 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 nl.knaw.dans.managedeposit.core.service; + +import org.apache.commons.io.filefilter.AbstractFileFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Path; + +public class DepthFileFilter extends AbstractFileFilter { + private static final Logger log = LoggerFactory.getLogger(DepthFileFilter.class); + private final Path absoluteBaseFolder; + private final int requiredNameCount; + + public DepthFileFilter(Path baseFolder, int depthLimit) { + this.absoluteBaseFolder = baseFolder.toAbsolutePath(); + this.requiredNameCount = this.absoluteBaseFolder.getNameCount() + depthLimit; + } + + private boolean confirmParent(File file) { + var path = file.getAbsoluteFile().toPath(); + if (!path.startsWith(absoluteBaseFolder)) { + log.warn(String.format("[%s] must be a child of [%s]", path, absoluteBaseFolder)); + return false; + } + return path.getNameCount() == requiredNameCount; + } + + @Override + public boolean accept(File file) { + return confirmParent(file); + } + + @Override + public boolean accept(File dir, String name) { + return confirmParent(dir); + } +} diff --git a/src/main/java/nl/knaw/dans/managedeposit/core/service/IngestPathMonitor.java b/src/main/java/nl/knaw/dans/managedeposit/core/service/IngestPathMonitor.java index 179b49e..b0cfbd2 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/core/service/IngestPathMonitor.java +++ b/src/main/java/nl/knaw/dans/managedeposit/core/service/IngestPathMonitor.java @@ -18,7 +18,6 @@ import io.dropwizard.lifecycle.Managed; import org.apache.commons.io.IOCase; import org.apache.commons.io.filefilter.FileFilterUtils; -import org.apache.commons.io.filefilter.HiddenFileFilter; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.io.monitor.FileAlterationListenerAdaptor; import org.apache.commons.io.monitor.FileAlterationMonitor; @@ -47,23 +46,31 @@ public IngestPathMonitor(List depositBoxesPaths, DepositStatusUpdater depo } private void startMonitors() throws Exception { - IOFileFilter directories = FileFilterUtils.and(FileFilterUtils.directoryFileFilter(), HiddenFileFilter.VISIBLE); - IOFileFilter files = FileFilterUtils.and(FileFilterUtils.fileFileFilter(), FileFilterUtils.nameFileFilter("deposit.properties", IOCase.INSENSITIVE)); - IOFileFilter filter = FileFilterUtils.or(directories, files); - - log.info("Starting 'IngestPathMonitor', file filter: deposit.properties"); + log.info("Starting 'IngestPathMonitor', with file filter: deposit.properties"); + var observers = new ArrayList<>(); for (Path folder : toMonitorPaths) { - FileAlterationObserver observer = new FileAlterationObserver(folder.toFile(), filter); + IOFileFilter directories = FileFilterUtils.and( + FileFilterUtils.directoryFileFilter(), + new DepthFileFilter(folder, 1) + ); + IOFileFilter files = FileFilterUtils.and( + FileFilterUtils.fileFileFilter(), + FileFilterUtils.nameFileFilter("deposit.properties", IOCase.INSENSITIVE), + new DepthFileFilter(folder, 2) + ); + IOFileFilter filter = FileFilterUtils.or(directories, files); + FileAlterationObserver observer = new FileAlterationObserver(folder.toFile(), filter); observer.addListener(this); + observers.add(observer); - FileAlterationMonitor monitor = new FileAlterationMonitor(this.pollingInterval, observer); - fileAlterationMonitors.add(monitor); - - monitor.start(); - log.debug("'IngestPathMonitor' is going to monitor the folder '{}'", folder); } + + FileAlterationMonitor monitor = new FileAlterationMonitor(this.pollingInterval, observers.toArray(new FileAlterationObserver[0])); + fileAlterationMonitors.add(monitor); + log.debug("'IngestPathMonitor' is going to monitor the folders\n '{}'", toMonitorPaths); + monitor.start(); } @Override @@ -94,19 +101,19 @@ public void stop() throws Exception { @Override public void onFileCreate(File file) { log.debug("onFileCreate: '{}'", file.getAbsolutePath()); - depositStatusUpdater.onCreateDeposit(file); + depositStatusUpdater.onDepositCreate(file); } @Override public void onFileDelete(File file) { log.debug("onFileDelete: '{}'", file.getAbsolutePath()); - depositStatusUpdater.onDeleteDeposit(file); + depositStatusUpdater.onDepositDelete(file); } @Override public void onFileChange(File file) { log.debug("onFileChange: '{}'", file.getAbsolutePath()); - depositStatusUpdater.onChangeDeposit(file); + depositStatusUpdater.onDepositChange(file); } } diff --git a/src/main/java/nl/knaw/dans/managedeposit/core/service/TextTruncation.java b/src/main/java/nl/knaw/dans/managedeposit/core/service/TextTruncation.java index 4192693..f367890 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/core/service/TextTruncation.java +++ b/src/main/java/nl/knaw/dans/managedeposit/core/service/TextTruncation.java @@ -16,9 +16,8 @@ package nl.knaw.dans.managedeposit.core.service; public class TextTruncation { - public static final long pollingInterval = 5 * 1000; - public static final int maxDescriptionLength = 1024; - public static final int maxDirectoryLength = 512; + public static final int MAX_DESCRIPTION_LENGTH = 1024; + public static final int MAX_DIRECTORY_LENGTH = 512; public static String stripEnd(String text, int maxLength) { return text.length() > maxLength ? text.substring(0, maxLength) : text; @@ -29,5 +28,4 @@ public static String stripBegin(String text, int maxLength) { return textLength > maxLength ? text.substring(textLength - maxLength) : text; } - } diff --git a/src/main/java/nl/knaw/dans/managedeposit/db/DepositPropertiesDAO.java b/src/main/java/nl/knaw/dans/managedeposit/db/DepositPropertiesDAO.java index 1db4751..60db574 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/db/DepositPropertiesDAO.java +++ b/src/main/java/nl/knaw/dans/managedeposit/db/DepositPropertiesDAO.java @@ -19,6 +19,8 @@ import nl.knaw.dans.managedeposit.core.DepositProperties; import org.hibernate.SessionFactory; import org.hibernate.query.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaDelete; @@ -26,7 +28,7 @@ import javax.persistence.criteria.CriteriaUpdate; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; -import java.nio.file.Path; +import java.time.DateTimeException; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; @@ -38,6 +40,7 @@ @SuppressWarnings("resource") public class DepositPropertiesDAO extends AbstractDAO { + private static final Logger log = LoggerFactory.getLogger(DepositPropertiesDAO.class); public DepositPropertiesDAO(SessionFactory sessionFactory) { super(sessionFactory); @@ -70,7 +73,7 @@ public List findAll() { public List findSelection(Map> queryParameters) { CriteriaBuilder criteriaBuilder = currentSession().getCriteriaBuilder(); - if (queryParameters.size() == 0) + if (queryParameters.isEmpty()) return findAll(); CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(DepositProperties.class); @@ -83,7 +86,7 @@ public List findSelection(Map> queryPara public Optional deleteSelection(Map> queryParameters) { var criteriaBuilder = currentSession().getCriteriaBuilder(); - if (queryParameters.size() == 0) // Note: all records will be deleted (accidentally) without any specified query parameter + if (queryParameters.isEmpty()) // Note: all records will be deleted (accidentally) without any specified query parameter return Optional.of(0); CriteriaDelete deleteQuery = criteriaBuilder.createCriteriaDelete(DepositProperties.class); @@ -129,20 +132,30 @@ private Predicate buildQueryCriteria(Map> queryParameters, case "startdate": case "enddate": - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - LocalDate date = LocalDate.parse(value, formatter); - var asked_date = OffsetDateTime.of(date.atStartOfDay(), ZoneOffset.UTC); - - if (parameter.equals("startdate")) - orPredicateItem = criteriaBuilder.greaterThan(root.get("depositCreationTimestamp"), asked_date); - else - orPredicateItem = criteriaBuilder.lessThan(root.get("depositCreationTimestamp"), asked_date); + if (value.isEmpty()) { + orPredicateItem = criteriaBuilder.isNull(root.get("depositCreationTimestamp")); + } + else { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate date = LocalDate.parse(value, formatter); + var asked_date = OffsetDateTime.of(date.atStartOfDay(), ZoneOffset.UTC); + + if (parameter.equals("startdate")) + orPredicateItem = criteriaBuilder.greaterThan(root.get("depositCreationTimestamp"), asked_date); + else + orPredicateItem = criteriaBuilder.lessThan(root.get("depositCreationTimestamp"), asked_date); + } + catch (DateTimeException e) { + log.warn("Error parsing the date: {}", e.getMessage()); + continue; + } + } break; default: orPredicateItem = criteriaBuilder.equal(root.get(key), value); } - orPredicatesList.add(orPredicateItem); } @@ -169,18 +182,4 @@ public Optional updateDeleteFlag(String depositId, boolean deleted) { return Optional.of(query.executeUpdate()); } - public Optional updateDepositLocation(String depositId, Path currentParentPath) { - CriteriaBuilder criteriaBuilder = currentSession().getCriteriaBuilder(); - CriteriaUpdate criteriaUpdate = criteriaBuilder.createCriteriaUpdate(DepositProperties.class); - Root root = criteriaUpdate.from(DepositProperties.class); - - Predicate predicate = buildQueryCriteria(Map.of("depositId", List.of(depositId)), criteriaBuilder, root); - criteriaUpdate.where(predicate); - - criteriaUpdate.set("location", currentParentPath.toString()); - - var query = currentSession().createQuery(criteriaUpdate); - return Optional.of(query.executeUpdate()); - } - } diff --git a/src/main/java/nl/knaw/dans/managedeposit/resources/DepositPropertiesResource.java b/src/main/java/nl/knaw/dans/managedeposit/resources/DepositPropertiesResource.java index aef3290..343132c 100644 --- a/src/main/java/nl/knaw/dans/managedeposit/resources/DepositPropertiesResource.java +++ b/src/main/java/nl/knaw/dans/managedeposit/resources/DepositPropertiesResource.java @@ -40,25 +40,27 @@ public DepositPropertiesResource(DepositPropertiesDAO depositPropertiesDAO) { private String writeHelpInfoText() { return - "DD Manage Deposit is running. \n" + - "Usage: \n" + - " - Create reports: GET basePath/report \n" + - " - Clean database: POST basePath/delete-deposit \n" + - " Query string parameters: user, state, startdate, enddate \n" + - " 'startdate'/'enddate' format: yyyy-MM-dd \n" + - " Possible 'state' value: ARCHIVED, DRAFT, FAILED, FINALIZING, INVALID, REJECTED, SUBMITTED, UPLOADED, PUBLISHED \n" + - " Examples: \n" + - " curl -i -X GET basePath/report?startdate=yyyy-MM-dd \n" + - " curl -i -X GET basePath/delete-deposit?user=XXX&state=REJECTED \n" + - " curl -i -X POST basePath/delete-deposit?user=XXX \n" + - " curl -i -X POST basePath/delete-deposit?user=XXX&state=REJECTED"; + """ + DD Manage Deposit is running.\s + Usage:\s + - Create reports: GET basePath/report\s + - Clean database: POST basePath/delete-deposit\s + - Query string parameters: user, state, startdate, enddate, deleted\s + - 'startdate'/'enddate' format: yyyy-MM-dd\s + - 'deleted' is a boolean with values: 'true' or 'false'\s + - Possible 'state' values: ARCHIVED, DRAFT, FAILED, FINALIZING, INVALID, REJECTED, SUBMITTED, UPLOADED, PUBLISHED\s + - To give an undefined parameter (when column's value is empty or null): 'parameterName=' (ex. 'user=')\s + Examples:\s + curl -i -X GET basePath/report?startdate=yyyy-MM-dd\s + curl -i -X GET basePath/delete-deposit?user=XXX&state=REJECTED\s + curl -i -X POST basePath/delete-deposit?user=XXX\s + curl -i -X POST basePath/delete-deposit?user=XXX&state=REJECTED"""; } @GET @UnitOfWork public Response getApiInformation() { - return Response .status(Response.Status.OK) .entity(this.helpInfo) diff --git a/src/test/java/nl/knaw/dans/managedeposit/AbstractDatabaseTest.java b/src/test/java/nl/knaw/dans/managedeposit/AbstractDatabaseTest.java new file mode 100644 index 0000000..c9e88bb --- /dev/null +++ b/src/test/java/nl/knaw/dans/managedeposit/AbstractDatabaseTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 DANS - Data Archiving and Networked Services (info@dans.knaw.nl) + * + * 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 nl.knaw.dans.managedeposit; + +import io.dropwizard.testing.junit5.DAOTestExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import nl.knaw.dans.managedeposit.core.DepositProperties; +import nl.knaw.dans.managedeposit.db.DepositPropertiesDAO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(DropwizardExtensionsSupport.class) +public abstract class AbstractDatabaseTest extends AbstractTestWithTestDir { + protected final DAOTestExtension daoTestExtension = DAOTestExtension.newBuilder() + .addEntityClass(DepositProperties.class) + .build(); + protected DepositPropertiesDAO dao; + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + dao = new DepositPropertiesDAO(daoTestExtension.getSessionFactory()); + } +} diff --git a/src/test/java/nl/knaw/dans/managedeposit/AbstractTestWithTestDir.java b/src/test/java/nl/knaw/dans/managedeposit/AbstractTestWithTestDir.java new file mode 100644 index 0000000..950d084 --- /dev/null +++ b/src/test/java/nl/knaw/dans/managedeposit/AbstractTestWithTestDir.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 DANS - Data Archiving and Networked Services (info@dans.knaw.nl) + * + * 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 nl.knaw.dans.managedeposit; + +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.BeforeEach; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class AbstractTestWithTestDir { + protected final Path testDir = Path.of("target/test") + .resolve(getClass().getSimpleName()); + + @BeforeEach + public void setUp() throws Exception { + FileUtils.deleteDirectory(testDir.toFile()); + } + + /** + * Assume that a bug is not yet fixed. This allows to skip assertions while still showing the code covered by the test. + * + * @param message the message to display + */ + public void assumeNotYetFixed (String message) { + assumeTrue(false, message); + } +} diff --git a/src/test/java/nl/knaw/dans/managedeposit/core/service/DepositStatusUpdaterOnDepositUpdateTest.java b/src/test/java/nl/knaw/dans/managedeposit/core/service/DepositStatusUpdaterOnDepositUpdateTest.java new file mode 100644 index 0000000..6a841ca --- /dev/null +++ b/src/test/java/nl/knaw/dans/managedeposit/core/service/DepositStatusUpdaterOnDepositUpdateTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2023 DANS - Data Archiving and Networked Services (info@dans.knaw.nl) + * + * 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 nl.knaw.dans.managedeposit.core.service; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import nl.knaw.dans.managedeposit.AbstractDatabaseTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.time.OffsetDateTime; + +import static java.nio.file.Files.createDirectories; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +public class DepositStatusUpdaterOnDepositUpdateTest extends AbstractDatabaseTest { + private ListAppender listAppender; + + @BeforeEach + public void setup() throws Exception { + super.setUp(); + var logger = (Logger) LoggerFactory.getLogger(DepositStatusUpdater.class); + listAppender = new ListAppender<>(); + listAppender.start(); + logger.setLevel(Level.DEBUG); + logger.addAppender(listAppender); + } + + @Test + public void onCreateDeposit_should_add_a_db_record() throws IOException { + var depositStatusUpdater = new DepositStatusUpdater(dao); + + // Prepare test data + var propertiesFile = testDir.resolve("bag/deposit.properties"); + createDirectories(propertiesFile.getParent()); + Files.writeString(propertiesFile, """ + creation.timestamp = 2023-08-16T17:40:41.390209+02:00 + depositor.userId = user001 + bag-store.bag-name = revision03 + """); + + // Call the method under test + depositStatusUpdater.onDepositCreate(propertiesFile.toFile()); + + // Check the logs + var formattedMessage = listAppender.list.get(0).getFormattedMessage(); + assertThat(formattedMessage).startsWith("onDepositCreate: A new deposit has been registered '"); + assertThat(formattedMessage).endsWith("DepositStatusUpdaterOnDepositUpdateTest/bag'"); + + // Check the database + var maybeDepositProperties = daoTestExtension.inTransaction(() -> + dao.findById("bag") + ); + assertThat(maybeDepositProperties).isNotEmpty().get() + .hasFieldOrPropertyWithValue("depositId", "bag") + .hasFieldOrPropertyWithValue("depositor", "user001") + .hasFieldOrPropertyWithValue("bagName", "revision03") + .hasFieldOrPropertyWithValue("depositState", "") + .hasFieldOrPropertyWithValue("depositCreationTimestamp", OffsetDateTime.parse("2023-08-16T17:40:41.390209+02:00")) + .hasFieldOrPropertyWithValue("location", testDir.toAbsolutePath().toString()) + .hasFieldOrPropertyWithValue("storageInBytes", 113L); + + var depositPropertiesList = daoTestExtension.inTransaction(() -> + dao.findAll() + ); + assertThat(depositPropertiesList).hasSize(1); + } + + @Test + public void onCreateDeposit_should_create_new_db_record_when_creation_timestamp_is_empty() throws IOException { + var depositStatusUpdater = new DepositStatusUpdater(dao); + + // Prepare test data + var propertiesFile = testDir.resolve("bag/deposit.properties"); + createDirectories(propertiesFile.getParent()); + Files.writeString(propertiesFile, """ + creation.timestamp + depositor.userId = user001 + bag-store.bag-name = revision03 + """); + + // Call the method under test + depositStatusUpdater.onDepositCreate(propertiesFile.toFile()); + + // Check the logs + var formattedMessage = listAppender.list.get(0).getFormattedMessage(); + assertThat(formattedMessage).startsWith("onDepositCreate: A new deposit has been registered '"); + assertThat(formattedMessage).endsWith("DepositStatusUpdaterOnDepositUpdateTest/bag'"); + + // Check the database + var maybeDepositProperties = daoTestExtension.inTransaction(() -> dao.findById("bag")); + assertThat(maybeDepositProperties).isNotEmpty(); + } + + @Test + public void onCreateDeposit_should_create_new_db_record_when_creation_timestamp_is_not_present() throws IOException { + var depositStatusUpdater = new DepositStatusUpdater(dao); + + // Prepare test data + var propertiesFile = testDir.resolve("bag/deposit.properties"); + createDirectories(propertiesFile.getParent()); + Files.writeString(propertiesFile, """ + depositor.userId = user001 + bag-store.bag-name = revision03 + """); + + // Call the method under test + depositStatusUpdater.onDepositCreate(propertiesFile.toFile()); + + // Check the logs + var formattedMessage = listAppender.list.get(0).getFormattedMessage(); + assertThat(formattedMessage).startsWith("onDepositCreate: A new deposit has been registered '"); + assertThat(formattedMessage).endsWith("DepositStatusUpdaterOnDepositUpdateTest/bag'"); + + // Check the database + var maybeDepositProperties = daoTestExtension.inTransaction(() -> dao.findById("bag")); + assertThat(maybeDepositProperties).isNotEmpty(); + } + + @Test + public void onCreateDeposit_should_not_create_new_db_record_when_creation_timestamp_is_invalid() throws IOException { + var depositStatusUpdater = new DepositStatusUpdater(dao); + + // Prepare test data + var propertiesFile = testDir.resolve("bag/deposit.properties"); + createDirectories(propertiesFile.getParent()); + Files.writeString(propertiesFile, """ + creation.timestamp = 2023-08-16 17:40:41.390209+02:00 + depositor.userId = user001 + bag-store.bag-name = revision03 + """); + + // Call the method under test + depositStatusUpdater.onDepositCreate(propertiesFile.toFile()); + + // Check the logs + var formattedMessage = listAppender.list.get(0).getFormattedMessage(); + assertThat(formattedMessage).startsWith("Error creating / Updating deposit record: '"); + assertThat(formattedMessage).endsWith("DepositStatusUpdaterOnDepositUpdateTest/bag'"); + + // Check the database + var maybeDepositProperties = daoTestExtension.inTransaction(() -> dao.findById("bag")); + assertThat(maybeDepositProperties).isEmpty(); + } + + // TODO: other scenario's and test classes for onChangeDeposit and onDeleteDeposit +} \ No newline at end of file diff --git a/src/test/java/nl/knaw/dans/managedeposit/core/service/IngestPathMonitorTest.java b/src/test/java/nl/knaw/dans/managedeposit/core/service/IngestPathMonitorTest.java index 27a320a..b7d2a39 100644 --- a/src/test/java/nl/knaw/dans/managedeposit/core/service/IngestPathMonitorTest.java +++ b/src/test/java/nl/knaw/dans/managedeposit/core/service/IngestPathMonitorTest.java @@ -15,6 +15,135 @@ */ package nl.knaw.dans.managedeposit.core.service; -public class IngestPathMonitorTest { - // Stub -} +import nl.knaw.dans.managedeposit.AbstractTestWithTestDir; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.nio.file.Files; + +import static java.nio.file.Files.createDirectories; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class IngestPathMonitorTest extends AbstractTestWithTestDir { + + private IngestPathMonitor startMonitor(DepositStatusUpdater mockUpdater, int pollingInterval) throws Exception { + var monitor = new IngestPathMonitor(singletonList(testDir), mockUpdater, pollingInterval); + monitor.start(); + Thread.sleep(60); // wait for the monitor to get ready + return monitor; + } + + @Test + public void should_ignore_properties_in_root() throws Exception { + var mockUpdater = Mockito.mock(DepositStatusUpdater.class); + var monitor = startMonitor(mockUpdater, 50); + + createDirectories(testDir); + var propertiesFile = Files.createFile(testDir.resolve("deposit.properties")); + Thread.sleep(70);// Wait for the monitor to pick up the new file + + Mockito.verify(mockUpdater, Mockito.times(0)) + .onDepositCreate(propertiesFile.toFile()); + Mockito.verifyNoMoreInteractions(mockUpdater); + + monitor.stop(); + } + + @Test + public void should_pick_up_new_properties_in_bag() throws Exception { + var mockUpdater = Mockito.mock(DepositStatusUpdater.class); + var monitor = startMonitor(mockUpdater, 50); + + var propertiesFile = testDir.resolve("bag/deposit.properties"); + createDirectories(propertiesFile.getParent()); + Files.createFile(propertiesFile); + Thread.sleep(70);// Wait for the monitor to pick up the new file + + Mockito.verify(mockUpdater, Mockito.times(1)).onDepositCreate(propertiesFile.toFile()); + Mockito.verifyNoMoreInteractions(mockUpdater); + + monitor.stop(); + } + + @Test + public void should_ignore_properties_in_bag_content() throws Exception { + var mockUpdater = Mockito.mock(DepositStatusUpdater.class); + var monitor = startMonitor(mockUpdater, 50); + + var propertiesFile = testDir.resolve("bag/content/deposit.properties"); + createDirectories(propertiesFile.getParent()); + Files.createFile(propertiesFile); + Thread.sleep(70);// Wait for the monitor to pick up the new file + + Mockito.verify(mockUpdater, Mockito.times(0)).onDepositCreate(propertiesFile.toFile()); + Mockito.verifyNoMoreInteractions(mockUpdater); + + monitor.stop(); + } + + + @Test + public void should_pick_up_deleted_bag() throws Exception { + var mockUpdater = Mockito.mock(DepositStatusUpdater.class); + var monitor = startMonitor(mockUpdater, 50); + + var propertiesFile = testDir.resolve("bag/deposit.properties"); + createDirectories(propertiesFile.getParent()); + Files.createFile(propertiesFile); + Thread.sleep(70); + FileUtils.deleteDirectory(propertiesFile.getParent().toFile()); + Thread.sleep(70); + + Mockito.verify(mockUpdater, Mockito.times(1)).onDepositDelete(propertiesFile.toFile()); + + monitor.stop(); + } + + @Test + public void should_pick_up_changed_properties() throws Exception { + var mockUpdater = Mockito.mock(DepositStatusUpdater.class); + var monitor = startMonitor(mockUpdater, 20); + + var propertiesFile = testDir.resolve("bag/deposit.properties"); + createDirectories(propertiesFile.getParent()); + Files.createFile(propertiesFile); + Thread.sleep(30); + Files.writeString(propertiesFile, "just some garbage"); + Thread.sleep(30); + + Mockito.verify(mockUpdater, Mockito.times(1)).onDepositChange(propertiesFile.toFile()); + + monitor.stop(); + } + + @Test + public void should_pick_up_deleted_root() throws Exception { + var mockUpdater = Mockito.mock(DepositStatusUpdater.class); + var monitor = startMonitor(mockUpdater, 20); + + var propertiesFile = testDir.resolve("bag/deposit.properties"); + createDirectories(propertiesFile.getParent()); + Files.createFile(propertiesFile); + Thread.sleep(30); + FileUtils.deleteDirectory(testDir.toFile()); + Thread.sleep(30); + + Mockito.verify(mockUpdater, Mockito.times(1)).onDepositCreate(propertiesFile.toFile()); + Mockito.verifyNoMoreInteractions(mockUpdater); + assumeNotYetFixed("The monitor should pick up the deletion of the root folder, it might imply deletion of many bags"); + monitor.stop(); + } + + @Test + public void should_throw_when_stopping_a_stopped_monitor() throws Exception { + var mockUpdater = Mockito.mock(DepositStatusUpdater.class); + var monitor = startMonitor(mockUpdater, 20); + monitor.stop(); + + assertThatThrownBy(monitor::stop) + .isInstanceOf(RuntimeException.class) + .hasRootCauseInstanceOf(IllegalStateException.class); + } +} \ No newline at end of file