From c2af6176b879758151f4803b5e907041913aaab4 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 29 May 2025 10:08:43 +0200 Subject: [PATCH 1/2] log4j-docgen: Support boxed and native Java types in XSD generation Previously, `SchemaGenerator` did not handle configuration attributes with boxed types (e.g., `Integer`, `Boolean`), leading to their omission from the generated XSD schema. This update introduces: * Support for boxed Java types as configuration attributes. * Improved handling of other native Java types that map to XML built-in data types (e.g., `BigDecimal`, `URL`). These enhancements ensure that all relevant configuration attributes are accurately represented in the schema. Fixes: #135 --- .../docgen/generator/SchemaGenerator.java | 60 ++++++++++--------- .../SchemaGeneratorTest/expected-plugins.xsd | 27 ++++++++- .../resources/SchemaGeneratorTest/plugins.xml | 25 ++++++++ src/changelog/.0.x.x/135_native-types.xml | 8 +++ 4 files changed, 91 insertions(+), 29 deletions(-) create mode 100644 src/changelog/.0.x.x/135_native-types.xml diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/SchemaGenerator.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/SchemaGenerator.java index e2874c31..707a6069 100644 --- a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/SchemaGenerator.java +++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/SchemaGenerator.java @@ -16,15 +16,21 @@ */ package org.apache.logging.log4j.docgen.generator; +import static java.util.Map.entry; import static java.util.Objects.requireNonNull; import java.io.IOException; import java.io.OutputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -61,6 +67,27 @@ public final class SchemaGenerator { private static final String CHARSET_NAME = "UTF-8"; + private static final Map XML_BUILTIN_TYPES = Map.ofEntries( + entry(BigDecimal.class.getName(), "decimal"), + entry(BigInteger.class.getName(), "integer"), + entry(boolean.class.getName(), "boolean"), + entry(Boolean.class.getName(), "boolean"), + entry(byte.class.getName(), "byte"), + entry(Byte.class.getName(), "byte"), + entry(double.class.getName(), "double"), + entry(Double.class.getName(), "double"), + entry(float.class.getName(), "float"), + entry(Float.class.getName(), "float"), + entry(int.class.getName(), "int"), + entry(Integer.class.getName(), "int"), + entry(short.class.getName(), "short"), + entry(Short.class.getName(), "short"), + entry(String.class.getName(), "string"), + entry(long.class.getName(), "long"), + entry(Long.class.getName(), "long"), + entry(URI.class.getName(), "anyURI"), + entry(URL.class.getName(), "anyURI")); + private SchemaGenerator() {} public static void generateSchema(final SchemaGeneratorArgs args) throws XMLStreamException { @@ -137,19 +164,7 @@ private static void writeTypes(final TypeLookup lookup, final XMLStreamWriter wr } private static boolean isBuiltinXmlType(final String className) { - switch (className) { - case "boolean": - case "byte": - case "double": - case "float": - case "int": - case "short": - case "long": - case "java.lang.String": - return true; - default: - return false; - } + return XML_BUILTIN_TYPES.containsKey(className); } private static void writeScalarType(final ScalarType type, final XMLStreamWriter writer) throws XMLStreamException { @@ -304,23 +319,12 @@ private static void writePluginAttribute( @Nullable private static String getXmlType(final TypeLookup lookup, final String className) { - switch (className) { - case "boolean": - case "byte": - case "double": - case "float": - case "int": - case "short": - case "long": - return className; - case "java.lang.String": - return "string"; + final String builtinType = XML_BUILTIN_TYPES.get(className); + if (builtinType != null) { + return builtinType; } final ArtifactSourcedType type = lookup.get(className); - if (type != null) { - return LOG4J_PREFIX + ":" + className; - } - return null; + return type != null ? LOG4J_PREFIX + ":" + className : null; } private static void writeMultiplicity( diff --git a/log4j-docgen/src/test/resources/SchemaGeneratorTest/expected-plugins.xsd b/log4j-docgen/src/test/resources/SchemaGeneratorTest/expected-plugins.xsd index ee0d5688..4cd4cb34 100644 --- a/log4j-docgen/src/test/resources/SchemaGeneratorTest/expected-plugins.xsd +++ b/log4j-docgen/src/test/resources/SchemaGeneratorTest/expected-plugins.xsd @@ -19,7 +19,8 @@ ~ This is a test schema used in `SchemaGeneratorTest`. ~ Unlike this file the `SchemaGenerator` strips whitespace. --> - + @@ -500,4 +501,28 @@ A conversion pattern is composed of literal text and format control expressions + + + Dummy plugin to test all types of builtin XML attributes. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/log4j-docgen/src/test/resources/SchemaGeneratorTest/plugins.xml b/log4j-docgen/src/test/resources/SchemaGeneratorTest/plugins.xml index b610c000..2f6f6966 100644 --- a/log4j-docgen/src/test/resources/SchemaGeneratorTest/plugins.xml +++ b/log4j-docgen/src/test/resources/SchemaGeneratorTest/plugins.xml @@ -267,6 +267,31 @@ A conversion pattern is composed of literal text and format control expressions + + + Dummy plugin to test all types of builtin XML attributes. + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/changelog/.0.x.x/135_native-types.xml b/src/changelog/.0.x.x/135_native-types.xml new file mode 100644 index 00000000..5b4d83f0 --- /dev/null +++ b/src/changelog/.0.x.x/135_native-types.xml @@ -0,0 +1,8 @@ + + + + Fix support of boxed and native Java types in XSD generation. + From 8ca898466970d1d01d4aab57dc521c78c5059d97 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 30 May 2025 13:01:35 +0200 Subject: [PATCH 2/2] log4j-docgen: Support attributes as a union of strict type and String This update enhances the generated XML schema by allowing each attribute to accept either its strict, expected type or a `${...}` expression. This accommodates use cases where property substitution is used, but at the same time allows IDE auto-completions. > [!WARNING] > This PR depends on #190 and should not be reviewed until that is merged. Closes #136 --- .../docgen/generator/SchemaGenerator.java | 130 ++++++-- .../SchemaGeneratorTest/expected-plugins.xsd | 298 ++++++++++++------ src/changelog/.0.x.x/136_union-types.xml | 8 + 3 files changed, 314 insertions(+), 122 deletions(-) create mode 100644 src/changelog/.0.x.x/136_union-types.xml diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/SchemaGenerator.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/SchemaGenerator.java index 707a6069..c947be7d 100644 --- a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/SchemaGenerator.java +++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/SchemaGenerator.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.inject.Named; @@ -67,6 +68,24 @@ public final class SchemaGenerator { private static final String CHARSET_NAME = "UTF-8"; + private static final String PROPERTY_SUBSTITUTION_TYPE = "property-substitution"; + private static final String BOOLEAN_TYPE = "boolean"; + private static final String STRING_TYPE = "string"; + private static final ScalarType BOOLEAN_SCALAR_TYPE = new ScalarType(); + + static { + BOOLEAN_SCALAR_TYPE.setClassName(BOOLEAN_TYPE); + final Description description = new Description(); + description.setText( + "A custom boolean type that allows `true`, `false`, or a property substitution expression."); + BOOLEAN_SCALAR_TYPE.setDescription(description); + for (final Boolean value : new Boolean[] {true, false}) { + final ScalarValue scalarValue = new ScalarValue(); + scalarValue.setName(value.toString()); + BOOLEAN_SCALAR_TYPE.addValue(scalarValue); + } + } + private static final Map XML_BUILTIN_TYPES = Map.ofEntries( entry(BigDecimal.class.getName(), "decimal"), entry(BigInteger.class.getName(), "integer"), @@ -138,6 +157,12 @@ private static void writeSchema(final String version, final TypeLookup lookup, f } private static void writeTypes(final TypeLookup lookup, final XMLStreamWriter writer) throws XMLStreamException { + writePropertySubstitutionType(writer); + // A union with member types `xsd:boolean` and `log4j:property-substitution` does not allow auto-completion + // in IDEs. This is why we define a `log4j:boolean` type from scratch. + writeScalarType(BOOLEAN_SCALAR_TYPE, writer); + writeUnionBuiltinTypes(writer); + for (final ArtifactSourcedType sourcedType : lookup.values()) { final Type type = sourcedType.type; if (isBuiltinXmlType(type.getClassName())) { @@ -167,12 +192,66 @@ private static boolean isBuiltinXmlType(final String className) { return XML_BUILTIN_TYPES.containsKey(className); } + /** + * A restriction of {@code string} that requires at least one property substitution expression {@code ${...}}. + */ + private static void writePropertySubstitutionType(final XMLStreamWriter writer) throws XMLStreamException { + writer.writeStartElement(XSD_NAMESPACE, "simpleType"); + writer.writeAttribute("name", PROPERTY_SUBSTITUTION_TYPE); + + writeDocumentation("A string with a property substitution expression.", writer); + + writer.writeStartElement(XSD_NAMESPACE, "restriction"); + writer.writeAttribute("base", "string"); + + writer.writeEmptyElement(XSD_NAMESPACE, "pattern"); + writer.writeAttribute("value", ".*\\$\\{.*\\}.*"); + + writer.writeEndElement(); + writer.writeEndElement(); + } + + /** + * Define types that are the union of a builtin type and {@value PROPERTY_SUBSTITUTION_TYPE}. + *

+ * IDEs don't propose auto-completion for these types. + *

+ */ + private static void writeUnionBuiltinTypes(final XMLStreamWriter writer) throws XMLStreamException { + final Collection types = new TreeSet<>(XML_BUILTIN_TYPES.values()); + // `xsd:string` is a superset of PROPERTY_SUBSTITUTION_TYPE, so no union is needed. + types.remove(STRING_TYPE); + // The union of `xsd:boolean` with PROPERTY_SUBSTITUTION_TYPE does not show auto-completion in IDEs. + // `log4j:boolean` will be generated from an _ad-hoc_ ScalarType definition in `base-log4j-types.xml`. + types.remove(BOOLEAN_TYPE); + for (final String type : types) { + writeUnionBuiltinType(type, writer); + } + } + + private static void writeUnionBuiltinType(final String type, final XMLStreamWriter writer) + throws XMLStreamException { + writer.writeStartElement(XSD_NAMESPACE, "simpleType"); + writer.writeAttribute("name", type); + + writeDocumentation("Union of `xsd:" + type + "` and ` " + PROPERTY_SUBSTITUTION_TYPE + "`.", writer); + + writer.writeEmptyElement(XSD_NAMESPACE, "union"); + writer.writeAttribute("memberTypes", type + " log4j:" + PROPERTY_SUBSTITUTION_TYPE); + + writer.writeEndElement(); + } + private static void writeScalarType(final ScalarType type, final XMLStreamWriter writer) throws XMLStreamException { writer.writeStartElement(XSD_NAMESPACE, "simpleType"); writer.writeAttribute("name", type.getClassName()); writeDocumentation(type.getDescription(), writer); + writer.writeStartElement(XSD_NAMESPACE, "union"); + writer.writeAttribute("memberTypes", "log4j:" + PROPERTY_SUBSTITUTION_TYPE); + writer.writeStartElement(XSD_NAMESPACE, "simpleType"); + writer.writeStartElement(XSD_NAMESPACE, "restriction"); writer.writeAttribute("base", "string"); @@ -182,6 +261,8 @@ private static void writeScalarType(final ScalarType type, final XMLStreamWriter writer.writeEndElement(); writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndElement(); } private static void writePluginType( @@ -240,22 +321,30 @@ private static void writeAbstractType( private static void writeDocumentation(@Nullable final Description description, final XMLStreamWriter writer) throws XMLStreamException { if (description != null) { - writer.writeStartElement(XSD_NAMESPACE, "annotation"); - writer.writeStartElement(XSD_NAMESPACE, "documentation"); - writer.writeCharacters(description.getText()); - writer.writeEndElement(); - writer.writeEndElement(); + writeDocumentation(description.getText(), writer); } } + private static void writeDocumentation(final String text, final XMLStreamWriter writer) throws XMLStreamException { + writer.writeStartElement(XSD_NAMESPACE, "annotation"); + writer.writeStartElement(XSD_NAMESPACE, "documentation"); + writer.writeCharacters(text); + writer.writeEndElement(); + writer.writeEndElement(); + } + private static void writeScalarValue(final ScalarValue value, final XMLStreamWriter writer) throws XMLStreamException { - writer.writeStartElement(XSD_NAMESPACE, "enumeration"); - writer.writeAttribute("value", value.getName()); - - writeDocumentation(value.getDescription(), writer); - - writer.writeEndElement(); + final Description description = value.getDescription(); + if (description != null) { + writer.writeStartElement(XSD_NAMESPACE, "enumeration"); + writer.writeAttribute("value", value.getName()); + writeDocumentation(value.getDescription(), writer); + writer.writeEndElement(); + } else { + writer.writeEmptyElement(XSD_NAMESPACE, "enumeration"); + writer.writeAttribute("value", value.getName()); + } } private static void writePluginElement( @@ -303,25 +392,28 @@ private static void writePluginElement( private static void writePluginAttribute( final TypeLookup lookup, final PluginAttribute attribute, final XMLStreamWriter writer) throws XMLStreamException { - @Nullable final String xmlType = getXmlType(lookup, attribute.getType()); - if (xmlType == null) { - return; + final String xmlType = getXmlType(lookup, attribute.getType()); + final Description description = attribute.getDescription(); + if (description != null) { + writer.writeStartElement(XSD_NAMESPACE, "attribute"); + } else { + writer.writeEmptyElement(XSD_NAMESPACE, "attribute"); } - writer.writeStartElement(XSD_NAMESPACE, "attribute"); writer.writeAttribute("name", attribute.getName()); - writer.writeAttribute("type", xmlType); - final Description description = attribute.getDescription(); + // If the type is unknown, use `string` + writer.writeAttribute("type", xmlType != null ? xmlType : "string"); if (description != null) { writeDocumentation(description, writer); + writer.writeEndElement(); } - writer.writeEndElement(); } @Nullable private static String getXmlType(final TypeLookup lookup, final String className) { final String builtinType = XML_BUILTIN_TYPES.get(className); if (builtinType != null) { - return builtinType; + // Use the union types for all built-in types, except `string`. + return STRING_TYPE.equals(builtinType) ? STRING_TYPE : LOG4J_PREFIX + ":" + builtinType; } final ArtifactSourcedType type = lookup.get(className); return type != null ? LOG4J_PREFIX + ":" + className : null; diff --git a/log4j-docgen/src/test/resources/SchemaGeneratorTest/expected-plugins.xsd b/log4j-docgen/src/test/resources/SchemaGeneratorTest/expected-plugins.xsd index 4cd4cb34..ec9f7888 100644 --- a/log4j-docgen/src/test/resources/SchemaGeneratorTest/expected-plugins.xsd +++ b/log4j-docgen/src/test/resources/SchemaGeneratorTest/expected-plugins.xsd @@ -22,55 +22,138 @@ - + - Represents a logging level. -NOTE: The Log4j API supports custom levels, the following list contains only the standard ones. + A string with a property substitution expression. - - - Special level that disables logging. -No events should be logged at this level. - - - - - A fatal event that will prevent the application from continuing - - - - - An error in the application, possibly recoverable - - - - - An event that might possible lead to an error - - - - - An event for informational purposes - - - - - A general debugging event - - - - - A fine-grained debug message, typically capturing the flow through the application - - - - - Special level indicating all events should be logged - - + + + + A custom boolean type that allows `true`, `false`, or a property substitution expression. + + + + + + + + + + + + + + Union of `xsd:anyURI` and ` property-substitution`. + + + + + + Union of `xsd:byte` and ` property-substitution`. + + + + + + Union of `xsd:decimal` and ` property-substitution`. + + + + + + Union of `xsd:double` and ` property-substitution`. + + + + + + Union of `xsd:float` and ` property-substitution`. + + + + + + Union of `xsd:int` and ` property-substitution`. + + + + + + Union of `xsd:integer` and ` property-substitution`. + + + + + + Union of `xsd:long` and ` property-substitution`. + + + + + + Union of `xsd:short` and ` property-substitution`. + + + + + + Represents a logging level. +NOTE: The Log4j API supports custom levels, the following list contains only the standard ones. + + + + + + + + Special level that disables logging. +No events should be logged at this level. + + + + + + A fatal event that will prevent the application from continuing + + + + + An error in the application, possibly recoverable + + + + + An event that might possible lead to an error + + + + + An event for informational purposes + + + + + A general debugging event + + + + + A fine-grained debug message, typically capturing the flow through the application + + + + + + Special level indicating all events should be logged + + + + + + Appends log events. @@ -104,23 +187,28 @@ It is recommended that, where possible, `Filter` implementations create a generi The result that can returned from a filter method call. - - - - The event will be processed without further filtering based on the log Level. - - - - - No decision could be made, further filtering should occur. - - - - - The event should not be processed. - - - + + + + + + The event will be processed without further filtering based on the log Level. + + + + + + No decision could be made, further filtering should occur. + + + + + The event should not be processed. + + + + + @@ -152,23 +240,23 @@ It is recommended that, where possible, `Filter` implementations create a generi Must be unique. - + If set to `false` logging exceptions will be forwarded to the caller. - + If set to `true` (default) the appender will buffer messages before sending them. This attribute is ignored if `immediateFlush` is set to `true`. - + Size in bytes of the appender's buffer. - + If set to `true`, the appender flushes the output stream at each message and buffering is disabled regardless of the value of `bufferedIo`. @@ -184,18 +272,22 @@ buffering is disabled regardless of the value of `bufferedIo`. Specifies the target of a console appender. - - - - Logs to the standard output. - - - - - Logs to the standard error. - - - + + + + + + Logs to the standard output. + + + + + Logs to the standard error. + + + + + @@ -283,7 +375,7 @@ If the provided value is invalid, then the default destination of standard out w Name of the configuration - + Number of seconds between polls for configuration changes @@ -300,7 +392,7 @@ Possible values are `enable` and `disable`. The shutdown hook is enabled by default, unless Log4j detects the presence of the Servlet API. - + Timeout in milliseconds of the logger context shut down @@ -310,7 +402,7 @@ The shutdown hook is enabled by default, unless Log4j detects the presence of th Sets the level of the status logger - + If set to `true` the configuration file will be validated using an XML schema. @@ -325,7 +417,7 @@ The shutdown hook is enabled by default, unless Log4j detects the presence of th The name of the level. - + An integer determines the strength of the custom level relative to the built-in levels. @@ -369,7 +461,7 @@ The shutdown hook is enabled by default, unless Log4j detects the presence of th The level of the logger. - + When set to `false` location information will **not** be computed. @@ -407,7 +499,7 @@ The default value depends on the logger context implementation: it is `false` fo The level of the logger. - + When set to `false` location information will **not** be computed. @@ -474,12 +566,12 @@ Use this filter when you want to control the mean rate and maximum burst of log Log events less specific than this level are filtered, while events with level equal or more specific always match. - + Sets the average number of events per second to allow. - + Sets the maximum number of events that can occur before events are filtered for exceeding the average rate. @@ -505,24 +597,24 @@ A conversion pattern is composed of literal text and format control expressions Dummy plugin to test all types of builtin XML attributes. - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + \ No newline at end of file diff --git a/src/changelog/.0.x.x/136_union-types.xml b/src/changelog/.0.x.x/136_union-types.xml new file mode 100644 index 00000000..a9da4fff --- /dev/null +++ b/src/changelog/.0.x.x/136_union-types.xml @@ -0,0 +1,8 @@ + + + + Add support for property substitution expressions in XML attributes. +