From 9ac1933e4ee042095cb8f312342f20bb08458c7f Mon Sep 17 00:00:00 2001 From: Dou Mok Date: Thu, 29 Jun 2023 12:55:01 -0700 Subject: [PATCH] Add public API method 'deleteMetadata()' plus new junit tests and update javadocs --- .../java/org/dataone/hashstore/HashStore.java | 6 +- .../filehashstore/FileHashStore.java | 77 +++++++++++-- .../FileHashStoreInterfaceTest.java | 108 +++++++++++++++++- 3 files changed, 174 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/dataone/hashstore/HashStore.java b/src/main/java/org/dataone/hashstore/HashStore.java index 3b4db9cd..d3463bd6 100644 --- a/src/main/java/org/dataone/hashstore/HashStore.java +++ b/src/main/java/org/dataone/hashstore/HashStore.java @@ -150,7 +150,11 @@ InputStream retrieveObject(String pid) * @param pid Authority-based identifier * @param formatId Metadata namespace/format * @return - * @throws Exception TODO: Add specific exceptions + * @throws IllegalArgumentException When pid or formatId is null or empty + * @throws FileNotFoundException When requested pid has no metadata + * @throws IOException I/O error when deleting empty directories + * @throws NoSuchAlgorithmException When algorithm used to calcualte object + * address is not supported */ boolean deleteMetadata(String pid, String formatId) throws Exception; diff --git a/src/main/java/org/dataone/hashstore/filehashstore/FileHashStore.java b/src/main/java/org/dataone/hashstore/filehashstore/FileHashStore.java index f32e0d4e..2c3bac45 100644 --- a/src/main/java/org/dataone/hashstore/filehashstore/FileHashStore.java +++ b/src/main/java/org/dataone/hashstore/filehashstore/FileHashStore.java @@ -9,7 +9,6 @@ import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; @@ -24,6 +23,7 @@ import java.util.Map; import java.util.Objects; import java.util.Random; +import java.util.stream.Stream; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @@ -657,9 +657,56 @@ public boolean deleteObject(String pid) } @Override - public boolean deleteMetadata(String pid, String formatId) throws Exception { - // TODO: Implement method - return false; + public boolean deleteMetadata(String pid, String formatId) + throws IllegalArgumentException, FileNotFoundException, IOException, NoSuchAlgorithmException { + logFileHashStore.debug("FileHashStore.deleteMetadata - Called to delete metadata for pid: " + pid); + + if (pid == null || pid.trim().isEmpty()) { + String errMsg = "FileHashStore.deleteMetadata - pid cannot be null or empty, pid: " + pid; + logFileHashStore.error(errMsg); + throw new IllegalArgumentException(errMsg); + } + if (formatId == null || formatId.trim().isEmpty()) { + String errMsg = "FileHashStore.deleteMetadata - formatId cannot be null or empty, formatId: " + pid; + logFileHashStore.error(errMsg); + throw new IllegalArgumentException(errMsg); + } + + // Get permanent address of the pid by calculating its sha-256 hex digest + String metadataCid = this.getPidHexDigest(pid + formatId, OBJECT_STORE_ALGORITHM); + String metadataCidShardString = this.getHierarchicalPathString(this.DIRECTORY_DEPTH, this.DIRECTORY_WIDTH, + metadataCid); + Path metadataCidPath = this.METADATA_STORE_DIRECTORY.resolve(metadataCidShardString); + + // Check to see if object exists + if (!Files.exists(metadataCidPath)) { + String errMsg = "FileHashStore.deleteMetadata - File does not exist for pid: " + pid + + " with metadata address: " + metadataCidPath; + logFileHashStore.warn(errMsg); + throw new FileNotFoundException(errMsg); + } + + // Delete file + Files.delete(metadataCidPath); + + // Then delete any empty directories + Path parent = metadataCidPath.getParent(); + while (parent != null && isDirectoryEmpty(parent)) { + if (parent.equals(this.METADATA_STORE_DIRECTORY)) { + // Do not delete the metadata store directory + break; + + } else { + Files.delete(parent); + logFileHashStore.info("FileHashStore.deleteMetadata - Deleting parent directory for: " + pid + + " with parent address: " + parent); + parent = parent.getParent(); + } + } + + logFileHashStore.info("FileHashStore.deleteMetadata - File deleted for: " + pid + " with metadata address: " + + metadataCidPath); + return true; } @Override @@ -1295,16 +1342,24 @@ protected boolean writeToTmpMetadataFile(File tmpFile, InputStream metadataStrea } /** - * Determines whether a given directory is empty or not - * + * Checks whether a directory is empty or contains files. If a file is found, it + * returns true. + * * @param directory Directory to check - * @return False if not empty - * @throws IOException If I/O occurs when calling Files for a new directory - * stream + * @return True if a file is found or the directory is empty, False otherwise + * @throws IOException If I/O occurs when accessing directory */ private static boolean isDirectoryEmpty(Path directory) throws IOException { - try (DirectoryStream stream = Files.newDirectoryStream(directory)) { - return !stream.iterator().hasNext(); + try (Stream stream = Files.list(directory)) { + // The findFirst() method is called on the stream created from the given + // directory to retrieve the first element. If the stream is empty (i.e., the + // directory is empty), findFirst() will return an empty Optional. + // + // The isPresent() method is called on the Optional returned by + // findFirst(). If the Optional contains a value (i.e., an element was found), + // isPresent() returns true. If the Optional is empty (i.e., the stream is + // empty), isPresent() returns false. + return !stream.findFirst().isPresent(); } } } \ No newline at end of file diff --git a/src/test/java/org/dataone/hashstore/filehashstore/FileHashStoreInterfaceTest.java b/src/test/java/org/dataone/hashstore/filehashstore/FileHashStoreInterfaceTest.java index 6866ed2c..4b84d5d2 100644 --- a/src/test/java/org/dataone/hashstore/filehashstore/FileHashStoreInterfaceTest.java +++ b/src/test/java/org/dataone/hashstore/filehashstore/FileHashStoreInterfaceTest.java @@ -944,7 +944,7 @@ public void deleteObject() throws Exception { // Double check that file doesn't exist assertFalse(Files.exists(objInfo.getAbsPath())); - // Double check that store root still exists + // Double check that object directory still exists Path storePath = (Path) this.fhsProperties.get("storePath"); Path storeObjectPath = storePath.resolve("objects"); assertTrue(Files.exists(storeObjectPath)); @@ -952,7 +952,7 @@ public void deleteObject() throws Exception { } /** - * Confirm that deleteObject deletes object and empty sub directories + * Confirm that deleteObject throws exception when associated pid obj not found */ @Test(expected = FileNotFoundException.class) public void deleteObject_pidNotFound() throws Exception { @@ -960,7 +960,7 @@ public void deleteObject_pidNotFound() throws Exception { } /** - * Confirm that deleteObject deletes object and empty sub directories + * Confirm that deleteObject throws exception when pid is null */ @Test(expected = IllegalArgumentException.class) public void deleteObject_pidNull() throws Exception { @@ -968,7 +968,7 @@ public void deleteObject_pidNull() throws Exception { } /** - * Confirm that deleteObject deletes object and empty sub directories + * Confirm that deleteObject throws exception when pid is empty */ @Test(expected = IllegalArgumentException.class) public void deleteObject_pidEmpty() throws Exception { @@ -976,10 +976,108 @@ public void deleteObject_pidEmpty() throws Exception { } /** - * Confirm that deleteObject deletes object and empty sub directories + * Confirm that deleteObject throws exception when pid is empty spaces */ @Test(expected = IllegalArgumentException.class) public void deleteObject_pidEmptySpaces() throws Exception { fileHashStore.deleteObject(" "); } + + /** + * Confirm that deleteMetadata deletes object and empty sub directories + */ + @Test + public void deleteMetadata() throws Exception { + for (String pid : testData.pidList) { + String pidFormatted = pid.replace("/", "_"); + + // Get test metadata file + Path testMetaDataFile = testData.getTestFile(pidFormatted + ".xml"); + + InputStream metadataStream = Files.newInputStream(testMetaDataFile); + String metadataCid = fileHashStore.storeMetadata(metadataStream, pid, null); + + String storeFormatId = (String) this.fhsProperties.get("storeMetadataNamespace"); + boolean isMetadataDeleted = fileHashStore.deleteMetadata(pid, storeFormatId); + assertTrue(isMetadataDeleted); + + // Double check that file doesn't exist + Path storePath = (Path) this.fhsProperties.get("storePath"); + Path metadataStoreDirectory = storePath.resolve("metadata"); + int storeDepth = (int) this.fhsProperties.get("storeDepth"); + int storeWidth = (int) this.fhsProperties.get("storeWidth"); + String metadataCidShardString = fileHashStore.getHierarchicalPathString(storeDepth, storeWidth, + metadataCid); + Path metadataCidPath = metadataStoreDirectory.resolve(metadataCidShardString); + assertFalse(Files.exists(metadataCidPath)); + + // Double check that metadata directory still exists + Path storeObjectPath = storePath.resolve("metadata"); + assertTrue(Files.exists(storeObjectPath)); + } + } + + /** + * Confirm that deleteMetadata throws exception when associated pid obj not + * found + */ + @Test(expected = FileNotFoundException.class) + public void deleteMetadata_pidNotFound() throws Exception { + String formatId = "http://hashstore.tests/types/v1.0"; + fileHashStore.deleteMetadata("dou.2023.hashstore.1", formatId); + } + + /** + * Confirm that deleteMetadata throws exception when pid is null + */ + @Test(expected = IllegalArgumentException.class) + public void deleteMetadata_pidNull() throws Exception { + String formatId = "http://hashstore.tests/types/v1.0"; + fileHashStore.deleteMetadata(null, formatId); + } + + /** + * Confirm that deleteMetadata throws exception when pid is empty + */ + @Test(expected = IllegalArgumentException.class) + public void deleteMetadata_pidEmpty() throws Exception { + String formatId = "http://hashstore.tests/types/v1.0"; + fileHashStore.deleteMetadata("", formatId); + } + + /** + * Confirm that deleteMetadata throws exception when pid is empty spaces + */ + @Test(expected = IllegalArgumentException.class) + public void deleteMetadata_pidEmptySpaces() throws Exception { + String formatId = "http://hashstore.tests/types/v1.0"; + fileHashStore.deleteMetadata(" ", formatId); + } + + /** + * Confirm that deleteMetadata throws exception when formatId is null + */ + @Test(expected = IllegalArgumentException.class) + public void deleteMetadata_formatIdNull() throws Exception { + String pid = "dou.2023.hashstore.1"; + fileHashStore.deleteMetadata(pid, null); + } + + /** + * Confirm that deleteMetadata throws exception when formatId is empty + */ + @Test(expected = IllegalArgumentException.class) + public void deleteMetadata_formatIdEmpty() throws Exception { + String pid = "dou.2023.hashstore.1"; + fileHashStore.deleteMetadata(pid, ""); + } + + /** + * Confirm that deleteMetadata throws exception when formatId is empty spaces + */ + @Test(expected = IllegalArgumentException.class) + public void deleteMetadata_formatIdEmptySpaces() throws Exception { + String pid = "dou.2023.hashstore.1"; + fileHashStore.deleteMetadata(pid, " "); + } }