diff --git a/buildSrc/src/main/kotlin/com.hedera.block.common.gradle.kts b/buildSrc/src/main/kotlin/com.hedera.block.common.gradle.kts new file mode 100644 index 000000000..5fdfbba27 --- /dev/null +++ b/buildSrc/src/main/kotlin/com.hedera.block.common.gradle.kts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +plugins { + id("java-library") + id("com.hedera.block.conventions") + id("me.champeau.jmh") +} + +val maven = publishing.publications.create("maven") { from(components["java"]) } + +signing.sign(maven) diff --git a/buildSrc/src/main/kotlin/com.hedera.block.spotless-java-conventions.gradle.kts b/buildSrc/src/main/kotlin/com.hedera.block.spotless-java-conventions.gradle.kts index 54f051165..248a8c9a2 100644 --- a/buildSrc/src/main/kotlin/com.hedera.block.spotless-java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/com.hedera.block.spotless-java-conventions.gradle.kts @@ -19,11 +19,18 @@ plugins { id("com.diffplug.spotless") } spotless { java { targetExclude("build/generated/**/*.java", "build/generated/**/*.proto") - // enable toggle comment support + // Enables the spotless:on and spotless:off comments toggleOffOn() // don't need to set target, it is inferred from java // apply a specific flavor of google-java-format - googleJavaFormat("1.17.0").aosp().reflowLongStrings() + // also reflow long strings, and do not format javadoc + // because the default setup is _very_ bad for javadoc + // We need to figure out a "correct" _separate_ setup for that. + googleJavaFormat("1.17.0").aosp().reflowLongStrings().formatJavadoc(false) + // Fix some left-out items from the google plugin + indentWithSpaces(4) + trimTrailingWhitespace() + endWithNewline() // make sure every file has the following copyright header. // optionally, Spotless can set copyright years by digging // through git history (see "license" section below). diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 000000000..af82a2868 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +plugins { + id("java-library") + id("com.hedera.block.common") +} + +description = "Commons module with logic that could be abstracted and reused." + +testModuleInfo { requiresStatic("com.github.spotbugs.annotations") } diff --git a/common/src/main/java/com/hedera/block/common/constants/StringsConstants.java b/common/src/main/java/com/hedera/block/common/constants/StringsConstants.java new file mode 100644 index 000000000..4aa7a0b7b --- /dev/null +++ b/common/src/main/java/com/hedera/block/common/constants/StringsConstants.java @@ -0,0 +1,32 @@ +/* + * 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.common.constants; + +/** A class that hold common String literals used across projects. */ +public final class StringsConstants { + /** + * File name for application properties + */ + public static final String APPLICATION_PROPERTIES = "app.properties"; + + /** + * File name for logging properties + */ + public static final String LOGGING_PROPERTIES = "logging.properties"; + + private StringsConstants() {} +} 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 new file mode 100644 index 000000000..d8dd9ae60 --- /dev/null +++ b/common/src/main/java/com/hedera/block/common/utils/FileUtilities.java @@ -0,0 +1,221 @@ +/* + * 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.common.utils; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.lang.System.Logger; +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.Objects; +import java.util.Set; +import java.util.zip.GZIPInputStream; + +/** A utility class that deals with logic related to dealing with files. */ +public final class FileUtilities { + private static final Logger LOGGER = System.getLogger(FileUtilities.class.getName()); + + /** + * The default file permissions for new files. + *

+ * 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)); + + /** + * Default folder permissions for new folders. + *

+ * 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)); + + /** + * 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. + * + * @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( + @NonNull final Path toCreate, + @NonNull final System.Logger.Level logLevel, + @NonNull final String semanticPathName, + final boolean createDir) + throws IOException { + createPathIfNotExists( + toCreate, + logLevel, + DEFAULT_FILE_PERMISSIONS, + DEFAULT_FOLDER_PERMISSIONS, + semanticPathName, + createDir); + } + + /** + * Create a new path (folder or file) 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 + * 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( + @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) + throws IOException { + Objects.requireNonNull(toCreate); + Objects.requireNonNull(logLevel); + Objects.requireNonNull(filePermissions); + Objects.requireNonNull(folderPermissions); + 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); + 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); + LOGGER.log(logLevel, logMessage); + } + } + + /** + * Read a GZIP file and return the content as a byte array. + *

+ * This method is _unsafe_ because it reads the entire file content into + * a single byte array, which can cause memory issues, and may fail if the + * file contains a large amount of data. + * + * @param filePath Path to the GZIP file. + * @return byte array containing the _uncompressed_ content of the GZIP file. + * @throws IOException if unable to read the file. + * @throws OutOfMemoryError if a byte array large enough to contain the + * file contents cannot be allocated (either because it exceeds MAX_INT + * bytes or exceeds available heap memory). + */ + public static byte[] readGzipFileUnsafe(@NonNull final Path filePath) throws IOException { + Objects.requireNonNull(filePath); + try (final var gzipInputStream = new GZIPInputStream(Files.newInputStream(filePath))) { + return gzipInputStream.readAllBytes(); + } + } + + /** + * Read a file and return the content as a byte array. + *

+ * This method uses default extensions for gzip and block files. + *

+ * This method is _unsafe_ because it reads the entire file content into + * a single byte array, which can cause memory issues, and may fail if the + * file contains a large amount of data. + * + * @param filePath Path to the file + * @return byte array of the content of the file or null if the file extension is not + * supported + * @throws IOException if unable to read the file. + * @throws OutOfMemoryError if a byte array large enough to contain the + * file contents cannot be allocated (either because it exceeds MAX_INT + * bytes or exceeds available heap memory). + */ + public static byte[] readFileBytesUnsafe(@NonNull final Path filePath) throws IOException { + return readFileBytesUnsafe(filePath, ".blk", ".gz"); + } + + /** + * Read a file and return the content as a byte array. + *

+ * This method is _unsafe_ because it reads the entire file content into + * a single byte array, which can cause memory issues, and may fail if the + * file contains a large amount of data. + * + * @param filePath Path to the file to read. + * @param blockFileExtension A file extension for block files. + * @param gzipFileExtension A file extension for gzip files. + * @return A byte array with the full contents of the file, or null if the + * file extension requested does not match at least one of the + * extensions provided (GZip or Block). + * @throws IOException if unable to read the file. + * @throws OutOfMemoryError if a byte array large enough to contain the + * file contents cannot be allocated (either because it exceeds MAX_INT + * bytes or exceeds available heap memory). + */ + public static byte[] readFileBytesUnsafe( + @NonNull final Path filePath, + @NonNull final String blockFileExtension, + @NonNull final String gzipFileExtension) + throws IOException { + final String filePathAsString = Objects.requireNonNull(filePath).toString(); + Objects.requireNonNull(blockFileExtension); + Objects.requireNonNull(gzipFileExtension); + if (filePathAsString.endsWith(gzipFileExtension)) { + return readGzipFileUnsafe(filePath); + } else if (filePathAsString.endsWith(blockFileExtension)) { + return Files.readAllBytes(filePath); + } else { + return null; + } + } + + private FileUtilities() {} +} diff --git a/common/src/main/java/com/hedera/block/common/utils/StringUtilities.java b/common/src/main/java/com/hedera/block/common/utils/StringUtilities.java new file mode 100644 index 000000000..4977fdcae --- /dev/null +++ b/common/src/main/java/com/hedera/block/common/utils/StringUtilities.java @@ -0,0 +1,42 @@ +/* + * 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.common.utils; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; + +/** A utility class that deals with logic related to Strings. */ +public final class StringUtilities { + /** + * This method checks if a given {@link String} is blank, meaning if it is {@code null} or + * contains only whitespaces as defined by {@link String#isBlank()}. If the given {@link String} + * is not blank, then we return it, else we throw {@link IllegalArgumentException}. + * + * @param toCheck a {@link String} to be checked if is blank as defined above + * @return the {@link String} to be checked if it is not blank as defined above + * @throws IllegalArgumentException if the input {@link String} to be checked is blank + */ + public static String requireNotBlank(@NonNull final String toCheck) { + if (Objects.requireNonNull(toCheck).isBlank()) { + throw new IllegalArgumentException("A String required to be non-blank is blank."); + } else { + return toCheck; + } + } + + private StringUtilities() {} +} diff --git a/common/src/main/java/module-info.java b/common/src/main/java/module-info.java new file mode 100644 index 000000000..a6f56aab4 --- /dev/null +++ b/common/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module com.hedera.block.common { + exports com.hedera.block.common.constants; + exports com.hedera.block.common.utils; + + requires static com.github.spotbugs.annotations; +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9dd268f44..e1441bb3a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,6 +19,7 @@ plugins { } // Include the subprojects +include(":common") include(":suites") include(":stream") include(":server")