diff --git a/common/src/main/java/com/hedera/block/common/utils/FileUtilities.java b/common/src/main/java/com/hedera/block/common/utils/FileUtilities.java index d8dd9ae60..db82a7bdc 100644 --- a/common/src/main/java/com/hedera/block/common/utils/FileUtilities.java +++ b/common/src/main/java/com/hedera/block/common/utils/FileUtilities.java @@ -38,12 +38,11 @@ public final class FileUtilities { * Default permissions are set to: rw-r--r-- */ private static final FileAttribute> DEFAULT_FILE_PERMISSIONS = - PosixFilePermissions.asFileAttribute( - Set.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.OWNER_WRITE, - PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ)); + PosixFilePermissions.asFileAttribute(Set.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ)); /** * Default folder permissions for new folders. @@ -51,91 +50,61 @@ public final class FileUtilities { * Default permissions are set to: rwxr-xr-x */ private static final FileAttribute> DEFAULT_FOLDER_PERMISSIONS = - PosixFilePermissions.asFileAttribute( - Set.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.OWNER_WRITE, - PosixFilePermission.OWNER_EXECUTE, - PosixFilePermission.GROUP_READ, - PosixFilePermission.GROUP_EXECUTE, - PosixFilePermission.OTHERS_READ, - PosixFilePermission.OTHERS_EXECUTE)); + PosixFilePermissions.asFileAttribute(Set.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_EXECUTE)); /** - * Log message template used when a path is not created because a file - * or folder already exists at the requested path. - */ - private static final String PRE_EXISTING_FOLDER_MESSAGE = - "Requested %s [%s] not created because %s already exists at %s"; - - /** - * Create a new path (folder or file) if it does not exist. - * Any folders or files created will use default permissions. + * Create a new path (folder) if it does not exist. + * Any folders created will use default permissions. * * @param toCreate valid, non-null instance of {@link Path} to be created * @param logLevel valid, non-null instance of {@link System.Logger.Level} to use * @param semanticPathName valid, non-blank {@link String} used for logging that represents the * desired path semantically - * @param createDir {@link Boolean} value if we should create a directory or a file * @throws IOException if the path cannot be created */ - public static void createPathIfNotExists( + public static void createFolderPathIfNotExists( @NonNull final Path toCreate, @NonNull final System.Logger.Level logLevel, - @NonNull final String semanticPathName, - final boolean createDir) + @NonNull final String semanticPathName) throws IOException { - createPathIfNotExists( - toCreate, - logLevel, - DEFAULT_FILE_PERMISSIONS, - DEFAULT_FOLDER_PERMISSIONS, - semanticPathName, - createDir); + createFolderPathIfNotExists(toCreate, logLevel, DEFAULT_FOLDER_PERMISSIONS, semanticPathName); } /** - * Create a new path (folder or file) if it does not exist. + * Create a new path (folder) if it does not exist. * * @param toCreate The path to be created. * @param logLevel The logging level to use when logging this event. - * @param filePermissions Permissions to use when creating a new file. - * @param folderPermissions Permissions to use when creating a new folder. - * @param semanticPathName A name to represent the path in a logging + * @param permissions Permissions to use when creating the path. + * @param semanticPathName A name (non-blank) to represent the path in a logging * statement. - * @param createDir A flag indicating we should create a directory - * (true) or a file (false) * @throws IOException if the path cannot be created due to a filesystem * error. */ - public static void createPathIfNotExists( + public static void createFolderPathIfNotExists( @NonNull final Path toCreate, @NonNull final System.Logger.Level logLevel, - @NonNull final FileAttribute> filePermissions, - @NonNull final FileAttribute> folderPermissions, - @NonNull final String semanticPathName, - final boolean createDir) + @NonNull final FileAttribute> permissions, + @NonNull final String semanticPathName) throws IOException { Objects.requireNonNull(toCreate); Objects.requireNonNull(logLevel); - Objects.requireNonNull(filePermissions); - Objects.requireNonNull(folderPermissions); + Objects.requireNonNull(permissions); StringUtilities.requireNotBlank(semanticPathName); - final String requestedType = createDir ? "directory" : "file"; if (Files.notExists(toCreate)) { - if (createDir) { - Files.createDirectories(toCreate, folderPermissions); - } else { - Files.createFile(toCreate, filePermissions); - } - final String logMessage = - "Created %s [%s] at %s".formatted(requestedType, semanticPathName, toCreate); + Files.createDirectories(toCreate, permissions); + final String logMessage = "Created [%s] at '%s'".formatted(semanticPathName, toCreate); LOGGER.log(logLevel, logMessage); } else { - final String actualType = Files.isDirectory(toCreate) ? "directory" : "file"; - final String logMessage = - PRE_EXISTING_FOLDER_MESSAGE.formatted( - requestedType, semanticPathName, actualType, toCreate); + final String logMessage = "Requested [%s] not created because the directory already exists at '%s'" + .formatted(semanticPathName, toCreate); LOGGER.log(logLevel, logMessage); } } diff --git a/common/src/test/java/com/hedera/block/common/utils/FileUtilitiesTest.java b/common/src/test/java/com/hedera/block/common/utils/FileUtilitiesTest.java index dafb5e139..e736bee86 100644 --- a/common/src/test/java/com/hedera/block/common/utils/FileUtilitiesTest.java +++ b/common/src/test/java/com/hedera/block/common/utils/FileUtilitiesTest.java @@ -24,6 +24,7 @@ import java.lang.System.Logger.Level; import java.nio.file.Files; import java.nio.file.Path; +import java.util.function.Consumer; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -31,11 +32,21 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +/** + * Tests for {@link FileUtilities} functionality. + */ class FileUtilitiesTest { - private static final String FILE_WITH_UNRECOGNIZED_EXTENSION = "src/test/resources/nonexistent.unrecognized"; - + /** + * This test aims to verify that a folder path will be created for a given + * path if it does not exist. First we assert that the path we want to make + * does not exist, then we run the actual method and assert that the path + * was created and is an empty folder. + * + * @param tempDir junit temp dir + * @throws IOException propagated from {@link FileUtilities#createFolderPathIfNotExists(Path, Level, String)} + */ @Test - void test_createPathIfNotExists_CreatesDirIfDoesNotExist(@TempDir final Path tempDir) throws IOException { + void testCreateFolderPathIfNotExists(@TempDir final Path tempDir) throws IOException { final String newDir = "newDir"; final Path toCreate = tempDir.resolve(newDir); @@ -44,15 +55,24 @@ void test_createPathIfNotExists_CreatesDirIfDoesNotExist(@TempDir final Path tem assertThat(toCreate).doesNotExist(); // run actual - FileUtilities.createPathIfNotExists(toCreate, Level.ERROR, "test dir 1", true); + FileUtilities.createFolderPathIfNotExists(toCreate, Level.ERROR, "test dir 1"); // assert assertThat(toCreate).exists().isDirectory(); assertThat(tempDir.toFile().listFiles()).hasSize(1).contains(toCreate.toFile()); } + /** + * This test aims to verify that a folder path will not be created for a + * given path if it already exists. First we assert that the path we want to + * make already exists, then we run the actual method and assert that the + * path was unchanged and is an empty folder and nothing else was created. + * + * @param tempDir junit temp dir + * @throws IOException propagated from {@link FileUtilities#createFolderPathIfNotExists(Path, Level, String)} + */ @Test - void test_createPathIfNotExists_DoesNotCreateDirIfExists(@TempDir final Path tempDir) throws IOException { + void testSkipFolderCreationIfPathExists(@TempDir final Path tempDir) throws IOException { final String newDir = "newDir"; final Path toCreate = tempDir.resolve(newDir); @@ -70,60 +90,24 @@ void test_createPathIfNotExists_DoesNotCreateDirIfExists(@TempDir final Path tem assertThat(toCreate).exists().isDirectory(); // run actual - FileUtilities.createPathIfNotExists(toCreate, Level.ERROR, "test dir 1", true); + FileUtilities.createFolderPathIfNotExists(toCreate, Level.ERROR, "test dir 1"); // assert assertThat(toCreate).exists().isDirectory(); assertThat(tempDirAsFile.listFiles()).hasSize(1).contains(toCreateAsFile); } - @Test - void test_createPathIfNotExists_CreatesFileIfDoesNotExist(@TempDir final Path tempDir) throws IOException { - final String newFile = "newFile"; - final Path toCreate = tempDir.resolve(newFile); - - // ensure the temp directory is empty in the beginning - assertThat(tempDir).isEmptyDirectory(); - assertThat(toCreate).doesNotExist(); - - // run actual - FileUtilities.createPathIfNotExists(toCreate, Level.ERROR, "test file 1", false); - - // assert - assertThat(toCreate).exists().isEmptyFile(); - assertThat(tempDir.toFile().listFiles()).hasSize(1).contains(toCreate.toFile()); - } - - @Test - void test_createPathIfNotExists_DoesNotCreateFileIfExists(@TempDir final Path tempDir) throws IOException { - final String newFile = "newFile"; - final Path toCreate = tempDir.resolve(newFile); - - // ensure the temp directory is empty in the beginning - assertThat(tempDir).isEmptyDirectory(); - assertThat(toCreate).doesNotExist(); - - // create 'newFile' - Files.createFile(toCreate); - - // ensure the temp directory contains only 'newFile' before running actual - final File tempDirAsFile = tempDir.toFile(); - final File toCreateAsFile = toCreate.toFile(); - assertThat(tempDirAsFile.listFiles()).hasSize(1).contains(toCreateAsFile); - assertThat(toCreate).exists().isEmptyFile(); - - // run actual - FileUtilities.createPathIfNotExists(toCreate, Level.ERROR, "test file 1", false); - - // assert - assertThat(toCreate).exists().isEmptyFile(); - assertThat(tempDirAsFile.listFiles()).hasSize(1).contains(toCreateAsFile); - } - + /** + * This test aims to verify that reading a gzip file that exists and is + * valid, will return a byte array with the expected content. + * + * @param filePath parameterized, to read gzip files from + * @param expectedContent parameterized, expected content after reading the file + * @throws IOException propagated from {@link FileUtilities#readGzipFileUnsafe(Path)} + */ @ParameterizedTest @MethodSource("validGzipFiles") - void test_readGzipFileUnsafe_ReturnsByteArrayWithValidContentForValidGzipFile( - final Path filePath, final String expectedContent) throws IOException { + void testReadGzipFileUnsafe(final Path filePath, final String expectedContent) throws IOException { final byte[] actualContent = FileUtilities.readGzipFileUnsafe(filePath); assertThat(actualContent) .isNotNull() @@ -134,63 +118,77 @@ void test_readGzipFileUnsafe_ReturnsByteArrayWithValidContentForValidGzipFile( .isEqualTo(expectedContent); } + /** + * This test aims to verify that reading an invalid gzip file throws an + * {@link IOException}. + * + * @param filePath parameterized, to read gzip files from + */ @ParameterizedTest @MethodSource("invalidFiles") - void test_readGzipFileUnsafe_ThrowsIOExceptionForInvalidGzipFile(final Path filePath) { + void testReadGzipFileUnsafeThrows(final Path filePath) { assertThatIOException().isThrownBy(() -> FileUtilities.readGzipFileUnsafe(filePath)); } + /** + * This test aims to verify that reading a file that exists and is valid and + * is found by the extension parameter, will return a byte array with the + * expected content. + * + * @param filePath parameterized, to read files from + * @param expectedContent parameterized, expected content after reading the file + * @throws IOException propagated from {@link FileUtilities#readFileBytesUnsafe(Path)} + * and {@link FileUtilities#readFileBytesUnsafe(Path, String, String)} + */ @ParameterizedTest @MethodSource({"validGzipFiles", "validBlkFiles"}) - void test_readFileBytesUnsafe_ReturnsByteArrayWithValidContentForValidFile( - final Path filePath, final String expectedContent) throws IOException { - final byte[] actualContent = FileUtilities.readFileBytesUnsafe(filePath); - assertThat(actualContent) - .isNotNull() - .isNotEmpty() - .asString() - .isNotNull() - .isNotBlank() - .isEqualTo(expectedContent); - } - - @Test - void test_readFileBytesUnsafe_ReturnsNullByteArrayWhenExtensionIsNotRecognized() throws IOException { - final byte[] actualContent = FileUtilities.readFileBytesUnsafe(Path.of(FILE_WITH_UNRECOGNIZED_EXTENSION)); - assertThat(actualContent).isNull(); - } - - @ParameterizedTest - @MethodSource("invalidFiles") - void test_readFileBytesUnsafe_ThrowsIOExceptionForInvalidGzipFile(final Path filePath) { - assertThatIOException().isThrownBy(() -> FileUtilities.readFileBytesUnsafe(filePath)); - } + void testReadFileBytesUnsafe(final Path filePath, final String expectedContent) throws IOException { + final Consumer asserts = actual -> { + assertThat(actual) + .isNotNull() + .isNotEmpty() + .asString() + .isNotNull() + .isNotBlank() + .isEqualTo(expectedContent); + }; - @ParameterizedTest - @MethodSource({"validGzipFiles", "validBlkFiles"}) - void test_readFileBytesUnsafe_ReturnsByteArrayWithValidContentForValidFileWithGivenExtension( - final Path filePath, final String expectedContent) throws IOException { final byte[] actualContent = FileUtilities.readFileBytesUnsafe(filePath, ".blk", ".gz"); - assertThat(actualContent) - .isNotNull() - .isNotEmpty() - .asString() - .isNotNull() - .isNotBlank() - .isEqualTo(expectedContent); + assertThat(actualContent).satisfies(asserts); + + // overloaded has same extensions as above + final byte[] actualContentOverloaded = FileUtilities.readFileBytesUnsafe(filePath); + assertThat(actualContentOverloaded).satisfies(asserts); } + /** + * This test aims to verify that reading a file that is not recognized by + * the block file extension we provide, will return null. + * + * @throws IOException propagated from {@link FileUtilities#readFileBytesUnsafe(Path)} + * and {@link FileUtilities#readFileBytesUnsafe(Path, String, String)} + */ @Test - void test_readFileBytesUnsafe_ReturnsNullByteArrayWhenExtensionIsNotRecognizedWithGivenExtension() - throws IOException { - final byte[] actualContent = - FileUtilities.readFileBytesUnsafe(Path.of(FILE_WITH_UNRECOGNIZED_EXTENSION), ".blk", ".gz"); + void testReadFileBytesUnsafeReturnsNull() throws IOException { + final Path path = Path.of("src/test/resources/nonexistent.unrecognized"); + + final byte[] actualContent = FileUtilities.readFileBytesUnsafe(path, ".blk", ".gz"); assertThat(actualContent).isNull(); + + final byte[] actualContentOverloaded = FileUtilities.readFileBytesUnsafe(path); + assertThat(actualContentOverloaded).isNull(); } + /** + * This test aims to verify that reading an invalid file, be that it is + * in some way corrupted or nonexistent, will throw an {@link IOException}. + * + * @param filePath parameterized, to read block files from + */ @ParameterizedTest @MethodSource("invalidFiles") - void test_readFileBytesUnsafe_ThrowsIOExceptionForInvalidGzipFileWithGivenExtension(final Path filePath) { + void testReadFileBytesUnsafeThrows(final Path filePath) { + assertThatIOException().isThrownBy(() -> FileUtilities.readFileBytesUnsafe(filePath)); assertThatIOException().isThrownBy(() -> FileUtilities.readFileBytesUnsafe(filePath, ".blk", ".gz")); } @@ -208,6 +206,8 @@ private static Stream validBlkFiles() { private static Stream invalidFiles() { return Stream.of( - Arguments.of("src/test/resources/invalid1.gz"), Arguments.of("src/test/resources/nonexistent.gz")); + Arguments.of("src/test/resources/invalid1.gz"), + Arguments.of("src/test/resources/nonexistent.gz"), + Arguments.of("src/test/resources/nonexistent.blk")); } } diff --git a/common/src/test/java/com/hedera/block/common/utils/StringUtilitiesTest.java b/common/src/test/java/com/hedera/block/common/utils/StringUtilitiesTest.java index 7576845fd..60777925a 100644 --- a/common/src/test/java/com/hedera/block/common/utils/StringUtilitiesTest.java +++ b/common/src/test/java/com/hedera/block/common/utils/StringUtilitiesTest.java @@ -26,30 +26,47 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +/** + * Tests for {@link StringUtilities} functionality. + */ class StringUtilitiesTest { + /** + * This test aims to verify that the {@link StringUtilities#requireNotBlank(String)} will return the input + * 'toTest' parameter if the non-blank check passes. + * + * @param toTest parameterized, the string to test + */ @ParameterizedTest @MethodSource("nonBlankStrings") - void test_requireNotBlank_ReturnsInputStringIfItIsNotBlankOrNull(final String toTest) { + void testRequireNotBlankPass(final String toTest) { final String actual = StringUtilities.requireNotBlank(toTest); assertThat(actual).isNotNull().isNotBlank().isEqualTo(toTest); } + /** + * This test aims to verify that the {@link StringUtilities#requireNotBlank(String)} will throw an + * {@link IllegalArgumentException} if the non-blank check fails. + * + * @param toTest parameterized, the string to test + */ @ParameterizedTest @MethodSource("blankStrings") - void test_requireNotBlank_ThrowsIllegalArgumentExceptionIfInputStringIsBlank( - final String toTest) { + void testRequireNotBlankFail(final String toTest) { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> StringUtilities.requireNotBlank(toTest)); } + /** + * This test aims to verify that the {@link StringUtilities#requireNotBlank(String)} will throw a + * {@link NullPointerException} if the input string to test is null. + */ @Test void test_requireNotBlank_ThrowsNullPointerExceptionIfInputStringIsNull() { assertThatNullPointerException().isThrownBy(() -> StringUtilities.requireNotBlank(null)); } /** - * Some simple non-blank strings only with spaces and new lines and tabs (there are more - * whitespace chars). + * Some simple non-blank strings only with spaces and new lines and tabs (there are more whitespace chars). */ private static Stream nonBlankStrings() { return Stream.of( diff --git a/server/src/main/java/com/hedera/block/server/Constants.java b/server/src/main/java/com/hedera/block/server/Constants.java index e471d213d..72f085fda 100644 --- a/server/src/main/java/com/hedera/block/server/Constants.java +++ b/server/src/main/java/com/hedera/block/server/Constants.java @@ -16,36 +16,31 @@ package com.hedera.block.server; -import edu.umd.cs.findbugs.annotations.NonNull; - /** Constants used in the BlockNode service. */ public final class Constants { - private Constants() {} - - /** Constant mapped to the application.properties file in resources with default values */ - @NonNull public static final String APPLICATION_PROPERTIES = "app.properties"; - - /** - * Constant mapped to the Helidon logging.properties file in the docker directory with default - * values. - */ - @NonNull public static final String LOGGING_PROPERTIES = "logging.properties"; + /** Constant mapped to the semantic name of the Block Node root directory */ + public static final String BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME = "Block Node Root Directory"; /** Constant mapped to the name of the BlockStream service in the .proto file */ - @NonNull public static final String SERVICE_NAME_BLOCK_STREAM = "BlockStreamService"; + public static final String SERVICE_NAME_BLOCK_STREAM = "BlockStreamService"; /** Constant mapped to the name of the BlockAccess service in the .proto file */ - @NonNull public static final String SERVICE_NAME_BLOCK_ACCESS = "BlockAccessService"; + public static final String SERVICE_NAME_BLOCK_ACCESS = "BlockAccessService"; /** Constant mapped to the publishBlockStream service method name in the .proto file */ - @NonNull public static final String CLIENT_STREAMING_METHOD_NAME = "publishBlockStream"; + public static final String CLIENT_STREAMING_METHOD_NAME = "publishBlockStream"; /** Constant mapped to the subscribeBlockStream service method name in the .proto file */ - @NonNull public static final String SERVER_STREAMING_METHOD_NAME = "subscribeBlockStream"; + public static final String SERVER_STREAMING_METHOD_NAME = "subscribeBlockStream"; /** Constant mapped to the singleBlock service method name in the .proto file */ - @NonNull public static final String SINGLE_BLOCK_METHOD_NAME = "singleBlock"; + public static final String SINGLE_BLOCK_METHOD_NAME = "singleBlock"; /** Constant defining the block file extension */ - @NonNull public static final String BLOCK_FILE_EXTENSION = ".blk"; + public static final String BLOCK_FILE_EXTENSION = ".blk"; + + /** Constant defining the compressed file extension */ + public static final String COMPRESSED_FILE_EXTENSION = ".zstd"; + + private Constants() {} } diff --git a/server/src/main/java/com/hedera/block/server/Server.java b/server/src/main/java/com/hedera/block/server/Server.java index 2c525ea21..fa9a215d7 100644 --- a/server/src/main/java/com/hedera/block/server/Server.java +++ b/server/src/main/java/com/hedera/block/server/Server.java @@ -16,8 +16,8 @@ package com.hedera.block.server; -import static com.hedera.block.server.Constants.APPLICATION_PROPERTIES; -import static com.hedera.block.server.Constants.LOGGING_PROPERTIES; +import static com.hedera.block.common.constants.StringsConstants.APPLICATION_PROPERTIES; +import static com.hedera.block.common.constants.StringsConstants.LOGGING_PROPERTIES; import static io.helidon.config.ConfigSources.file; import static java.lang.System.Logger; import static java.lang.System.Logger.Level.INFO; @@ -49,21 +49,19 @@ public static void main(final String[] args) throws IOException { LOGGER.log(INFO, "Starting BlockNode Server"); // Set the global configuration - final Config config = - Config.builder() - .sources(file(Paths.get("/app", LOGGING_PROPERTIES)).optional()) - .build(); + final Config config = Config.builder() + .sources(file(Paths.get("/app", LOGGING_PROPERTIES)).optional()) + .build(); Config.global(config); // Init BlockNode Configuration - final Configuration configuration = - ConfigurationBuilder.create() - .withSource(ServerMappedConfigSourceInitializer.getMappedConfigSource()) - .withSource(SystemPropertiesConfigSource.getInstance()) - .withSources(new ClasspathFileConfigSource(Path.of(APPLICATION_PROPERTIES))) - .autoDiscoverExtensions() - .build(); + final Configuration configuration = ConfigurationBuilder.create() + .withSource(ServerMappedConfigSourceInitializer.getMappedConfigSource()) + .withSource(SystemPropertiesConfigSource.getInstance()) + .withSources(new ClasspathFileConfigSource(Path.of(APPLICATION_PROPERTIES))) + .autoDiscoverExtensions() + .build(); // Init Dagger DI Component, passing in the configuration. // this is where all the dependencies are wired up (magic happens) diff --git a/server/src/main/java/com/hedera/block/server/persistence/storage/FileUtils.java b/server/src/main/java/com/hedera/block/server/persistence/storage/FileUtils.java deleted file mode 100644 index 7e5727452..000000000 --- a/server/src/main/java/com/hedera/block/server/persistence/storage/FileUtils.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * 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 com.hedera.block.server.persistence.storage; - -import static java.lang.System.Logger; - -import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.Set; - -/** FileUtils methods provide common functionality for the storage package. */ -public final class FileUtils { - - private static final Logger LOGGER = System.getLogger(FileUtils.class.getName()); - - private FileUtils() {} - - /** - * Default file permissions defines the file and directory for the storage package. - * - *

Default permissions are set to: rwxr-xr-x - */ - @NonNull - public static final FileAttribute> defaultPerms = - PosixFilePermissions.asFileAttribute( - Set.of( - PosixFilePermission.OWNER_READ, - PosixFilePermission.OWNER_WRITE, - PosixFilePermission.OWNER_EXECUTE, - PosixFilePermission.GROUP_READ, - PosixFilePermission.GROUP_EXECUTE, - PosixFilePermission.OTHERS_READ, - PosixFilePermission.OTHERS_EXECUTE)); - - /** - * Use this to create a Dir if it does not exist with the given permissions and log the result. - * - * @param blockNodePath the path to create - * @param logLevel the log level to use - * @param perms the permissions to use when creating the directory - * @throws IOException if the directory cannot be created - */ - public static void createPathIfNotExists( - @NonNull final Path blockNodePath, - @NonNull final System.Logger.Level logLevel, - @NonNull FileAttribute> perms) - throws IOException { - // Initialize the Block directory if it does not exist - if (Files.notExists(blockNodePath)) { - Files.createDirectory(blockNodePath, perms); - LOGGER.log(logLevel, "Created block node root directory: " + blockNodePath); - } else { - LOGGER.log(logLevel, "Using existing block node root directory: " + blockNodePath); - } - } -} diff --git a/server/src/main/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfig.java b/server/src/main/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfig.java index 490549279..0e626f5de 100644 --- a/server/src/main/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfig.java +++ b/server/src/main/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfig.java @@ -16,13 +16,14 @@ package com.hedera.block.server.persistence.storage; +import static com.hedera.block.server.Constants.BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME; import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.INFO; +import com.hedera.block.common.utils.FileUtilities; import com.swirlds.config.api.ConfigData; import com.swirlds.config.api.ConfigProperty; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -34,8 +35,7 @@ */ @ConfigData("persistence.storage") public record PersistenceStorageConfig(@ConfigProperty(defaultValue = "") String rootPath) { - private static final System.Logger LOGGER = - System.getLogger(PersistenceStorageConfig.class.getName()); + private static final System.Logger LOGGER = System.getLogger(PersistenceStorageConfig.class.getName()); /** * Constructor to set the default root path if not provided, it will be set to the data @@ -52,12 +52,13 @@ public record PersistenceStorageConfig(@ConfigProperty(defaultValue = "") String throw new IllegalArgumentException(rootPath + " Root path must be absolute"); } // Create Directory if it does not exist - if (Files.notExists(path)) { - try { - FileUtils.createPathIfNotExists(path, ERROR, FileUtils.defaultPerms); - } catch (IOException e) { - throw new RuntimeException(e); - } + try { + FileUtilities.createFolderPathIfNotExists(path, ERROR, BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME); + } catch (final IOException e) { + final String message = + "Unable to instantiate [%s]! Unable to create the root directory for the block storage [%s]" + .formatted(this.getClass().getName(), path); + throw new RuntimeException(message, e); } LOGGER.log(INFO, "Persistence Storage configuration persistence.storage.rootPath: " + path); diff --git a/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReader.java b/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReader.java index 4ff7484f1..908776b81 100644 --- a/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReader.java +++ b/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReader.java @@ -36,8 +36,10 @@ import java.nio.file.Path; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -46,23 +48,20 @@ * containing block items. The block items are stored as files within the block directory. */ class BlockAsDirReader implements BlockReader { - private final Logger LOGGER = System.getLogger(getClass().getName()); - private final Path blockNodeRootPath; - private final FileAttribute> filePerms; + private final FileAttribute> folderPermissions; /** * Constructor for the BlockAsDirReader class. It initializes the BlockAsDirReader with the * given parameters. * * @param config the configuration to retrieve the block node root path - * @param filePerms the file permissions to set on the block node root path + * @param folderPermissions the folder permissions to set on the block node root path, default will be used if null provided */ BlockAsDirReader( @NonNull final PersistenceStorageConfig config, - @NonNull final FileAttribute> filePerms) { - + final FileAttribute> folderPermissions) { LOGGER.log(INFO, "Initializing FileSystemBlockReader"); final Path blockNodeRootPath = Path.of(config.rootPath()); @@ -71,7 +70,20 @@ class BlockAsDirReader implements BlockReader { LOGGER.log(INFO, "Block Node Root Path: " + blockNodeRootPath); this.blockNodeRootPath = blockNodeRootPath; - this.filePerms = filePerms; + + if (Objects.nonNull(folderPermissions)) { + this.folderPermissions = folderPermissions; + } else { + // default permissions for folders + this.folderPermissions = PosixFilePermissions.asFileAttribute(Set.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_EXECUTE)); + } } /** @@ -133,8 +145,7 @@ public Optional read(final long blockNumber) throws IOException, ParseExc } @NonNull - private Optional readBlockItem(@NonNull final String blockItemPath) - throws IOException, ParseException { + private Optional readBlockItem(@NonNull final String blockItemPath) throws IOException, ParseException { try (final FileInputStream fis = new FileInputStream(blockItemPath)) { @@ -177,7 +188,7 @@ private boolean isPathDisqualified(@NonNull final Path path) { try { // If resetting the permissions fails or // if the path is still unreadable, return true. - setPerm(path, filePerms.value()); + setPerm(path, folderPermissions.value()); if (!path.toFile().canRead()) { return true; } @@ -202,8 +213,7 @@ private boolean isPathDisqualified(@NonNull final Path path) { * @param perms the permissions to set on the path * @throws IOException if an I/O error occurs */ - protected void setPerm(@NonNull final Path path, @NonNull final Set perms) - throws IOException { + protected void setPerm(@NonNull final Path path, @NonNull final Set perms) throws IOException { Files.setPosixFilePermissions(path, perms); } } diff --git a/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderBuilder.java b/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderBuilder.java index 0ee7936d6..6bb4f7da2 100644 --- a/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderBuilder.java +++ b/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderBuilder.java @@ -16,12 +16,12 @@ package com.hedera.block.server.persistence.storage.read; -import com.hedera.block.server.persistence.storage.FileUtils; import com.hedera.block.server.persistence.storage.PersistenceStorageConfig; import com.hedera.hapi.block.stream.Block; import edu.umd.cs.findbugs.annotations.NonNull; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; +import java.util.Objects; import java.util.Set; /** @@ -29,13 +29,12 @@ * *

When a block reader is created, it will provide access to read blocks from storage. */ -public class BlockAsDirReaderBuilder { - - private FileAttribute> filePerms = FileUtils.defaultPerms; +public final class BlockAsDirReaderBuilder { private final PersistenceStorageConfig config; + private FileAttribute> folderPermissions; - private BlockAsDirReaderBuilder(@NonNull PersistenceStorageConfig config) { - this.config = config; + private BlockAsDirReaderBuilder(@NonNull final PersistenceStorageConfig config) { + this.config = Objects.requireNonNull(config); } /** @@ -46,7 +45,7 @@ private BlockAsDirReaderBuilder(@NonNull PersistenceStorageConfig config) { * @return a block reader builder configured with required parameters. */ @NonNull - public static BlockAsDirReaderBuilder newBuilder(@NonNull PersistenceStorageConfig config) { + public static BlockAsDirReaderBuilder newBuilder(@NonNull final PersistenceStorageConfig config) { return new BlockAsDirReaderBuilder(config); } @@ -54,17 +53,13 @@ public static BlockAsDirReaderBuilder newBuilder(@NonNull PersistenceStorageConf * Optionally, provide file permissions for the block reader to use when managing block files * and directories. * - *

By default, the block reader will use the permissions defined in {@link - * FileUtils#defaultPerms}. This method is primarily used for testing purposes. Default values - * should be sufficient for production use. - * - * @param filePerms the file permissions to use when managing block files and directories. + * @param folderPermissions the folder permissions to use when managing block files as directories. * @return a block reader builder configured with required parameters. */ @NonNull - public BlockAsDirReaderBuilder filePerms( - @NonNull final FileAttribute> filePerms) { - this.filePerms = filePerms; + public BlockAsDirReaderBuilder folderPermissions( + @NonNull final FileAttribute> folderPermissions) { + this.folderPermissions = Objects.requireNonNull(folderPermissions); return this; } @@ -75,6 +70,6 @@ public BlockAsDirReaderBuilder filePerms( */ @NonNull public BlockReader build() { - return new BlockAsDirReader(config, filePerms); + return new BlockAsDirReader(config, folderPermissions); } } diff --git a/server/src/main/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemover.java b/server/src/main/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemover.java index 3a108594a..5fbc4a161 100644 --- a/server/src/main/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemover.java +++ b/server/src/main/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemover.java @@ -24,32 +24,23 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.PosixFilePermission; -import java.util.Set; +import java.util.Objects; /** * The BlockAsDirRemover class removes a block from the file system. The block is stored as a * directory containing block items. The block items are stored as files within the block directory. */ public class BlockAsDirRemover implements BlockRemover { - private final Logger LOGGER = System.getLogger(getClass().getName()); - private final Path blockNodeRootPath; - private final FileAttribute> filePerms; /** * Create a block remover to manage removing blocks from storage. * * @param blockNodeRootPath the root path where blocks are stored. - * @param filePerms the file permissions used to manage removing blocks. */ - public BlockAsDirRemover( - @NonNull final Path blockNodeRootPath, - @NonNull final FileAttribute> filePerms) { - this.blockNodeRootPath = blockNodeRootPath; - this.filePerms = filePerms; + public BlockAsDirRemover(@NonNull final Path blockNodeRootPath) { + this.blockNodeRootPath = Objects.requireNonNull(blockNodeRootPath); } /** @@ -60,7 +51,6 @@ public BlockAsDirRemover( */ @Override public void remove(final long id) throws IOException { - // Calculate the block path and proactively set the permissions // for removal final Path blockPath = blockNodeRootPath.resolve(String.valueOf(id)); @@ -68,9 +58,6 @@ public void remove(final long id) throws IOException { LOGGER.log(ERROR, "Block does not exist: {0}", id); return; } - - Files.setPosixFilePermissions(blockPath, filePerms.value()); - // Best effort to delete the block if (!delete(blockPath.toFile())) { LOGGER.log(ERROR, "Failed to delete block: {0}", id); @@ -78,7 +65,6 @@ public void remove(final long id) throws IOException { } private static boolean delete(@NonNull final File file) { - // Recursively delete the contents // of the directory if (file.isDirectory()) { @@ -89,7 +75,6 @@ private static boolean delete(@NonNull final File file) { } } } - return file.delete(); } } diff --git a/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriter.java b/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriter.java index 74fe72a1d..f6457ae60 100644 --- a/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriter.java +++ b/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriter.java @@ -17,15 +17,16 @@ package com.hedera.block.server.persistence.storage.write; import static com.hedera.block.server.Constants.BLOCK_FILE_EXTENSION; +import static com.hedera.block.server.Constants.BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME; import static com.hedera.block.server.metrics.BlockNodeMetricTypes.Counter.BlocksPersisted; import static java.lang.System.Logger; import static java.lang.System.Logger.Level.DEBUG; import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.INFO; +import com.hedera.block.common.utils.FileUtilities; import com.hedera.block.server.config.BlockNodeContext; import com.hedera.block.server.metrics.MetricsService; -import com.hedera.block.server.persistence.storage.FileUtils; import com.hedera.block.server.persistence.storage.PersistenceStorageConfig; import com.hedera.block.server.persistence.storage.remove.BlockRemover; import com.hedera.hapi.block.stream.BlockItem; @@ -36,7 +37,9 @@ import java.nio.file.Path; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -50,46 +53,56 @@ * caller. */ class BlockAsDirWriter implements BlockWriter> { - private final Logger LOGGER = System.getLogger(getClass().getName()); - private final Path blockNodeRootPath; - private long blockNodeFileNameIndex; - private Path currentBlockDir; - private final FileAttribute> filePerms; + private final FileAttribute> folderPermissions; private final BlockRemover blockRemover; private final MetricsService metricsService; + private long blockNodeFileNameIndex; + private Path currentBlockDir; /** * Use the corresponding builder to construct a new BlockAsDirWriter with the given parameters. * * @param blockRemover the block remover to use for removing blocks - * @param filePerms the file permissions to use for writing blocks + * @param folderPermissions the folder permissions to use for writing blocks, if null provided then defaults will be used * @param blockNodeContext the block node context to use for writing blocks * @throws IOException if an error occurs while initializing the BlockAsDirWriter */ BlockAsDirWriter( @NonNull final BlockRemover blockRemover, - @NonNull final FileAttribute> filePerms, + final FileAttribute> folderPermissions, @NonNull final BlockNodeContext blockNodeContext) throws IOException { - LOGGER.log(INFO, "Initializing FileSystemBlockStorage"); - PersistenceStorageConfig config = + final PersistenceStorageConfig config = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); - final Path blockNodeRootPath = Path.of(config.rootPath()); + final Path blockNodeRootPath = Path.of(config.rootPath()); LOGGER.log(INFO, "Block Node Root Path: " + blockNodeRootPath); this.blockNodeRootPath = blockNodeRootPath; this.blockRemover = blockRemover; - this.filePerms = filePerms; + this.metricsService = blockNodeContext.metricsService(); - // Initialize the block node root directory if it does not exist - FileUtils.createPathIfNotExists(blockNodeRootPath, INFO, filePerms); + if (Objects.nonNull(folderPermissions)) { + this.folderPermissions = folderPermissions; + } else { + // default permissions for folders + this.folderPermissions = PosixFilePermissions.asFileAttribute(Set.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_EXECUTE)); + } - this.metricsService = blockNodeContext.metricsService(); + // Initialize the block node root directory if it does not exist + FileUtilities.createFolderPathIfNotExists( + blockNodeRootPath, INFO, this.folderPermissions, BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME); } /** @@ -99,8 +112,7 @@ class BlockAsDirWriter implements BlockWriter> { * @throws IOException if an error occurs while writing the block item */ @Override - public Optional> write(@NonNull final List blockItems) - throws IOException { + public Optional> write(@NonNull final List blockItems) throws IOException { if (blockItems.getFirst().hasBlockHeader()) { resetState(blockItems.getFirst()); @@ -147,8 +159,7 @@ public Optional> write(@NonNull final List blockItems * @param blockItem the block item to write * @throws IOException if an error occurs while writing the block item */ - protected void write(@NonNull final Path blockItemFilePath, @NonNull final BlockItem blockItem) - throws IOException { + protected void write(@NonNull final Path blockItemFilePath, @NonNull final BlockItem blockItem) throws IOException { try (final FileOutputStream fos = new FileOutputStream(blockItemFilePath.toString())) { BlockItem.PROTOBUF.toBytes(blockItem).writeTo(fos); @@ -169,7 +180,8 @@ private void resetState(@NonNull final BlockItem blockItem) throws IOException { repairPermissions(blockNodeRootPath); // Construct the path to the block directory - FileUtils.createPathIfNotExists(calculateBlockPath(), DEBUG, filePerms); + FileUtilities.createFolderPathIfNotExists( + calculateBlockPath(), DEBUG, folderPermissions, BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME); // Reset blockNodeFileNameIndex = 0; @@ -178,13 +190,10 @@ private void resetState(@NonNull final BlockItem blockItem) throws IOException { private void repairPermissions(@NonNull final Path path) throws IOException { final boolean isWritable = Files.isWritable(path); if (!isWritable) { - LOGGER.log( - ERROR, - "Block node root directory is not writable. Attempting to change the" - + " permissions."); + LOGGER.log(ERROR, "Block node root directory is not writable. Attempting to change the" + " permissions."); // Attempt to restore the permissions on the block node root directory - Files.setPosixFilePermissions(path, filePerms.value()); + Files.setPosixFilePermissions(path, folderPermissions.value()); } } diff --git a/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterBuilder.java b/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterBuilder.java index ee6fee813..c5378dcc9 100644 --- a/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterBuilder.java +++ b/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterBuilder.java @@ -17,7 +17,6 @@ package com.hedera.block.server.persistence.storage.write; import com.hedera.block.server.config.BlockNodeContext; -import com.hedera.block.server.persistence.storage.FileUtils; import com.hedera.block.server.persistence.storage.PersistenceStorageConfig; import com.hedera.block.server.persistence.storage.remove.BlockAsDirRemover; import com.hedera.block.server.persistence.storage.remove.BlockRemover; @@ -28,6 +27,7 @@ import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; import java.util.List; +import java.util.Objects; import java.util.Set; /** @@ -35,19 +35,16 @@ * *

When a block writer is created, it will provide access to write blocks to storage. */ -public class BlockAsDirWriterBuilder { - +public final class BlockAsDirWriterBuilder { private final BlockNodeContext blockNodeContext; - private FileAttribute> filePerms = FileUtils.defaultPerms; + private FileAttribute> folderPermissions; private BlockRemover blockRemover; private BlockAsDirWriterBuilder(@NonNull final BlockNodeContext blockNodeContext) { - this.blockNodeContext = blockNodeContext; - PersistenceStorageConfig config = + this.blockNodeContext = Objects.requireNonNull(blockNodeContext); + final PersistenceStorageConfig config = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); - - this.blockRemover = - new BlockAsDirRemover(Path.of(config.rootPath()), FileUtils.defaultPerms); + this.blockRemover = new BlockAsDirRemover(Path.of(config.rootPath())); } /** @@ -57,9 +54,7 @@ private BlockAsDirWriterBuilder(@NonNull final BlockNodeContext blockNodeContext * @return a block writer builder configured with required parameters. */ @NonNull - public static BlockAsDirWriterBuilder newBuilder( - @NonNull final BlockNodeContext blockNodeContext) { - + public static BlockAsDirWriterBuilder newBuilder(@NonNull final BlockNodeContext blockNodeContext) { return new BlockAsDirWriterBuilder(blockNodeContext); } @@ -67,17 +62,13 @@ public static BlockAsDirWriterBuilder newBuilder( * Optionally, provide file permissions for the block writer to use when managing block files * and directories. * - *

By default, the block writer will use the permissions defined in {@link - * FileUtils#defaultPerms}. This method is primarily used for testing purposes. Default values - * should be sufficient for production use. - * - * @param filePerms the file permissions to use when managing block files and directories. + * @param folderPermissions the folder permissions to use when managing block files as directories. * @return a block writer builder configured with required parameters. */ @NonNull - public BlockAsDirWriterBuilder filePerms( - @NonNull FileAttribute> filePerms) { - this.filePerms = filePerms; + public BlockAsDirWriterBuilder folderPermissions( + @NonNull final FileAttribute> folderPermissions) { + this.folderPermissions = Objects.requireNonNull(folderPermissions); return this; } @@ -92,8 +83,8 @@ public BlockAsDirWriterBuilder filePerms( * @return a block writer builder configured with required parameters. */ @NonNull - public BlockAsDirWriterBuilder blockRemover(@NonNull BlockRemover blockRemover) { - this.blockRemover = blockRemover; + public BlockAsDirWriterBuilder blockRemover(@NonNull final BlockRemover blockRemover) { + this.blockRemover = Objects.requireNonNull(blockRemover); return this; } @@ -105,6 +96,6 @@ public BlockAsDirWriterBuilder blockRemover(@NonNull BlockRemover blockRemover) */ @NonNull public BlockWriter> build() throws IOException { - return new BlockAsDirWriter(blockRemover, filePerms, blockNodeContext); + return new BlockAsDirWriter(blockRemover, folderPermissions, blockNodeContext); } } diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index 579ce946b..60f924102 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -19,6 +19,7 @@ exports com.hedera.block.server.service; exports com.hedera.block.server.grpc; + requires com.hedera.block.common; requires com.hedera.block.stream; requires com.google.protobuf; requires com.hedera.pbj.runtime; diff --git a/server/src/test/java/com/hedera/block/server/persistence/PersistenceInjectionModuleTest.java b/server/src/test/java/com/hedera/block/server/persistence/PersistenceInjectionModuleTest.java index ee3a0e89f..d4d9f4753 100644 --- a/server/src/test/java/com/hedera/block/server/persistence/PersistenceInjectionModuleTest.java +++ b/server/src/test/java/com/hedera/block/server/persistence/PersistenceInjectionModuleTest.java @@ -45,26 +45,35 @@ @ExtendWith(MockitoExtension.class) class PersistenceInjectionModuleTest { - @Mock private BlockNodeContext blockNodeContext; - @Mock private PersistenceStorageConfig persistenceStorageConfig; - @Mock private SubscriptionHandler subscriptionHandler; - @Mock private Notifier notifier; - @Mock private BlockWriter> blockWriter; - @Mock private ServiceStatus serviceStatus; + @Mock + private BlockNodeContext blockNodeContext; + + @Mock + private PersistenceStorageConfig persistenceStorageConfig; + + @Mock + private SubscriptionHandler subscriptionHandler; + + @Mock + private Notifier notifier; + + @Mock + private BlockWriter> blockWriter; + + @Mock + private ServiceStatus serviceStatus; @BeforeEach void setup() throws IOException { // Setup any necessary mocks before each test blockNodeContext = TestConfigUtil.getTestBlockNodeContext(); - persistenceStorageConfig = - blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); + persistenceStorageConfig = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); } @Test void testProvidesBlockWriter() { - BlockWriter> blockWriter = - PersistenceInjectionModule.providesBlockWriter(blockNodeContext); + BlockWriter> blockWriter = PersistenceInjectionModule.providesBlockWriter(blockNodeContext); assertNotNull(blockWriter); } @@ -73,17 +82,14 @@ void testProvidesBlockWriter() { void testProvidesBlockWriter_IOException() { BlockNodeContext blockNodeContext = mock(BlockNodeContext.class); PersistenceStorageConfig persistenceStorageConfig = mock(PersistenceStorageConfig.class); - when(persistenceStorageConfig.rootPath()).thenReturn("invalid-path*9/////+>"); + when(persistenceStorageConfig.rootPath()).thenReturn("/invalid_path/:invalid_directory"); Configuration configuration = mock(Configuration.class); when(blockNodeContext.configuration()).thenReturn(configuration); - when(configuration.getConfigData(PersistenceStorageConfig.class)) - .thenReturn(persistenceStorageConfig); + when(configuration.getConfigData(PersistenceStorageConfig.class)).thenReturn(persistenceStorageConfig); // Expect a RuntimeException due to the IOException - RuntimeException exception = - assertThrows( - RuntimeException.class, - () -> PersistenceInjectionModule.providesBlockWriter(blockNodeContext)); + RuntimeException exception = assertThrows( + RuntimeException.class, () -> PersistenceInjectionModule.providesBlockWriter(blockNodeContext)); // Verify the exception message assertTrue(exception.getMessage().contains("Failed to create block writer")); @@ -92,8 +98,7 @@ void testProvidesBlockWriter_IOException() { @Test void testProvidesBlockReader() { - BlockReader blockReader = - PersistenceInjectionModule.providesBlockReader(persistenceStorageConfig); + BlockReader blockReader = PersistenceInjectionModule.providesBlockReader(persistenceStorageConfig); assertNotNull(blockReader); } @@ -104,13 +109,8 @@ void testProvidesStreamValidatorBuilder() throws IOException { BlockNodeContext blockNodeContext = TestConfigUtil.getTestBlockNodeContext(); // Call the method under test - BlockNodeEventHandler> streamVerifier = - new StreamPersistenceHandlerImpl( - subscriptionHandler, - notifier, - blockWriter, - blockNodeContext, - serviceStatus); + BlockNodeEventHandler> streamVerifier = new StreamPersistenceHandlerImpl( + subscriptionHandler, notifier, blockWriter, blockNodeContext, serviceStatus); assertNotNull(streamVerifier); } diff --git a/server/src/test/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderTest.java b/server/src/test/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderTest.java index 4e2fc2141..5c24babcb 100644 --- a/server/src/test/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderTest.java +++ b/server/src/test/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderTest.java @@ -26,7 +26,6 @@ import static org.mockito.Mockito.spy; import com.hedera.block.server.config.BlockNodeContext; -import com.hedera.block.server.persistence.storage.FileUtils; import com.hedera.block.server.persistence.storage.PersistenceStorageConfig; import com.hedera.block.server.persistence.storage.write.BlockAsDirWriterBuilder; import com.hedera.block.server.persistence.storage.write.BlockWriter; @@ -55,7 +54,8 @@ public class BlockAsDirReaderTest { private final Logger LOGGER = System.getLogger(getClass().getName()); - @TempDir private Path testPath; + @TempDir + private Path testPath; private BlockNodeContext blockNodeContext; private PersistenceStorageConfig config; @@ -63,14 +63,14 @@ public class BlockAsDirReaderTest { @BeforeEach public void setUp() throws IOException { blockNodeContext = - TestConfigUtil.getTestBlockNodeContext( - Map.of("persistence.storage.rootPath", testPath.toString())); + TestConfigUtil.getTestBlockNodeContext(Map.of("persistence.storage.rootPath", testPath.toString())); config = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); } @Test public void testReadBlockDoesNotExist() throws IOException, ParseException { - final BlockReader blockReader = BlockAsDirReaderBuilder.newBuilder(config).build(); + final BlockReader blockReader = + BlockAsDirReaderBuilder.newBuilder(config).build(); final Optional blockOpt = blockReader.read(10000); assertTrue(blockOpt.isEmpty()); } @@ -89,7 +89,8 @@ public void testReadPermsRepairSucceeded() throws IOException, ParseException { removeBlockReadPerms(1, config); // The default BlockReader will attempt to repair the permissions and should succeed - final BlockReader blockReader = BlockAsDirReaderBuilder.newBuilder(config).build(); + final BlockReader blockReader = + BlockAsDirReaderBuilder.newBuilder(config).build(); final Optional blockOpt = blockReader.read(1); assertFalse(blockOpt.isEmpty()); assertEquals(10, blockOpt.get().items().size()); @@ -108,10 +109,9 @@ public void testRemoveBlockReadPermsRepairFailed() throws IOException, ParseExce // For this test, build the Reader with ineffective repair permissions to // simulate a failed repair (root changed the perms, etc.) - final BlockReader blockReader = - BlockAsDirReaderBuilder.newBuilder(config) - .filePerms(TestUtils.getNoPerms()) - .build(); + final BlockReader blockReader = BlockAsDirReaderBuilder.newBuilder(config) + .folderPermissions(TestUtils.getNoPerms()) + .build(); final Optional blockOpt = blockReader.read(1); assertTrue(blockOpt.isEmpty()); } @@ -126,7 +126,8 @@ public void testRemoveBlockItemReadPerms() throws IOException { removeBlockItemReadPerms(1, 1, config); - final BlockReader blockReader = BlockAsDirReaderBuilder.newBuilder(config).build(); + final BlockReader blockReader = + BlockAsDirReaderBuilder.newBuilder(config).build(); assertThrows(IOException.class, () -> blockReader.read(1)); } @@ -136,11 +137,11 @@ public void testPathIsNotDirectory() throws IOException, ParseException { final Path blockNodeRootPath = Path.of(config.rootPath()); // Write a file named "1" where a directory should be - PersistTestUtils.writeBlockItemToPath( - blockNodeRootPath.resolve(Path.of("1")), blockItems.getFirst()); + PersistTestUtils.writeBlockItemToPath(blockNodeRootPath.resolve(Path.of("1")), blockItems.getFirst()); // Should return empty because the path is not a directory - final BlockReader blockReader = BlockAsDirReaderBuilder.newBuilder(config).build(); + final BlockReader blockReader = + BlockAsDirReaderBuilder.newBuilder(config).build(); final Optional blockOpt = blockReader.read(1); assertTrue(blockOpt.isEmpty()); } @@ -191,7 +192,8 @@ public void testParseExceptionHandling() throws IOException, ParseException { blockWriter.write(blockItems); // Read the block back and confirm it's read successfully - final BlockReader blockReader = BlockAsDirReaderBuilder.newBuilder(config).build(); + final BlockReader blockReader = + BlockAsDirReaderBuilder.newBuilder(config).build(); final Optional blockOpt = blockReader.read(1); assertFalse(blockOpt.isEmpty()); @@ -201,14 +203,14 @@ public void testParseExceptionHandling() throws IOException, ParseException { Path blockPath = blockNodeRootPath.resolve(String.valueOf(1)); byte[] bytes; - try (final FileInputStream fis = - new FileInputStream(blockPath.resolve("1" + BLOCK_FILE_EXTENSION).toFile())) { + try (final FileInputStream fis = new FileInputStream( + blockPath.resolve("1" + BLOCK_FILE_EXTENSION).toFile())) { bytes = fis.readAllBytes(); } // Corrupt the block item file by reversing the bytes - try (final FileOutputStream fos = - new FileOutputStream(blockPath.resolve("1" + BLOCK_FILE_EXTENSION).toFile())) { + try (final FileOutputStream fos = new FileOutputStream( + blockPath.resolve("1" + BLOCK_FILE_EXTENSION).toFile())) { byte[] reversedBytes = reverseByteArray(bytes); fos.write(reversedBytes); } @@ -218,8 +220,7 @@ public void testParseExceptionHandling() throws IOException, ParseException { assertThrows(ParseException.class, () -> blockReader.read(1)); } - public static void removeBlockReadPerms(int blockNumber, final PersistenceStorageConfig config) - throws IOException { + public static void removeBlockReadPerms(int blockNumber, final PersistenceStorageConfig config) throws IOException { final Path blockNodeRootPath = Path.of(config.rootPath()); final Path blockPath = blockNodeRootPath.resolve(String.valueOf(blockNumber)); removePathReadPerms(blockPath); @@ -229,8 +230,8 @@ static void removePathReadPerms(final Path path) throws IOException { Files.setPosixFilePermissions(path, TestUtils.getNoRead().value()); } - private void removeBlockItemReadPerms( - int blockNumber, int blockItem, PersistenceStorageConfig config) throws IOException { + private void removeBlockItemReadPerms(int blockNumber, int blockItem, PersistenceStorageConfig config) + throws IOException { final Path blockNodeRootPath = Path.of(config.rootPath()); final Path blockPath = blockNodeRootPath.resolve(String.valueOf(blockNumber)); final Path blockItemPath = blockPath.resolve(blockItem + BLOCK_FILE_EXTENSION); @@ -241,7 +242,7 @@ private void removeBlockItemReadPerms( // IOException while allowing the real setPerm() method to remain protected. private static final class TestBlockAsDirReader extends BlockAsDirReader { public TestBlockAsDirReader(PersistenceStorageConfig config) { - super(config, FileUtils.defaultPerms); + super(config, null); } @Override diff --git a/server/src/test/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemoverTest.java b/server/src/test/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemoverTest.java index 9e1542733..f954a6f43 100644 --- a/server/src/test/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemoverTest.java +++ b/server/src/test/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemoverTest.java @@ -21,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import com.hedera.block.server.config.BlockNodeContext; -import com.hedera.block.server.persistence.storage.FileUtils; import com.hedera.block.server.persistence.storage.PersistenceStorageConfig; import com.hedera.block.server.persistence.storage.read.BlockAsDirReaderBuilder; import com.hedera.block.server.persistence.storage.read.BlockReader; @@ -29,7 +28,6 @@ import com.hedera.block.server.persistence.storage.write.BlockWriter; import com.hedera.block.server.util.PersistTestUtils; import com.hedera.block.server.util.TestConfigUtil; -import com.hedera.block.server.util.TestUtils; import com.hedera.hapi.block.stream.Block; import com.hedera.hapi.block.stream.BlockItem; import com.hedera.pbj.runtime.ParseException; @@ -58,8 +56,7 @@ public void setUp() throws IOException { LOGGER.log(INFO, "Created temp directory: " + testPath.toString()); blockNodeContext = - TestConfigUtil.getTestBlockNodeContext( - Map.of("persistence.storage.rootPath", testPath.toString())); + TestConfigUtil.getTestBlockNodeContext(Map.of("persistence.storage.rootPath", testPath.toString())); testConfig = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); } @@ -76,7 +73,7 @@ public void testRemoveNonExistentBlock() throws IOException, ParseException { } // Remove a block that does not exist - final BlockRemover blockRemover = new BlockAsDirRemover(testPath, FileUtils.defaultPerms); + final BlockRemover blockRemover = new BlockAsDirRemover(testPath); blockRemover.remove(2); // Verify the block was not removed @@ -95,37 +92,4 @@ public void testRemoveNonExistentBlock() throws IOException, ParseException { blockOpt = blockReader.read(1); assert (blockOpt.isEmpty()); } - - @Test - public void testRemoveBlockWithPermException() throws IOException, ParseException { - - // Write a block - final List blockItems = PersistTestUtils.generateBlockItems(1); - - final BlockWriter> blockWriter = - BlockAsDirWriterBuilder.newBuilder(blockNodeContext).build(); - - blockWriter.write(blockItems); - - // Set up the BlockRemover with permissions that will prevent the block from being removed - BlockRemover blockRemover = new BlockAsDirRemover(testPath, TestUtils.getNoPerms()); - blockRemover.remove(1); - - // Verify the block was not removed - final BlockReader blockReader = - BlockAsDirReaderBuilder.newBuilder(testConfig).build(); - Optional blockOpt = blockReader.read(1); - assert (blockOpt.isPresent()); - assertEquals( - blockItems.getFirst().blockHeader(), - blockOpt.get().items().getFirst().blockHeader()); - - // Now remove the block - blockRemover = new BlockAsDirRemover(testPath, FileUtils.defaultPerms); - blockRemover.remove(1); - - // Verify the block is removed - blockOpt = blockReader.read(1); - assert (blockOpt.isEmpty()); - } } diff --git a/server/src/test/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterTest.java b/server/src/test/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterTest.java index d9520ce03..eb6dd5801 100644 --- a/server/src/test/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterTest.java +++ b/server/src/test/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterTest.java @@ -18,6 +18,7 @@ import static com.hedera.block.server.persistence.storage.read.BlockAsDirReaderTest.removeBlockReadPerms; import static com.hedera.block.server.util.PersistTestUtils.generateBlockItems; +import static com.hedera.block.server.util.TestConfigUtil.DEFAULT_TEST_FOLDER_PERMISSIONS; import static java.lang.System.Logger; import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.INFO; @@ -33,7 +34,6 @@ import static org.mockito.Mockito.spy; import com.hedera.block.server.config.BlockNodeContext; -import com.hedera.block.server.persistence.storage.FileUtils; import com.hedera.block.server.persistence.storage.PersistenceStorageConfig; import com.hedera.block.server.persistence.storage.read.BlockAsDirReaderBuilder; import com.hedera.block.server.persistence.storage.read.BlockReader; @@ -75,8 +75,7 @@ public void setUp() throws IOException { LOGGER.log(INFO, "Created temp directory: " + testPath.toString()); blockNodeContext = - TestConfigUtil.getTestBlockNodeContext( - Map.of(PERSISTENCE_STORAGE_ROOT_PATH_KEY, testPath.toString())); + TestConfigUtil.getTestBlockNodeContext(Map.of(PERSISTENCE_STORAGE_ROOT_PATH_KEY, testPath.toString())); testConfig = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); } @@ -93,10 +92,9 @@ public void testWriterAndReaderHappyPath() throws IOException, ParseException { // Write a block final List blockItems = generateBlockItems(1); - final BlockWriter> blockWriter = - BlockAsDirWriterBuilder.newBuilder(blockNodeContext) - .filePerms(FileUtils.defaultPerms) - .build(); + final BlockWriter> blockWriter = BlockAsDirWriterBuilder.newBuilder(blockNodeContext) + .folderPermissions(DEFAULT_TEST_FOLDER_PERMISSIONS) + .build(); for (int i = 0; i < 10; i++) { if (i == 9) { Optional> result = blockWriter.write(List.of(blockItems.get(i))); @@ -112,7 +110,8 @@ public void testWriterAndReaderHappyPath() throws IOException, ParseException { } // Confirm the block - BlockReader blockReader = BlockAsDirReaderBuilder.newBuilder(testConfig).build(); + BlockReader blockReader = + BlockAsDirReaderBuilder.newBuilder(testConfig).build(); Optional blockOpt = blockReader.read(1); assertFalse(blockOpt.isEmpty()); @@ -155,7 +154,8 @@ public void testRemoveBlockWritePerms() throws IOException, ParseException { assertFalse(result.isPresent()); // Confirm we're able to read 1 block item - BlockReader blockReader = BlockAsDirReaderBuilder.newBuilder(testConfig).build(); + BlockReader blockReader = + BlockAsDirReaderBuilder.newBuilder(testConfig).build(); Optional blockOpt = blockReader.read(1); assertFalse(blockOpt.isEmpty()); assertEquals(1, blockOpt.get().items().size()); @@ -189,15 +189,12 @@ public void testRemoveBlockWritePerms() throws IOException, ParseException { public void testUnrecoverableIOExceptionOnWrite() throws IOException { final List blockItems = generateBlockItems(1); - final BlockRemover blockRemover = - new BlockAsDirRemover(Path.of(testConfig.rootPath()), FileUtils.defaultPerms); + final BlockRemover blockRemover = new BlockAsDirRemover(Path.of(testConfig.rootPath())); // Use a spy to simulate an IOException when the first block item is written - final BlockWriter> blockWriter = - spy( - BlockAsDirWriterBuilder.newBuilder(blockNodeContext) - .blockRemover(blockRemover) - .build()); + final BlockWriter> blockWriter = spy(BlockAsDirWriterBuilder.newBuilder(blockNodeContext) + .blockRemover(blockRemover) + .build()); doThrow(IOException.class).when(blockWriter).write(blockItems); assertThrows(IOException.class, () -> blockWriter.write(blockItems)); } @@ -235,7 +232,8 @@ public void testRemoveRootDirReadPerm() throws IOException, ParseException { } } - BlockReader blockReader = BlockAsDirReaderBuilder.newBuilder(testConfig).build(); + BlockReader blockReader = + BlockAsDirReaderBuilder.newBuilder(testConfig).build(); Optional blockOpt = blockReader.read(1); assertFalse(blockOpt.isEmpty()); assertEquals(10, blockOpt.get().items().size()); @@ -244,17 +242,13 @@ public void testRemoveRootDirReadPerm() throws IOException, ParseException { @Test public void testPartialBlockRemoval() throws IOException, ParseException { final List blockItems = generateBlockItems(3); - final BlockRemover blockRemover = - new BlockAsDirRemover(Path.of(testConfig.rootPath()), FileUtils.defaultPerms); + final BlockRemover blockRemover = new BlockAsDirRemover(Path.of(testConfig.rootPath())); // Use a spy of TestBlockAsDirWriter to proxy block items to the real writer // for the first 22 block items. Then simulate an IOException on the 23rd block item // thrown from a protected write method in the real class. This should trigger the // blockRemover instance to remove the partially written block. - final TestBlockAsDirWriter blockWriter = - spy( - new TestBlockAsDirWriter( - blockRemover, FileUtils.defaultPerms, blockNodeContext)); + final TestBlockAsDirWriter blockWriter = spy(new TestBlockAsDirWriter(blockRemover, null, blockNodeContext)); for (int i = 0; i < 23; i++) { // Prepare the block writer to call the actual write method @@ -311,8 +305,7 @@ private void removeRootReadPerms(final PersistenceStorageConfig config) throws I Files.setPosixFilePermissions(blockNodeRootPath, TestUtils.getNoRead().value()); } - private void removeBlockAllPerms(final int blockNumber, final PersistenceStorageConfig config) - throws IOException { + private void removeBlockAllPerms(final int blockNumber, final PersistenceStorageConfig config) throws IOException { final Path blockNodeRootPath = Path.of(config.rootPath()); final Path blockPath = blockNodeRootPath.resolve(String.valueOf(blockNumber)); Files.setPosixFilePermissions(blockPath, TestUtils.getNoPerms().value()); diff --git a/server/src/test/java/com/hedera/block/server/util/TestConfigUtil.java b/server/src/test/java/com/hedera/block/server/util/TestConfigUtil.java index d47793762..5253f9da2 100644 --- a/server/src/test/java/com/hedera/block/server/util/TestConfigUtil.java +++ b/server/src/test/java/com/hedera/block/server/util/TestConfigUtil.java @@ -28,27 +28,37 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.Collections; import java.util.Map; +import java.util.Set; public class TestConfigUtil { - public static final String CONSUMER_TIMEOUT_THRESHOLD_KEY = "consumer.timeoutThresholdMillis"; public static final String MEDIATOR_RING_BUFFER_SIZE_KEY = "mediator.ringBufferSize"; + public static final FileAttribute> DEFAULT_TEST_FOLDER_PERMISSIONS = + PosixFilePermissions.asFileAttribute(Set.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_EXECUTE)); private static final String TEST_APP_PROPERTIES_FILE = "app.properties"; private TestConfigUtil() {} @NonNull - public static BlockNodeContext getTestBlockNodeContext( - @NonNull Map customProperties) throws IOException { + public static BlockNodeContext getTestBlockNodeContext(@NonNull Map customProperties) + throws IOException { // create test configuration - TestConfigBuilder testConfigBuilder = - new TestConfigBuilder(true) - .withSource( - new ClasspathFileConfigSource(Path.of(TEST_APP_PROPERTIES_FILE))); + TestConfigBuilder testConfigBuilder = new TestConfigBuilder(true) + .withSource(new ClasspathFileConfigSource(Path.of(TEST_APP_PROPERTIES_FILE))); for (Map.Entry entry : customProperties.entrySet()) { String key = entry.getKey(); diff --git a/simulator/src/main/java/com/hedera/block/simulator/exception/BlockSimulatorParsingException.java b/simulator/src/main/java/com/hedera/block/simulator/exception/BlockSimulatorParsingException.java index 7f755b27d..701affaf7 100644 --- a/simulator/src/main/java/com/hedera/block/simulator/exception/BlockSimulatorParsingException.java +++ b/simulator/src/main/java/com/hedera/block/simulator/exception/BlockSimulatorParsingException.java @@ -23,7 +23,16 @@ public class BlockSimulatorParsingException extends Exception { * * @param message the detail message */ - public BlockSimulatorParsingException(String message) { + public BlockSimulatorParsingException(final String message) { super(message); } + + /** + * Constructs a new parsing exception with the specified cause. + * + * @param cause the cause of the exception, could be null, see super javadoc + */ + public BlockSimulatorParsingException(final Throwable cause) { + super(cause); + } } diff --git a/simulator/src/main/java/com/hedera/block/simulator/generator/BlockAsFileLargeDataSets.java b/simulator/src/main/java/com/hedera/block/simulator/generator/BlockAsFileLargeDataSets.java index fa8a3591d..9ccad224a 100644 --- a/simulator/src/main/java/com/hedera/block/simulator/generator/BlockAsFileLargeDataSets.java +++ b/simulator/src/main/java/com/hedera/block/simulator/generator/BlockAsFileLargeDataSets.java @@ -103,7 +103,7 @@ public Block getNextBlock() throws IOException, BlockSimulatorParsingException { LOGGER.log(INFO, "block loaded with items size= " + block.items().size()); return block; } catch (final ParseException e) { - throw new BlockSimulatorParsingException(e.getMessage()); + throw new BlockSimulatorParsingException(e); } } }