[EG-440] Add some error codes and the error resource generation tool ()

* [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)
}
/**
* 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)
}

View File

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

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

View File

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

View File

@ -108,5 +108,5 @@ include 'serialization-deterministic'
include 'tools:checkpoint-agent'
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: '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

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

View File

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

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

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