diff --git a/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/ErrorReporting.kt b/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/ErrorReporting.kt index b58557d67e..20cdb77cf6 100644 --- a/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/ErrorReporting.kt +++ b/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/ErrorReporting.kt @@ -43,6 +43,11 @@ class ErrorReporting private constructor(private val localeString: String, return ErrorReporting(localeString, location, contextProvider) } + /** + * Set the context provider to supply project-specific information about the errors. + * + * @param contextProvider The context provider to use with error reporting + */ fun withContextProvider(contextProvider: ErrorContextProvider) : ErrorReporting { return ErrorReporting(localeString, resourceLocation, contextProvider) } diff --git a/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/Errors.kt b/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/Errors.kt index 83e324b893..95779d23d6 100644 --- a/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/Errors.kt +++ b/common/logging/src/main/kotlin/net/corda/common/logging/errorReporting/Errors.kt @@ -4,7 +4,8 @@ package net.corda.common.logging.errorReporting * Namespaces for errors within the node. */ enum class NodeNamespaces { - DATABASE + DATABASE, + CORDAPP } /** @@ -17,4 +18,16 @@ enum class NodeDatabaseErrors : ErrorCodes { PASSWORD_REQUIRED_FOR_H2; override val namespace = NodeNamespaces.DATABASE.toString() +} + +/** + * Errors related to loading of Cordapps + */ +enum class CordappErrors : ErrorCodes { + DUPLICATE_CORDAPPS_INSTALLED, + MULTIPLE_CORDAPPS_FOR_FLOW, + MISSING_VERSION_ATTRIBUTE, + INVALID_VERSION_IDENTIFIER; + + override val namespace = NodeNamespaces.CORDAPP.toString() } \ No newline at end of file diff --git a/common/logging/src/main/resources/error-codes/cordapp-duplicate-cordapps-installed.properties b/common/logging/src/main/resources/error-codes/cordapp-duplicate-cordapps-installed.properties new file mode 100644 index 0000000000..0fd778435c --- /dev/null +++ b/common/logging/src/main/resources/error-codes/cordapp-duplicate-cordapps-installed.properties @@ -0,0 +1,4 @@ +errorTemplate = The CorDapp (name: {0}, file: {1}) is installed multiple times on the node. The following files correspond to the exact same content: {2} +shortDescription = A CorDapp has been installed multiple times on the same node. +actionsToFix = Investigate the logs to determine the files with duplicate content, and remove one of them from the cordapps directory. +aliases = iw8d4e \ No newline at end of file diff --git a/common/logging/src/main/resources/error-codes/cordapp-duplicate-cordapps-installed_en_US.properties b/common/logging/src/main/resources/error-codes/cordapp-duplicate-cordapps-installed_en_US.properties new file mode 100644 index 0000000000..2354427df3 --- /dev/null +++ b/common/logging/src/main/resources/error-codes/cordapp-duplicate-cordapps-installed_en_US.properties @@ -0,0 +1,3 @@ +errorTemplate = The CorDapp (name: {0}, file: {1}) is installed multiple times on the node. The following files correspond to the exact same content: {2} +shortDescription = A CorDapp has been installed multiple times on the same node. +actionsToFix = Investigate the logs to determine the files with duplicate content, and remove one of them from the cordapps directory. \ No newline at end of file diff --git a/common/logging/src/main/resources/error-codes/cordapp-invalid-version-identifier.properties b/common/logging/src/main/resources/error-codes/cordapp-invalid-version-identifier.properties new file mode 100644 index 0000000000..0922f80bb4 --- /dev/null +++ b/common/logging/src/main/resources/error-codes/cordapp-invalid-version-identifier.properties @@ -0,0 +1,4 @@ +errorTemplate = Version identifier ({0}) for attribute {1} must be a whole number starting from 1. +shortDescription = A version attribute was specified in the CorDapp manifest with an invalid value. The value must be a whole number, and it must be greater than or equal to 1. +actionsToFix = Investigate the logs to find the invalid attribute, and change the attribute value to be valid (a whole number greater than or equal to 1). +aliases = \ No newline at end of file diff --git a/common/logging/src/main/resources/error-codes/cordapp-invalid-version-identifier_en_US.properties b/common/logging/src/main/resources/error-codes/cordapp-invalid-version-identifier_en_US.properties new file mode 100644 index 0000000000..0922f80bb4 --- /dev/null +++ b/common/logging/src/main/resources/error-codes/cordapp-invalid-version-identifier_en_US.properties @@ -0,0 +1,4 @@ +errorTemplate = Version identifier ({0}) for attribute {1} must be a whole number starting from 1. +shortDescription = A version attribute was specified in the CorDapp manifest with an invalid value. The value must be a whole number, and it must be greater than or equal to 1. +actionsToFix = Investigate the logs to find the invalid attribute, and change the attribute value to be valid (a whole number greater than or equal to 1). +aliases = \ No newline at end of file diff --git a/common/logging/src/main/resources/error-codes/cordapp-missing-version-attribute.properties b/common/logging/src/main/resources/error-codes/cordapp-missing-version-attribute.properties new file mode 100644 index 0000000000..772b459195 --- /dev/null +++ b/common/logging/src/main/resources/error-codes/cordapp-missing-version-attribute.properties @@ -0,0 +1,4 @@ +errorTemplate = Target versionId attribute {0} not specified. Please specify a whole number starting from 1. +shortDescription = A required version attribute was not specified in the manifest of the CorDapp JAR. +actionsToFix = Investigate the logs to find out which version attribute has not been specified, and add that version attribute to the CorDapp manifest. +aliases = \ No newline at end of file diff --git a/common/logging/src/main/resources/error-codes/cordapp-missing-version-attribute_en_US.properties b/common/logging/src/main/resources/error-codes/cordapp-missing-version-attribute_en_US.properties new file mode 100644 index 0000000000..d8a7ab050a --- /dev/null +++ b/common/logging/src/main/resources/error-codes/cordapp-missing-version-attribute_en_US.properties @@ -0,0 +1,3 @@ +errorTemplate = Target versionId attribute {0} not specified. Please specify a whole number starting from 1. +shortDescription = A required version attribute was not specified in the manifest of the CorDapp JAR. +actionsToFix = Investigate the logs to find out which version attribute has not been specified, and add that version attribute to the CorDapp manifest. \ No newline at end of file diff --git a/common/logging/src/main/resources/error-codes/cordapp-multiple-cordapps-for-flow.properties b/common/logging/src/main/resources/error-codes/cordapp-multiple-cordapps-for-flow.properties new file mode 100644 index 0000000000..b1ebfb6bb4 --- /dev/null +++ b/common/logging/src/main/resources/error-codes/cordapp-multiple-cordapps-for-flow.properties @@ -0,0 +1,4 @@ +errorTemplate = There are multiple CorDapp JARs on the classpath for the flow {0}: [{1}] +shortDescription = Multiple CorDapp JARs on the classpath define the same flow class. As a result, the platform will not know which version of the flow to start when the flow is invoked. +actionsToFix = Investigate the logs to find out which CorDapp JARs define the same flow classes. The developers of these apps will need to resolve the clash. +aliases = \ No newline at end of file diff --git a/common/logging/src/main/resources/error-codes/cordapp-multiple-cordapps-for-flow_en_US.properties b/common/logging/src/main/resources/error-codes/cordapp-multiple-cordapps-for-flow_en_US.properties new file mode 100644 index 0000000000..b1ebfb6bb4 --- /dev/null +++ b/common/logging/src/main/resources/error-codes/cordapp-multiple-cordapps-for-flow_en_US.properties @@ -0,0 +1,4 @@ +errorTemplate = There are multiple CorDapp JARs on the classpath for the flow {0}: [{1}] +shortDescription = Multiple CorDapp JARs on the classpath define the same flow class. As a result, the platform will not know which version of the flow to start when the flow is invoked. +actionsToFix = Investigate the logs to find out which CorDapp JARs define the same flow classes. The developers of these apps will need to resolve the clash. +aliases = \ No newline at end of file diff --git a/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/CordappErrorsTest.kt b/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/CordappErrorsTest.kt new file mode 100644 index 0000000000..4e28410c78 --- /dev/null +++ b/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/CordappErrorsTest.kt @@ -0,0 +1,12 @@ +package net.corda.commmon.logging.errorReporting + +import net.corda.common.logging.errorReporting.CordappErrors + +class CordappErrorsTest : ErrorCodeTest(CordappErrors::class.java, true) { + override val dataForCodes = mapOf( + CordappErrors.MISSING_VERSION_ATTRIBUTE to listOf("test-attribute"), + CordappErrors.INVALID_VERSION_IDENTIFIER to listOf(-1, "test-attribute"), + CordappErrors.MULTIPLE_CORDAPPS_FOR_FLOW to listOf("MyTestFlow", "Jar 1, Jar 2"), + CordappErrors.DUPLICATE_CORDAPPS_INSTALLED to listOf("TestCordapp", "testapp.jar", "testapp2.jar") + ) +} \ No newline at end of file diff --git a/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/DatabaseErrorsTest.kt b/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/DatabaseErrorsTest.kt new file mode 100644 index 0000000000..d8697e9415 --- /dev/null +++ b/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/DatabaseErrorsTest.kt @@ -0,0 +1,13 @@ +package net.corda.commmon.logging.errorReporting + +import net.corda.common.logging.errorReporting.NodeDatabaseErrors +import java.net.InetAddress + +class DatabaseErrorsTest : ErrorCodeTest(NodeDatabaseErrors::class.java) { + override val dataForCodes = mapOf( + NodeDatabaseErrors.COULD_NOT_CONNECT to listOf(), + NodeDatabaseErrors.FAILED_STARTUP to listOf(), + NodeDatabaseErrors.MISSING_DRIVER to listOf(), + NodeDatabaseErrors.PASSWORD_REQUIRED_FOR_H2 to listOf(InetAddress.getLocalHost()) + ) +} \ No newline at end of file diff --git a/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/ErrorCodeTest.kt b/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/ErrorCodeTest.kt new file mode 100644 index 0000000000..1faf5deaef --- /dev/null +++ b/common/logging/src/test/kotlin/net/corda/commmon/logging/errorReporting/ErrorCodeTest.kt @@ -0,0 +1,61 @@ +package net.corda.commmon.logging.errorReporting + +import junit.framework.TestCase.assertFalse +import net.corda.common.logging.errorReporting.ErrorCode +import net.corda.common.logging.errorReporting.ErrorCodes +import net.corda.common.logging.errorReporting.ErrorResource +import net.corda.common.logging.errorReporting.ResourceBundleProperties +import org.junit.Test +import java.util.* +import kotlin.test.assertTrue + +/** + * Utility for testing that error code resource files behave as expected. + * + * This allows for testing that error messages are printed correctly if they are provided the correct parameters. The test will fail if any + * of the parameters of the template are not filled in. + * + * To use, override the `dataForCodes` with a map from an error code enum value to a list of parameters the message template takes. If any + * are missed, the test will fail. + * + * `printProperties`, if set to true, will print the properties out the resource files, with the error message filled in. This allows the + * message to be inspected. + */ +abstract class ErrorCodeTest(private val clazz: Class, + private val printProperties: Boolean = false) where T: Enum, T: ErrorCodes { + + abstract val dataForCodes: Map> + + private class TestError(override val code: T, + override val parameters: List) : ErrorCode where T: Enum, T: ErrorCodes + + @Test(timeout = 300_000) + fun `test error codes`() { + for ((code, params) in dataForCodes) { + val error = TestError(code, params) + val resource = ErrorResource.fromErrorCode(error, "error-codes", Locale.forLanguageTag("en-US")) + val message = resource.getErrorMessage(error.parameters.toTypedArray()) + assertFalse( + "The error message reported for code $code contains missing parameters", + message.contains("\\{.*}".toRegex()) + ) + val otherProperties = Triple(resource.shortDescription, resource.actionsToFix, resource.aliases) + if (printProperties) { + println("Data for $code") + println("Error Message = $message") + println("${ResourceBundleProperties.SHORT_DESCRIPTION} = ${otherProperties.first}") + println("${ResourceBundleProperties.ACTIONS_TO_FIX} = ${otherProperties.second}") + println("${ResourceBundleProperties.ALIASES} = ${otherProperties.third}") + println("") + } + } + } + + @Test(timeout = 300_000) + fun `ensure all error codes tested`() { + val expected = clazz.enumConstants.toSet() + val actual = dataForCodes.keys.toSet() + val missing = expected - actual + assertTrue(missing.isEmpty(), "The following codes have not been tested: $missing") + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index a8fef21fc4..b134c69815 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -2,6 +2,10 @@ package net.corda.node.internal.cordapp import io.github.classgraph.ClassGraph import io.github.classgraph.ScanResult +import net.corda.common.logging.errorReporting.CordappErrors +import net.corda.common.logging.errorReporting.ErrorCode +import net.corda.common.logging.errorReporting.NodeDatabaseErrors +import net.corda.common.logging.errorReporting.NodeNamespaces import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 @@ -144,9 +148,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: val duplicateCordapps = registeredCordapps.filter { it.jarHash == cordapp.jarHash }.toSet() if (duplicateCordapps.isNotEmpty()) { - throw IllegalStateException("The CorDapp (name: ${cordapp.info.shortName}, file: ${cordapp.name}) " + - "is installed multiple times on the node. The following files correspond to the exact same content: " + - "${duplicateCordapps.map { it.name }}") + throw DuplicateCordappsInstalledException(cordapp, duplicateCordapps) } if (registeredClassName in contractClasses) { throw IllegalStateException("More than one CorDapp installed on the node for contract $registeredClassName. " + @@ -227,12 +229,21 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: private fun parseVersion(versionStr: String?, attributeName: String): Int { if (versionStr == null) { - throw CordappInvalidVersionException("Target versionId attribute $attributeName not specified. Please specify a whole number starting from 1.") + throw CordappInvalidVersionException( + "Target versionId attribute $attributeName not specified. Please specify a whole number starting from 1.", + CordappErrors.MISSING_VERSION_ATTRIBUTE, + listOf(attributeName)) } val version = versionStr.toIntOrNull() - ?: throw CordappInvalidVersionException("Version identifier ($versionStr) for attribute $attributeName must be a whole number starting from 1.") + ?: throw CordappInvalidVersionException( + "Version identifier ($versionStr) for attribute $attributeName must be a whole number starting from 1.", + CordappErrors.INVALID_VERSION_IDENTIFIER, + listOf(versionStr, attributeName)) if (version < 1) { - throw CordappInvalidVersionException("Target versionId ($versionStr) for attribute $attributeName must not be smaller than 1.") + throw CordappInvalidVersionException( + "Target versionId ($versionStr) for attribute $attributeName must not be smaller than 1.", + CordappErrors.INVALID_VERSION_IDENTIFIER, + listOf(versionStr, attributeName)) } return version } @@ -403,12 +414,34 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: /** * Thrown when scanning CorDapps. */ -class MultipleCordappsForFlowException(message: String) : Exception(message) +class MultipleCordappsForFlowException( + message: String, + flowName: String, + jars: String +) : Exception(message), ErrorCode { + override val code = CordappErrors.MULTIPLE_CORDAPPS_FOR_FLOW + override val parameters = listOf(flowName, jars) +} /** * Thrown if an exception occurs whilst parsing version identifiers within cordapp configuration */ -class CordappInvalidVersionException(msg: String) : Exception(msg) +class CordappInvalidVersionException( + msg: String, + override val code: CordappErrors, + override val parameters: List = listOf() +) : Exception(msg), ErrorCode + +/** + * Thrown if duplicate CorDapps are installed on the node + */ +class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Set) + : IllegalStateException("The CorDapp (name: ${app.info.shortName}, file: ${app.name}) " + + "is installed multiple times on the node. The following files correspond to the exact same content: " + + "${duplicates.map { it.name }}"), ErrorCode { + override val code = CordappErrors.DUPLICATE_CORDAPPS_INSTALLED + override val parameters = listOf(app.info.shortName, app.name, duplicates.map { it.name }) +} abstract class CordappLoaderTemplate : CordappLoader { @@ -436,7 +469,9 @@ abstract class CordappLoaderTemplate : CordappLoader { } } throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow " + - "${entry.value.first().first.name}: [ ${entry.value.joinToString { it.second.jarPath.toString() }} ].") + "${entry.value.first().first.name}: [ ${entry.value.joinToString { it.second.jarPath.toString() }} ].", + entry.value.first().first.name, + entry.value.joinToString { it.second.jarPath.toString() }) } entry.value.single().second } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index fa98ff7e59..0d3f8d8f54 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -1,6 +1,7 @@ package net.corda.node.internal.cordapp import co.paralleluniverse.fibers.Suspendable +import net.corda.common.logging.errorReporting.CordappErrors import net.corda.core.flows.* import net.corda.node.VersionInfo import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES diff --git a/settings.gradle b/settings.gradle index f3ec7e75d8..d582cee14e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -108,5 +108,5 @@ include 'serialization-deterministic' include 'tools:checkpoint-agent' include 'detekt-plugins' -include 'tools:error-page-builder' +include 'tools:error-tool' diff --git a/tools/error-page-builder/build.gradle b/tools/error-tool/build.gradle similarity index 67% rename from tools/error-page-builder/build.gradle rename to tools/error-tool/build.gradle index ed16c572b3..d1e11ec376 100644 --- a/tools/error-page-builder/build.gradle +++ b/tools/error-tool/build.gradle @@ -1,6 +1,3 @@ -group 'net.corda' -version '4.5-SNAPSHOT' - apply plugin: 'kotlin' apply plugin: 'com.github.johnrengelman.shadow' @@ -12,9 +9,9 @@ dependencies { implementation project(":common-logging") implementation project(":tools:cliutils") implementation "info.picocli:picocli:$picocli_version" - testCompile group: 'junit', name: 'junit', version: '4.12' - implementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + + testCompile "junit:junit:4.12" } jar { @@ -23,10 +20,12 @@ jar { } shadowJar { - baseName = "corda-tools-error-page-builder" + baseName = "corda-tools-error-utils" manifest { attributes( - 'Main-Class': "net.corda.errorPageBuilder.ErrorPageBuilderKt" + 'Main-Class': "net.corda.errorUtilities.ErrorToolKt" ) } -} \ No newline at end of file +} + +assemble.dependsOn shadowJar \ No newline at end of file diff --git a/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorResourceUtilities.kt b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorResourceUtilities.kt new file mode 100644 index 0000000000..bcb88f6180 --- /dev/null +++ b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorResourceUtilities.kt @@ -0,0 +1,45 @@ +package net.corda.errorUtilities + +import java.net.URLClassLoader +import java.nio.file.Path + +/** + * A class for reading and processing error code resource bundles from a given directory. + */ +class ErrorResourceUtilities { + + companion object { + private val ERROR_INFO_RESOURCE_REGEX= ".*ErrorInfo.*".toRegex() + private val DEFAULT_RESOURCE_FILE_REGEX = "[^_]*\\.properties".toRegex() + private val PROPERTIES_FILE_REGEX = ".*\\.properties".toRegex() + + /** + * List all resource bundle names in a given directory + */ + fun listResourceNames(location: Path) : List { + return location.toFile().walkTopDown().filter { + it.name.matches(DEFAULT_RESOURCE_FILE_REGEX) && !it.name.matches(ERROR_INFO_RESOURCE_REGEX) + }.map { + it.nameWithoutExtension + }.toList() + } + + /** + * List all resource files in a given directory + */ + fun listResourceFiles(location: Path) : List { + return location.toFile().walkTopDown().filter { + it.name.matches(PROPERTIES_FILE_REGEX) + }.map { it.name }.toList() + } + + /** + * Create a classloader with all URLs in a given directory + */ + fun loaderFromDirectory(location: Path) : URLClassLoader { + val urls = arrayOf(location.toUri().toURL()) + val sysLoader = ClassLoader.getSystemClassLoader() + return URLClassLoader(urls, sysLoader) + } + } +} \ No newline at end of file diff --git a/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorTool.kt b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorTool.kt new file mode 100644 index 0000000000..1c2f620c76 --- /dev/null +++ b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorTool.kt @@ -0,0 +1,29 @@ +package net.corda.errorUtilities + +import net.corda.cliutils.CordaCliWrapper +import net.corda.cliutils.ExitCodes +import net.corda.cliutils.start +import net.corda.errorUtilities.docsTable.DocsTableCLI +import net.corda.errorUtilities.resourceGenerator.ResourceGeneratorCLI + +fun main(args: Array) = ErrorTool().start(args) + + +/** + * Entry point for the error utilities. + * + * By itself, this doesn't do anything - instead one of the subcommands should be invoked. + */ +class ErrorTool : CordaCliWrapper("error-utils", "Utilities for working with error codes and error reporting") { + + private val errorPageBuilder = DocsTableCLI() + private val errorResourceGenerator = ResourceGeneratorCLI() + + override fun additionalSubCommands() = setOf(errorPageBuilder, errorResourceGenerator) + + override fun runProgram(): Int { + println("No subcommand specified - please invoke one of the subcommands.") + printHelp() + return ExitCodes.FAILURE + } +} \ No newline at end of file diff --git a/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorToolCLIUtilities.kt b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorToolCLIUtilities.kt new file mode 100644 index 0000000000..b76c655b68 --- /dev/null +++ b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorToolCLIUtilities.kt @@ -0,0 +1,23 @@ +package net.corda.errorUtilities + +import java.lang.IllegalArgumentException +import java.nio.file.Files +import java.nio.file.Path + +/** + * Common functions to use among multiple of the error code subcommands + */ +class ErrorToolCLIUtilities { + companion object { + /** + * Checks that a directory provided through Picocli exists. + */ + fun checkDirectory(dir: Path?, expectedContents: String) : Path { + return dir?.also { + require(Files.exists(it)) { + "Directory $it does not exist. Please specify a valid direction for $expectedContents" + } + } ?: throw IllegalArgumentException("No location specified for $expectedContents. Please specify a directory for $expectedContents.") + } + } +} \ No newline at end of file diff --git a/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorToolExceptions.kt b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorToolExceptions.kt new file mode 100644 index 0000000000..070316493b --- /dev/null +++ b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/ErrorToolExceptions.kt @@ -0,0 +1,7 @@ +package net.corda.errorUtilities + +abstract class ErrorToolException(msg: String, cause: Exception? = null) : Exception(msg, cause) + +class ClassDoesNotExistException(classname: String) + : ErrorToolException("The class $classname could not be found in the provided JAR. " + + "Check that the correct fully qualified name has been provided and the JAR file is the correct one for this class.") \ No newline at end of file diff --git a/tools/error-page-builder/src/main/kotlin/net/corda/errorPageBuilder/ErrorPageBuilder.kt b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/docsTable/DocsTableCLI.kt similarity index 57% rename from tools/error-page-builder/src/main/kotlin/net/corda/errorPageBuilder/ErrorPageBuilder.kt rename to tools/error-tool/src/main/kotlin/net/corda/errorUtilities/docsTable/DocsTableCLI.kt index abe122310d..7773be6ff7 100644 --- a/tools/error-page-builder/src/main/kotlin/net/corda/errorPageBuilder/ErrorPageBuilder.kt +++ b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/docsTable/DocsTableCLI.kt @@ -1,22 +1,24 @@ -package net.corda.errorPageBuilder +package net.corda.errorUtilities.docsTable import net.corda.cliutils.CordaCliWrapper import net.corda.cliutils.ExitCodes -import net.corda.cliutils.start +import net.corda.errorUtilities.ErrorToolCLIUtilities import org.slf4j.LoggerFactory import picocli.CommandLine -import java.io.File import java.lang.IllegalArgumentException import java.nio.file.Files import java.nio.file.Path import java.util.* -fun main(args: Array) { - val builder = ErrorPageBuilder() - builder.start(args) -} - -class ErrorPageBuilder : CordaCliWrapper("error-page-builder", "Builds the error table for the error codes page") { +/** + * Error tool sub-command for generating the documentation for error codes. + * + * The command needs a location to output the documentation to and a directory containing the resource files. From this, it generates a + * Markdown table with all defined error codes. + * + * In the event that the file already exists, the tool will report an error and exit. + */ +class DocsTableCLI : CordaCliWrapper("build-docs", "Builds the error table for the error codes page") { @CommandLine.Parameters( index = "0", @@ -42,43 +44,27 @@ class ErrorPageBuilder : CordaCliWrapper("error-page-builder", "Builds the error var localeTag: String? = null companion object { - private val logger = LoggerFactory.getLogger(ErrorPageBuilder::class.java) + private val logger = LoggerFactory.getLogger(DocsTableCLI::class.java) private const val ERROR_CODES_FILE = "error-codes.md" } - private fun getOutputFile() : File { - return outputDir?.let { - require(Files.exists(it)) { - "Directory $it does not exist. Please specify a valid directory to write output to." - } - val outputPath = it.resolve(ERROR_CODES_FILE) - require(Files.notExists(outputPath)) { - "Output file $outputPath exists, please remove it and run again." - } - outputPath.toFile() - } ?: throw IllegalArgumentException("Directory not specified. Please specify a valid directory to write output to.") - } - - private fun getResourceDir() : Path { - return resourceLocation?.also { - require(Files.exists(it)) { - "Resource location $it does not exist. Please specify a valid location for error code resources" - } - } ?: throw IllegalArgumentException("Resource location not specified. Please specify a resource location.") - } - override fun runProgram(): Int { val locale = if (localeTag != null) Locale.forLanguageTag(localeTag) else Locale.getDefault() val (outputFile, resources) = try { - Pair(getOutputFile(), getResourceDir()) + val output = ErrorToolCLIUtilities.checkDirectory(outputDir, "output file") + val outputPath = output.resolve(ERROR_CODES_FILE) + require(Files.notExists(outputPath)) { + "Output file $outputPath exists, please remove it and run again." + } + Pair(outputPath, ErrorToolCLIUtilities.checkDirectory(resourceLocation, "resource bundle files")) } catch (e: IllegalArgumentException) { logger.error(e.message, e) return ExitCodes.FAILURE } - val tableGenerator = ErrorTableGenerator(resources.toFile(), locale) + val tableGenerator = DocsTableGenerator(resources, locale) try { val table = tableGenerator.generateMarkdown() - outputFile.writeText(table) + outputFile.toFile().writeText(table) } catch (e: IllegalArgumentException) { logger.error(e.message, e) return ExitCodes.FAILURE diff --git a/tools/error-page-builder/src/main/kotlin/net/corda/errorPageBuilder/ErrorTableGenerator.kt b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/docsTable/DocsTableGenerator.kt similarity index 63% rename from tools/error-page-builder/src/main/kotlin/net/corda/errorPageBuilder/ErrorTableGenerator.kt rename to tools/error-tool/src/main/kotlin/net/corda/errorUtilities/docsTable/DocsTableGenerator.kt index 6cf13436a2..d16675bf55 100644 --- a/tools/error-page-builder/src/main/kotlin/net/corda/errorPageBuilder/ErrorTableGenerator.kt +++ b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/docsTable/DocsTableGenerator.kt @@ -1,13 +1,16 @@ -package net.corda.errorPageBuilder +package net.corda.errorUtilities.docsTable import net.corda.common.logging.errorReporting.ErrorResource -import java.io.File +import net.corda.errorUtilities.ErrorResourceUtilities import java.lang.IllegalArgumentException -import java.net.URLClassLoader +import java.nio.file.Path import java.util.* -class ErrorTableGenerator(private val resourceLocation: File, - private val locale: Locale) { +/** + * Generate the documentation table given a resource file location set. + */ +class DocsTableGenerator(private val resourceLocation: Path, + private val locale: Locale) { companion object { private const val ERROR_CODE_HEADING = "codeHeading" @@ -15,7 +18,6 @@ class ErrorTableGenerator(private val resourceLocation: File, private const val DESCRIPTION_HEADING = "descriptionHeading" private const val TO_FIX_HEADING = "toFixHeading" private const val ERROR_HEADINGS_BUNDLE = "ErrorPageHeadings" - private const val ERROR_INFO_RESOURCE = "ErrorInfo.properties" } private fun getHeading(heading: String) : String { @@ -23,23 +25,10 @@ class ErrorTableGenerator(private val resourceLocation: File, return resource.getString(heading) } - private fun listResources() : Iterator { - return resourceLocation.walkTopDown().filter { - it.name.matches("[^_]*\\.properties".toRegex()) && !it.name.matches(ERROR_INFO_RESOURCE.toRegex()) - }.map { - it.nameWithoutExtension - }.iterator() - } - - private fun createLoader() : ClassLoader { - val urls = resourceLocation.walkTopDown().map { it.toURI().toURL() }.asIterable().toList().toTypedArray() - return URLClassLoader(urls) - } - private fun generateTable() : List> { val table = mutableListOf>() - val loader = createLoader() - for (resource in listResources()) { + val loader = ErrorResourceUtilities.loaderFromDirectory(resourceLocation) + for (resource in ErrorResourceUtilities.listResourceNames(resourceLocation)) { val errorResource = ErrorResource.fromLoader(resource, loader, locale) table.add(listOf(resource, errorResource.aliases, errorResource.shortDescription, errorResource.actionsToFix)) } @@ -59,7 +48,7 @@ class ErrorTableGenerator(private val resourceLocation: File, } fun generateMarkdown() : String { - if (!resourceLocation.exists()) throw IllegalArgumentException("Directory $resourceLocation does not exist.") + if (!resourceLocation.toFile().exists()) throw IllegalArgumentException("Directory $resourceLocation does not exist.") val tableData = generateTable() return formatTable(tableData) } diff --git a/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/resourceGenerator/ResourceGenerator.kt b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/resourceGenerator/ResourceGenerator.kt new file mode 100644 index 0000000000..2cb34684dd --- /dev/null +++ b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/resourceGenerator/ResourceGenerator.kt @@ -0,0 +1,79 @@ +package net.corda.errorUtilities.resourceGenerator + +import net.corda.common.logging.errorReporting.ErrorCodes +import net.corda.common.logging.errorReporting.ResourceBundleProperties +import net.corda.errorUtilities.ClassDoesNotExistException +import java.nio.file.Path +import java.util.* + +/** + * Generate a set of resource files from an enumeration of error codes. + */ +class ResourceGenerator(private val locales: List) { + + companion object { + internal const val MESSAGE_TEMPLATE_DEFAULT = "" + internal const val SHORT_DESCRIPTION_DEFAULT = "" + internal const val ACTIONS_TO_FIX_DEFAULT = "" + internal const val ALIASES_DEFAULT = "" + } + + private fun createResourceFile(name: String, location: Path) { + val file = location.resolve(name) + val text = """ + |${ResourceBundleProperties.MESSAGE_TEMPLATE} = $MESSAGE_TEMPLATE_DEFAULT + |${ResourceBundleProperties.SHORT_DESCRIPTION} = $SHORT_DESCRIPTION_DEFAULT + |${ResourceBundleProperties.ACTIONS_TO_FIX} = $ACTIONS_TO_FIX_DEFAULT + |${ResourceBundleProperties.ALIASES} = $ALIASES_DEFAULT + """.trimMargin() + file.toFile().writeText(text) + } + + /** + * Create a set of resource files in the given location. + * + * @param resources The resource file names to create + * @param resourceLocation The location to create the resource files + */ + fun createResources(resources: List, resourceLocation: Path) { + for (resource in resources) { + createResourceFile(resource, resourceLocation) + } + } + + private fun definedCodes(classes: List, loader: ClassLoader) : List { + return classes.flatMap { + val clazz = try { + loader.loadClass(it) + } catch (e: ClassNotFoundException) { + throw ClassDoesNotExistException(it) + } + if (ErrorCodes::class.java.isAssignableFrom(clazz) && clazz != ErrorCodes::class.java) { + val namespace = (clazz.enumConstants.first() as ErrorCodes).namespace.toLowerCase() + clazz.enumConstants.map { code -> "${namespace}-${code.toString().toLowerCase().replace("_", "-")}"} + } else { + listOf() + } + } + } + + private fun getExpectedResources(codes: List) : List { + return codes.flatMap { + val localeResources = locales.map { locale -> "${it}_${locale.toLanguageTag().replace("-", "_")}.properties"} + localeResources + "$it.properties" + } + } + + /** + * Calculate what resource files are missing from a set of resource files, given a set of error codes. + * + * @param classes The classes to generate resource files for + * @param resourceFiles The list of resource files + */ + fun calculateMissingResources(classes: List, resourceFiles: List, loader: ClassLoader) : List { + val codes = definedCodes(classes, loader) + val expected = getExpectedResources(codes) + val missing = expected - resourceFiles.toSet() + return missing.toList() + } +} \ No newline at end of file diff --git a/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/resourceGenerator/ResourceGeneratorCLI.kt b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/resourceGenerator/ResourceGeneratorCLI.kt new file mode 100644 index 0000000000..49b28f52ba --- /dev/null +++ b/tools/error-tool/src/main/kotlin/net/corda/errorUtilities/resourceGenerator/ResourceGeneratorCLI.kt @@ -0,0 +1,75 @@ +package net.corda.errorUtilities.resourceGenerator + +import net.corda.cliutils.CordaCliWrapper +import net.corda.cliutils.ExitCodes +import net.corda.errorUtilities.ErrorResourceUtilities +import net.corda.errorUtilities.ErrorToolCLIUtilities +import org.slf4j.LoggerFactory +import picocli.CommandLine +import java.lang.IllegalArgumentException +import java.nio.file.Path +import java.util.* + +/** + * Subcommand for generating resource bundles from error codes. + * + * This subcommand takes a directory containing built class files that define the enumerations of codes, and a directory containing any + * existing resource bundles. From this, it generates any missing resource files with the properties specified. The data under these + * properties should then be filled in by hand. + */ +class ResourceGeneratorCLI : CordaCliWrapper( + "generate-resources", + "Generate any missing resource files for a set of error codes" +) { + + @CommandLine.Parameters( + index = "0", + paramLabel = "JAR_FILE", + arity = "1", + description = ["JAR file containing class files of the error code definitions"] + ) + var jarFile: Path? = null + + @CommandLine.Parameters( + index = "1", + paramLabel = "RESOURCE_DIR", + arity = "1", + description = ["Directory containing resource bundles for the error codes"] + ) + var resourceDir: Path? = null + + @CommandLine.Parameters( + index="2..*", + paramLabel = "ERROR_CODE_CLASSES", + description = ["Fully qualified class names of the error code classes to generate resources for"] + ) + var classes: List = mutableListOf() + + @CommandLine.Option( + names = ["--locales"], + description = ["The set of locales to generate resource files for. Specified as locale tags, for example en-US"], + arity = "1" + ) + var locales: List = listOf("en-US") + + companion object { + private val logger = LoggerFactory.getLogger(ResourceGeneratorCLI::class.java) + } + + override fun runProgram(): Int { + val jarFileLocation = ErrorToolCLIUtilities.checkDirectory(jarFile, "error code definition class files") + val resourceLocation = ErrorToolCLIUtilities.checkDirectory(resourceDir, "resource bundle files") + val resourceGenerator = ResourceGenerator(locales.map { Locale.forLanguageTag(it) }) + try { + val resources = ErrorResourceUtilities.listResourceFiles(resourceLocation) + val loader = ErrorResourceUtilities.loaderFromDirectory(jarFileLocation) + val missingResources = resourceGenerator.calculateMissingResources(classes, resources, loader) + resourceGenerator.createResources(missingResources, resourceLocation) + loader.close() + } catch (e: IllegalArgumentException) { + logger.error(e.message, e) + return ExitCodes.FAILURE + } + return ExitCodes.SUCCESS + } +} \ No newline at end of file diff --git a/tools/error-page-builder/src/main/resources/ErrorPageHeadings.properties b/tools/error-tool/src/main/resources/ErrorPageHeadings.properties similarity index 100% rename from tools/error-page-builder/src/main/resources/ErrorPageHeadings.properties rename to tools/error-tool/src/main/resources/ErrorPageHeadings.properties diff --git a/tools/error-page-builder/src/main/resources/log4j2.xml b/tools/error-tool/src/main/resources/log4j2.xml similarity index 100% rename from tools/error-page-builder/src/main/resources/log4j2.xml rename to tools/error-tool/src/main/resources/log4j2.xml diff --git a/tools/error-page-builder/src/test/kotlin/ErrorTableGeneratorTest.kt b/tools/error-tool/src/test/kotlin/net/corda/errorUtilities/docsTable/DocsTableGeneratorTest.kt similarity index 74% rename from tools/error-page-builder/src/test/kotlin/ErrorTableGeneratorTest.kt rename to tools/error-tool/src/test/kotlin/net/corda/errorUtilities/docsTable/DocsTableGeneratorTest.kt index 211576dda1..59ca2ed17f 100644 --- a/tools/error-page-builder/src/test/kotlin/ErrorTableGeneratorTest.kt +++ b/tools/error-tool/src/test/kotlin/net/corda/errorUtilities/docsTable/DocsTableGeneratorTest.kt @@ -1,15 +1,15 @@ +package net.corda.errorUtilities.docsTable + import junit.framework.TestCase.assertEquals -import net.corda.errorPageBuilder.ErrorTableGenerator import org.junit.Test -import java.io.File import java.lang.IllegalArgumentException import java.nio.file.Paths import java.util.* -class ErrorTableGeneratorTest { +class DocsTableGeneratorTest { companion object { - private val RESOURCE_LOCATION = Paths.get("src/test/resources/test-errors").toAbsolutePath().toFile() + private val RESOURCE_LOCATION = Paths.get("src/test/resources/test-errors").toAbsolutePath() } private val englishTable = """| Error Code | Aliases | Description | Actions to Fix | @@ -22,24 +22,24 @@ class ErrorTableGeneratorTest { /| test-error | foo, bar | Teachtaireacht tástála | Roinnt gníomhartha | """.trimMargin("/") - @Test(timeout = 300_000) + @Test(timeout = 1000) fun `check error table is produced as expected`() { - val generator = ErrorTableGenerator(RESOURCE_LOCATION, Locale.forLanguageTag("en-US")) + val generator = DocsTableGenerator(RESOURCE_LOCATION, Locale.forLanguageTag("en-US")) val table = generator.generateMarkdown() // Raw strings in Kotlin always use Unix line endings, so this is required to keep the test passing on Windows assertEquals(englishTable.split("\n").joinToString(System.lineSeparator()), table) } - @Test(timeout = 300_000) + @Test(timeout = 1000) fun `check table in other locales is produced as expected`() { - val generator = ErrorTableGenerator(RESOURCE_LOCATION, Locale.forLanguageTag("ga-IE")) + val generator = DocsTableGenerator(RESOURCE_LOCATION, Locale.forLanguageTag("ga-IE")) val table = generator.generateMarkdown() assertEquals(irishTable.split("\n").joinToString(System.lineSeparator()), table) } - @Test(expected = IllegalArgumentException::class, timeout = 300_000) + @Test(expected = IllegalArgumentException::class, timeout = 1000) fun `error thrown if unknown directory passed to generator`() { - val generator = ErrorTableGenerator(File("not/a/directory"), Locale.getDefault()) + val generator = DocsTableGenerator(Paths.get("not/a/directory"), Locale.getDefault()) generator.generateMarkdown() } } \ No newline at end of file diff --git a/tools/error-tool/src/test/kotlin/net/corda/errorUtilities/resourceGenerator/ResourceGeneratorTest.kt b/tools/error-tool/src/test/kotlin/net/corda/errorUtilities/resourceGenerator/ResourceGeneratorTest.kt new file mode 100644 index 0000000000..0e8b2be854 --- /dev/null +++ b/tools/error-tool/src/test/kotlin/net/corda/errorUtilities/resourceGenerator/ResourceGeneratorTest.kt @@ -0,0 +1,56 @@ +package net.corda.errorUtilities.resourceGenerator + +import junit.framework.TestCase.assertEquals +import net.corda.common.logging.errorReporting.ResourceBundleProperties +import org.junit.Test +import java.util.* + +class ResourceGeneratorTest { + + private val classes = listOf(TestCodes1::class.qualifiedName!!, TestCodes2::class.qualifiedName!!) + + private fun expectedCodes() : List { + val codes1 = TestCodes1.values().map { "${it.namespace.toLowerCase()}-${it.name.replace("_", "-").toLowerCase()}" } + val codes2 = TestCodes2.values().map { "${it.namespace.toLowerCase()}-${it.name.replace("_", "-").toLowerCase()}" } + return codes1 + codes2 + } + + @Test(timeout = 1000) + fun `no codes marked as missing if all resources are present`() { + val resourceGenerator = ResourceGenerator(listOf()) + val currentFiles = expectedCodes().map { "$it.properties" } + val missing = resourceGenerator.calculateMissingResources(classes, currentFiles, TestCodes1::class.java.classLoader) + assertEquals(setOf(), missing.toSet()) + } + + @Test(timeout = 1000) + fun `missing locales are marked as missing when other locales are present`() { + val resourceGenerator = ResourceGenerator(listOf("en-US", "ga-IE").map { Locale.forLanguageTag(it) }) + val currentFiles = expectedCodes().flatMap { listOf("$it.properties", "${it}_en_US.properties") } + val missing = resourceGenerator.calculateMissingResources(classes, currentFiles, TestCodes1::class.java.classLoader) + assertEquals(expectedCodes().map { "${it}_ga_IE.properties" }.toSet(), missing.toSet()) + } + + @Test(timeout = 1000) + fun `test writing out files works correctly`() { + // First test that if all files are missing then the resource generator detects this + val resourceGenerator = ResourceGenerator(listOf()) + val currentFiles = listOf() + val missing = resourceGenerator.calculateMissingResources(classes, currentFiles, TestCodes1::class.java.classLoader) + assertEquals(expectedCodes().map { "$it.properties" }.toSet(), missing.toSet()) + + // Now check that all resource files that should be created are + val tempDir = createTempDir() + resourceGenerator.createResources(missing, tempDir.toPath()) + val createdFiles = tempDir.walkTopDown().filter { it.isFile && it.extension == "properties" }.map { it.name }.toList() + assertEquals(missing, createdFiles) + + // Now check that a created file has the expected properties and values + val properties = Properties() + properties.load(tempDir.walk().filter { it.isFile && it.extension == "properties"}.first().inputStream()) + assertEquals(ResourceGenerator.SHORT_DESCRIPTION_DEFAULT, properties.getProperty(ResourceBundleProperties.SHORT_DESCRIPTION)) + assertEquals(ResourceGenerator.ACTIONS_TO_FIX_DEFAULT, properties.getProperty(ResourceBundleProperties.ACTIONS_TO_FIX)) + assertEquals(ResourceGenerator.MESSAGE_TEMPLATE_DEFAULT, properties.getProperty(ResourceBundleProperties.MESSAGE_TEMPLATE)) + assertEquals(ResourceGenerator.ALIASES_DEFAULT, properties.getProperty(ResourceBundleProperties.ALIASES)) + } +} \ No newline at end of file diff --git a/tools/error-tool/src/test/kotlin/net/corda/errorUtilities/resourceGenerator/TestErrorCodes.kt b/tools/error-tool/src/test/kotlin/net/corda/errorUtilities/resourceGenerator/TestErrorCodes.kt new file mode 100644 index 0000000000..0e33357797 --- /dev/null +++ b/tools/error-tool/src/test/kotlin/net/corda/errorUtilities/resourceGenerator/TestErrorCodes.kt @@ -0,0 +1,23 @@ +package net.corda.errorUtilities.resourceGenerator + +import net.corda.common.logging.errorReporting.ErrorCodes + +// These test errors are not used directly, but their compiled class files are used to verify the resource generator functionality. +enum class TestNamespaces { + TN1, + TN2 +} + +enum class TestCodes1 : ErrorCodes { + CASE1, + CASE2; + + override val namespace = TestNamespaces.TN1.toString() +} + +enum class TestCodes2 : ErrorCodes { + CASE1, + CASE3; + + override val namespace = TestNamespaces.TN2.toString() +} \ No newline at end of file diff --git a/tools/error-page-builder/src/test/resources/ErrorPageHeadings.properties b/tools/error-tool/src/test/resources/ErrorPageHeadings.properties similarity index 100% rename from tools/error-page-builder/src/test/resources/ErrorPageHeadings.properties rename to tools/error-tool/src/test/resources/ErrorPageHeadings.properties diff --git a/tools/error-page-builder/src/test/resources/ErrorPageHeadings_ga_IE.properties b/tools/error-tool/src/test/resources/ErrorPageHeadings_ga_IE.properties similarity index 100% rename from tools/error-page-builder/src/test/resources/ErrorPageHeadings_ga_IE.properties rename to tools/error-tool/src/test/resources/ErrorPageHeadings_ga_IE.properties diff --git a/tools/error-page-builder/src/test/resources/test-errors/test-error.properties b/tools/error-tool/src/test/resources/test-errors/test-error.properties similarity index 100% rename from tools/error-page-builder/src/test/resources/test-errors/test-error.properties rename to tools/error-tool/src/test/resources/test-errors/test-error.properties diff --git a/tools/error-page-builder/src/test/resources/test-errors/test-error_ga_IE.properties b/tools/error-tool/src/test/resources/test-errors/test-error_ga_IE.properties similarity index 100% rename from tools/error-page-builder/src/test/resources/test-errors/test-error_ga_IE.properties rename to tools/error-tool/src/test/resources/test-errors/test-error_ga_IE.properties