mirror of
https://github.com/corda/corda.git
synced 2024-12-23 14:52:29 +00:00
[EG-440] Add some error codes and the error resource generation tool (#6192)
* [EG-438] First commit of error code interface * [EG-438] Implement error reporter and a few error codes * [EG-438] Add unit tests and default properties files * [EG-438] Add the error table builder * [EG-438] Update initial properties files * [EG-438] Add some Irish tests and the build.gradle * [EG-438] Fall back for aliases and use different resource strategy * [EG-438] Define the URL using a project-specific context * [EG-438] Tidy up initialization code * [EG-438] Add testing to generator and tidy up * [EG-438] Remove direct dependency on core and add own logging config * [EG-438] Fix compiler warnings and tidy up logging * [EG-438] Fix detekt warnings * [EG-438] Improve error messages * [EG-438] Address first set of review comments * [EG-438] Use enums and a builder for the reporter * [EG-438] Address first set of review comments * [EG-438] Use enums and a builder for the reporter * [EG-438] Add kdocs for error resource static methods * [EG-440] Add error code for duplicate CorDapp loading * [EG-438] Handle enums defined with underscores * [EG-440] Add errors for some CorDapp loading scenarios * [EG-440] Finish adding errors for CorDapp loading * [EG-440] Fix up errors in properties files * [EG-440] Start change to error code definition * [EG-440] Update error code definition and add resource generation tool * [EG-440] Tidy up error resource generation tool frontend * [EG-440] Small refactorings and add kdocs * [EG-440] Generate all missing resources * [EG-440] Some refactoring and start writing a test * [EG-440] Update unit test for resource generator * [EG-440] Renaming of various parts of the error tool * [EG-440] Add testing for errors and fix an issue in resource generation * [EG-440] Add a kdoc for context provider API * [EG-440] Remove old code from repository * [EG-440] Address some review comments
This commit is contained in:
parent
ab43238420
commit
ab95aa57a2
common/logging/src
main
kotlin/net/corda/common/logging/errorReporting
resources/error-codes
cordapp-duplicate-cordapps-installed.propertiescordapp-duplicate-cordapps-installed_en_US.propertiescordapp-invalid-version-identifier.propertiescordapp-invalid-version-identifier_en_US.propertiescordapp-missing-version-attribute.propertiescordapp-missing-version-attribute_en_US.propertiescordapp-multiple-cordapps-for-flow.propertiescordapp-multiple-cordapps-for-flow_en_US.properties
test/kotlin/net/corda/commmon/logging/errorReporting
node/src
main/kotlin/net/corda/node/internal/cordapp
test/kotlin/net/corda/node/internal/cordapp
tools/error-tool
build.gradle
src
main
kotlin/net/corda/errorUtilities
ErrorResourceUtilities.ktErrorTool.ktErrorToolCLIUtilities.ktErrorToolExceptions.kt
docsTable
resourceGenerator
resources
test
kotlin/net/corda/errorUtilities
resources
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
@ -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.
|
@ -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 =
|
@ -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 =
|
@ -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 =
|
@ -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.
|
@ -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 =
|
@ -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 =
|
@ -0,0 +1,12 @@
|
||||
package net.corda.commmon.logging.errorReporting
|
||||
|
||||
import net.corda.common.logging.errorReporting.CordappErrors
|
||||
|
||||
class CordappErrorsTest : ErrorCodeTest<CordappErrors>(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")
|
||||
)
|
||||
}
|
@ -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>(NodeDatabaseErrors::class.java) {
|
||||
override val dataForCodes = mapOf(
|
||||
NodeDatabaseErrors.COULD_NOT_CONNECT to listOf<Any>(),
|
||||
NodeDatabaseErrors.FAILED_STARTUP to listOf(),
|
||||
NodeDatabaseErrors.MISSING_DRIVER to listOf(),
|
||||
NodeDatabaseErrors.PASSWORD_REQUIRED_FOR_H2 to listOf(InetAddress.getLocalHost())
|
||||
)
|
||||
}
|
@ -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<T>(private val clazz: Class<T>,
|
||||
private val printProperties: Boolean = false) where T: Enum<T>, T: ErrorCodes {
|
||||
|
||||
abstract val dataForCodes: Map<T, List<Any>>
|
||||
|
||||
private class TestError<T>(override val code: T,
|
||||
override val parameters: List<Any>) : ErrorCode<T> where T: Enum<T>, 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")
|
||||
}
|
||||
}
|
@ -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<CordappErrors> {
|
||||
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<Any> = listOf()
|
||||
) : Exception(msg), ErrorCode<CordappErrors>
|
||||
|
||||
/**
|
||||
* Thrown if duplicate CorDapps are installed on the node
|
||||
*/
|
||||
class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Set<Cordapp>)
|
||||
: 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<CordappErrors> {
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -108,5 +108,5 @@ include 'serialization-deterministic'
|
||||
|
||||
include 'tools:checkpoint-agent'
|
||||
include 'detekt-plugins'
|
||||
include 'tools:error-page-builder'
|
||||
include 'tools:error-tool'
|
||||
|
||||
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assemble.dependsOn shadowJar
|
@ -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<String> {
|
||||
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<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String>) = 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
|
||||
}
|
||||
}
|
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
@ -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.")
|
@ -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<String>) {
|
||||
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
|
@ -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<String> {
|
||||
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<List<String>> {
|
||||
val table = mutableListOf<List<String>>()
|
||||
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)
|
||||
}
|
@ -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<Locale>) {
|
||||
|
||||
companion object {
|
||||
internal const val MESSAGE_TEMPLATE_DEFAULT = "<Message template>"
|
||||
internal const val SHORT_DESCRIPTION_DEFAULT = "<Short description>"
|
||||
internal const val ACTIONS_TO_FIX_DEFAULT = "<Actions to fix>"
|
||||
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<String>, resourceLocation: Path) {
|
||||
for (resource in resources) {
|
||||
createResourceFile(resource, resourceLocation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun definedCodes(classes: List<String>, loader: ClassLoader) : List<String> {
|
||||
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<String>) : List<String> {
|
||||
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<String>, resourceFiles: List<String>, loader: ClassLoader) : List<String> {
|
||||
val codes = definedCodes(classes, loader)
|
||||
val expected = getExpectedResources(codes)
|
||||
val missing = expected - resourceFiles.toSet()
|
||||
return missing.toList()
|
||||
}
|
||||
}
|
@ -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<String> = 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<String> = 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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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<String> {
|
||||
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<String>(), 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<String>()
|
||||
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))
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user