[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:
James Higgs 2020-04-29 11:21:50 +01:00 committed by GitHub
parent ab43238420
commit ab95aa57a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 566 additions and 85 deletions

View File

@ -43,6 +43,11 @@ class ErrorReporting private constructor(private val localeString: String,
return ErrorReporting(localeString, location, contextProvider) 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 { fun withContextProvider(contextProvider: ErrorContextProvider) : ErrorReporting {
return ErrorReporting(localeString, resourceLocation, contextProvider) return ErrorReporting(localeString, resourceLocation, contextProvider)
} }

View File

@ -4,7 +4,8 @@ package net.corda.common.logging.errorReporting
* Namespaces for errors within the node. * Namespaces for errors within the node.
*/ */
enum class NodeNamespaces { enum class NodeNamespaces {
DATABASE DATABASE,
CORDAPP
} }
/** /**
@ -17,4 +18,16 @@ enum class NodeDatabaseErrors : ErrorCodes {
PASSWORD_REQUIRED_FOR_H2; PASSWORD_REQUIRED_FOR_H2;
override val namespace = NodeNamespaces.DATABASE.toString() 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()
} }

View File

@ -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

View File

@ -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.

View File

@ -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 =

View File

@ -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 =

View File

@ -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 =

View File

@ -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.

View File

@ -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 =

View File

@ -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 =

View File

@ -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")
)
}

View File

@ -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())
)
}

View File

@ -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")
}
}

View File

@ -2,6 +2,10 @@ package net.corda.node.internal.cordapp
import io.github.classgraph.ClassGraph import io.github.classgraph.ClassGraph
import io.github.classgraph.ScanResult 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.cordapp.Cordapp
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256 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() val duplicateCordapps = registeredCordapps.filter { it.jarHash == cordapp.jarHash }.toSet()
if (duplicateCordapps.isNotEmpty()) { if (duplicateCordapps.isNotEmpty()) {
throw IllegalStateException("The CorDapp (name: ${cordapp.info.shortName}, file: ${cordapp.name}) " + throw DuplicateCordappsInstalledException(cordapp, duplicateCordapps)
"is installed multiple times on the node. The following files correspond to the exact same content: " +
"${duplicateCordapps.map { it.name }}")
} }
if (registeredClassName in contractClasses) { if (registeredClassName in contractClasses) {
throw IllegalStateException("More than one CorDapp installed on the node for contract $registeredClassName. " + 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 { private fun parseVersion(versionStr: String?, attributeName: String): Int {
if (versionStr == null) { 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() 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) { 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 return version
} }
@ -403,12 +414,34 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
/** /**
* Thrown when scanning CorDapps. * 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 * 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 { abstract class CordappLoaderTemplate : CordappLoader {
@ -436,7 +469,9 @@ abstract class CordappLoaderTemplate : CordappLoader {
} }
} }
throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow " + 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 entry.value.single().second
} }

View File

@ -1,6 +1,7 @@
package net.corda.node.internal.cordapp package net.corda.node.internal.cordapp
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.common.logging.errorReporting.CordappErrors
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.node.VersionInfo import net.corda.node.VersionInfo
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES

View File

@ -108,5 +108,5 @@ include 'serialization-deterministic'
include 'tools:checkpoint-agent' include 'tools:checkpoint-agent'
include 'detekt-plugins' include 'detekt-plugins'
include 'tools:error-page-builder' include 'tools:error-tool'

View File

@ -1,6 +1,3 @@
group 'net.corda'
version '4.5-SNAPSHOT'
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
@ -12,9 +9,9 @@ dependencies {
implementation project(":common-logging") implementation project(":common-logging")
implementation project(":tools:cliutils") implementation project(":tools:cliutils")
implementation "info.picocli:picocli:$picocli_version" implementation "info.picocli:picocli:$picocli_version"
testCompile group: 'junit', name: 'junit', version: '4.12'
implementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" implementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
testCompile "junit:junit:4.12"
} }
jar { jar {
@ -23,10 +20,12 @@ jar {
} }
shadowJar { shadowJar {
baseName = "corda-tools-error-page-builder" baseName = "corda-tools-error-utils"
manifest { manifest {
attributes( attributes(
'Main-Class': "net.corda.errorPageBuilder.ErrorPageBuilderKt" 'Main-Class': "net.corda.errorUtilities.ErrorToolKt"
) )
} }
} }
assemble.dependsOn shadowJar

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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.")
}
}
}

View File

@ -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.")

View File

@ -1,22 +1,24 @@
package net.corda.errorPageBuilder package net.corda.errorUtilities.docsTable
import net.corda.cliutils.CordaCliWrapper import net.corda.cliutils.CordaCliWrapper
import net.corda.cliutils.ExitCodes import net.corda.cliutils.ExitCodes
import net.corda.cliutils.start import net.corda.errorUtilities.ErrorToolCLIUtilities
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import picocli.CommandLine import picocli.CommandLine
import java.io.File
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.* import java.util.*
fun main(args: Array<String>) { /**
val builder = ErrorPageBuilder() * Error tool sub-command for generating the documentation for error codes.
builder.start(args) *
} * 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.
class ErrorPageBuilder : CordaCliWrapper("error-page-builder", "Builds the error table for the error codes page") { *
* 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( @CommandLine.Parameters(
index = "0", index = "0",
@ -42,43 +44,27 @@ class ErrorPageBuilder : CordaCliWrapper("error-page-builder", "Builds the error
var localeTag: String? = null var localeTag: String? = null
companion object { 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 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 { override fun runProgram(): Int {
val locale = if (localeTag != null) Locale.forLanguageTag(localeTag) else Locale.getDefault() val locale = if (localeTag != null) Locale.forLanguageTag(localeTag) else Locale.getDefault()
val (outputFile, resources) = try { 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) { } catch (e: IllegalArgumentException) {
logger.error(e.message, e) logger.error(e.message, e)
return ExitCodes.FAILURE return ExitCodes.FAILURE
} }
val tableGenerator = ErrorTableGenerator(resources.toFile(), locale) val tableGenerator = DocsTableGenerator(resources, locale)
try { try {
val table = tableGenerator.generateMarkdown() val table = tableGenerator.generateMarkdown()
outputFile.writeText(table) outputFile.toFile().writeText(table)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
logger.error(e.message, e) logger.error(e.message, e)
return ExitCodes.FAILURE return ExitCodes.FAILURE

View File

@ -1,13 +1,16 @@
package net.corda.errorPageBuilder package net.corda.errorUtilities.docsTable
import net.corda.common.logging.errorReporting.ErrorResource import net.corda.common.logging.errorReporting.ErrorResource
import java.io.File import net.corda.errorUtilities.ErrorResourceUtilities
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import java.net.URLClassLoader import java.nio.file.Path
import java.util.* 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 { companion object {
private const val ERROR_CODE_HEADING = "codeHeading" 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 DESCRIPTION_HEADING = "descriptionHeading"
private const val TO_FIX_HEADING = "toFixHeading" private const val TO_FIX_HEADING = "toFixHeading"
private const val ERROR_HEADINGS_BUNDLE = "ErrorPageHeadings" private const val ERROR_HEADINGS_BUNDLE = "ErrorPageHeadings"
private const val ERROR_INFO_RESOURCE = "ErrorInfo.properties"
} }
private fun getHeading(heading: String) : String { private fun getHeading(heading: String) : String {
@ -23,23 +25,10 @@ class ErrorTableGenerator(private val resourceLocation: File,
return resource.getString(heading) 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>> { private fun generateTable() : List<List<String>> {
val table = mutableListOf<List<String>>() val table = mutableListOf<List<String>>()
val loader = createLoader() val loader = ErrorResourceUtilities.loaderFromDirectory(resourceLocation)
for (resource in listResources()) { for (resource in ErrorResourceUtilities.listResourceNames(resourceLocation)) {
val errorResource = ErrorResource.fromLoader(resource, loader, locale) val errorResource = ErrorResource.fromLoader(resource, loader, locale)
table.add(listOf(resource, errorResource.aliases, errorResource.shortDescription, errorResource.actionsToFix)) table.add(listOf(resource, errorResource.aliases, errorResource.shortDescription, errorResource.actionsToFix))
} }
@ -59,7 +48,7 @@ class ErrorTableGenerator(private val resourceLocation: File,
} }
fun generateMarkdown() : String { 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() val tableData = generateTable()
return formatTable(tableData) return formatTable(tableData)
} }

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -1,15 +1,15 @@
package net.corda.errorUtilities.docsTable
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import net.corda.errorPageBuilder.ErrorTableGenerator
import org.junit.Test import org.junit.Test
import java.io.File
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import java.nio.file.Paths import java.nio.file.Paths
import java.util.* import java.util.*
class ErrorTableGeneratorTest { class DocsTableGeneratorTest {
companion object { 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 | 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 | /| test-error | foo, bar | Teachtaireacht tástála | Roinnt gníomhartha |
""".trimMargin("/") """.trimMargin("/")
@Test(timeout = 300_000) @Test(timeout = 1000)
fun `check error table is produced as expected`() { 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() val table = generator.generateMarkdown()
// Raw strings in Kotlin always use Unix line endings, so this is required to keep the test passing on Windows // 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) 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`() { 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() val table = generator.generateMarkdown()
assertEquals(irishTable.split("\n").joinToString(System.lineSeparator()), table) 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`() { 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() generator.generateMarkdown()
} }
} }

View File

@ -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))
}
}

View File

@ -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()
}