This is an automated email from the ASF dual-hosted git repository. pkarwasz pushed a commit to branch feat/union-types in repository https://gitbox.apache.org/repos/asf/logging-log4j-tools.git
commit 8ca898466970d1d01d4aab57dc521c78c5059d97 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Fri May 30 13:01:35 2025 +0200 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 --- .../log4j/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(-) 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 707a606..c947be7 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.Collection; 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<String, String> XML_BUILTIN_TYPES = Map.ofEntries( entry(BigDecimal.class.getName(), "decimal"), entry(BigInteger.class.getName(), "integer"), @@ -138,6 +157,12 @@ public final class SchemaGenerator { } 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 @@ public final class SchemaGenerator { 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}. + * <p> + * IDEs don't propose auto-completion for these types. + * </p> + */ + private static void writeUnionBuiltinTypes(final XMLStreamWriter writer) throws XMLStreamException { + final Collection<String> 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 @@ public final class SchemaGenerator { writer.writeEndElement(); writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndElement(); } private static void writePluginType( @@ -240,22 +321,30 @@ public final class SchemaGenerator { 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 @@ public final class SchemaGenerator { 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 4cd4cb3..ec9f788 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 @@ <schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:log4j="https://logging.apache.org/xml/ns" elementFormDefault="qualified" targetNamespace="https://logging.apache.org/xml/ns" version="1.2.3"> <element type="log4j:org.apache.logging.log4j.core.config.Configuration" name="Configuration"/> - <simpleType name="org.apache.logging.log4j.Level"> + <simpleType name="property-substitution"> <annotation> - <documentation>Represents a logging level. -NOTE: The Log4j API supports custom levels, the following list contains only the standard ones.</documentation> + <documentation>A string with a property substitution expression.</documentation> </annotation> <restriction base="string"> - <enumeration value="OFF"> - <annotation> - <documentation>Special level that disables logging. -No events should be logged at this level.</documentation> - </annotation> - </enumeration> - <enumeration value="FATAL"> - <annotation> - <documentation>A fatal event that will prevent the application from continuing</documentation> - </annotation> - </enumeration> - <enumeration value="ERROR"> - <annotation> - <documentation>An error in the application, possibly recoverable</documentation> - </annotation> - </enumeration> - <enumeration value="WARN"> - <annotation> - <documentation>An event that might possible lead to an error</documentation> - </annotation> - </enumeration> - <enumeration value="INFO"> - <annotation> - <documentation>An event for informational purposes</documentation> - </annotation> - </enumeration> - <enumeration value="DEBUG"> - <annotation> - <documentation>A general debugging event</documentation> - </annotation> - </enumeration> - <enumeration value="TRACE"> - <annotation> - <documentation>A fine-grained debug message, typically capturing the flow through the application</documentation> - </annotation> - </enumeration> - <enumeration value="ALL"> - <annotation> - <documentation>Special level indicating all events should be logged</documentation> - </annotation> - </enumeration> + <pattern value=".*\$\{.*\}.*"/> </restriction> </simpleType> + <simpleType name="boolean"> + <annotation> + <documentation>A custom boolean type that allows `true`, `false`, or a property substitution expression. + </documentation> + </annotation> + <union memberTypes="log4j:property-substitution"> + <simpleType> + <restriction base="string"> + <enumeration value="true"/> + <enumeration value="false"/> + </restriction> + </simpleType> + </union> + </simpleType> + <simpleType name="anyURI"> + <annotation> + <documentation>Union of `xsd:anyURI` and ` property-substitution`.</documentation> + </annotation> + <union memberTypes="anyURI log4j:property-substitution"/> + </simpleType> + <simpleType name="byte"> + <annotation> + <documentation>Union of `xsd:byte` and ` property-substitution`.</documentation> + </annotation> + <union memberTypes="byte log4j:property-substitution"/> + </simpleType> + <simpleType name="decimal"> + <annotation> + <documentation>Union of `xsd:decimal` and ` property-substitution`.</documentation> + </annotation> + <union memberTypes="decimal log4j:property-substitution"/> + </simpleType> + <simpleType name="double"> + <annotation> + <documentation>Union of `xsd:double` and ` property-substitution`.</documentation> + </annotation> + <union memberTypes="double log4j:property-substitution"/> + </simpleType> + <simpleType name="float"> + <annotation> + <documentation>Union of `xsd:float` and ` property-substitution`.</documentation> + </annotation> + <union memberTypes="float log4j:property-substitution"/> + </simpleType> + <simpleType name="int"> + <annotation> + <documentation>Union of `xsd:int` and ` property-substitution`.</documentation> + </annotation> + <union memberTypes="int log4j:property-substitution"/> + </simpleType> + <simpleType name="integer"> + <annotation> + <documentation>Union of `xsd:integer` and ` property-substitution`.</documentation> + </annotation> + <union memberTypes="integer log4j:property-substitution"/> + </simpleType> + <simpleType name="long"> + <annotation> + <documentation>Union of `xsd:long` and ` property-substitution`.</documentation> + </annotation> + <union memberTypes="long log4j:property-substitution"/> + </simpleType> + <simpleType name="short"> + <annotation> + <documentation>Union of `xsd:short` and ` property-substitution`.</documentation> + </annotation> + <union memberTypes="short log4j:property-substitution"/> + </simpleType> + <simpleType name="org.apache.logging.log4j.Level"> + <annotation> + <documentation>Represents a logging level. +NOTE: The Log4j API supports custom levels, the following list contains only the standard ones. + </documentation> + </annotation> + <union memberTypes="log4j:property-substitution"> + <simpleType> + <restriction base="string"> + <enumeration value="OFF"> + <annotation> + <documentation>Special level that disables logging. +No events should be logged at this level. + </documentation> + </annotation> + </enumeration> + <enumeration value="FATAL"> + <annotation> + <documentation>A fatal event that will prevent the application from continuing</documentation> + </annotation> + </enumeration> + <enumeration value="ERROR"> + <annotation> + <documentation>An error in the application, possibly recoverable</documentation> + </annotation> + </enumeration> + <enumeration value="WARN"> + <annotation> + <documentation>An event that might possible lead to an error</documentation> + </annotation> + </enumeration> + <enumeration value="INFO"> + <annotation> + <documentation>An event for informational purposes</documentation> + </annotation> + </enumeration> + <enumeration value="DEBUG"> + <annotation> + <documentation>A general debugging event</documentation> + </annotation> + </enumeration> + <enumeration value="TRACE"> + <annotation> + <documentation>A fine-grained debug message, typically capturing the flow through the application + </documentation> + </annotation> + </enumeration> + <enumeration value="ALL"> + <annotation> + <documentation>Special level indicating all events should be logged</documentation> + </annotation> + </enumeration> + </restriction> + </simpleType> + </union> + </simpleType> <group name="org.apache.logging.log4j.core.Appender"> <annotation> <documentation>Appends log events. @@ -104,23 +187,28 @@ It is recommended that, where possible, `Filter` implementations create a generi <annotation> <documentation>The result that can returned from a filter method call.</documentation> </annotation> - <restriction base="string"> - <enumeration value="ACCEPT"> - <annotation> - <documentation>The event will be processed without further filtering based on the log Level.</documentation> - </annotation> - </enumeration> - <enumeration value="NEUTRAL"> - <annotation> - <documentation>No decision could be made, further filtering should occur.</documentation> - </annotation> - </enumeration> - <enumeration value="DENY"> - <annotation> - <documentation>The event should not be processed.</documentation> - </annotation> - </enumeration> - </restriction> + <union memberTypes="log4j:property-substitution"> + <simpleType> + <restriction base="string"> + <enumeration value="ACCEPT"> + <annotation> + <documentation>The event will be processed without further filtering based on the log Level. + </documentation> + </annotation> + </enumeration> + <enumeration value="NEUTRAL"> + <annotation> + <documentation>No decision could be made, further filtering should occur.</documentation> + </annotation> + </enumeration> + <enumeration value="DENY"> + <annotation> + <documentation>The event should not be processed.</documentation> + </annotation> + </enumeration> + </restriction> + </simpleType> + </union> </simpleType> <group name="org.apache.logging.log4j.core.Layout"> <annotation> @@ -152,23 +240,23 @@ It is recommended that, where possible, `Filter` implementations create a generi Must be unique.</documentation> </annotation> </attribute> - <attribute name="ignoreExceptions" type="boolean"> + <attribute name="ignoreExceptions" type="log4j:boolean"> <annotation> <documentation>If set to `false` logging exceptions will be forwarded to the caller.</documentation> </annotation> </attribute> - <attribute name="bufferedIo" type="boolean"> + <attribute name="bufferedIo" type="log4j:boolean"> <annotation> <documentation>If set to `true` (default) the appender will buffer messages before sending them. This attribute is ignored if `immediateFlush` is set to `true`.</documentation> </annotation> </attribute> - <attribute name="bufferSize" type="int"> + <attribute name="bufferSize" type="log4j:int"> <annotation> <documentation>Size in bytes of the appender's buffer.</documentation> </annotation> </attribute> - <attribute name="immediateFlush" type="boolean"> + <attribute name="immediateFlush" type="log4j:boolean"> <annotation> <documentation>If set to `true`, the appender flushes the output stream at each message and buffering is disabled regardless of the value of `bufferedIo`.</documentation> @@ -184,18 +272,22 @@ buffering is disabled regardless of the value of `bufferedIo`.</documentation> <annotation> <documentation>Specifies the target of a console appender.</documentation> </annotation> - <restriction base="string"> - <enumeration value="SYSTEM_OUT"> - <annotation> - <documentation>Logs to the standard output.</documentation> - </annotation> - </enumeration> - <enumeration value="SYSTEM_ERR"> - <annotation> - <documentation>Logs to the standard error.</documentation> - </annotation> - </enumeration> - </restriction> + <union memberTypes="log4j:property-substitution"> + <simpleType> + <restriction base="string"> + <enumeration value="SYSTEM_OUT"> + <annotation> + <documentation>Logs to the standard output.</documentation> + </annotation> + </enumeration> + <enumeration value="SYSTEM_ERR"> + <annotation> + <documentation>Logs to the standard error.</documentation> + </annotation> + </enumeration> + </restriction> + </simpleType> + </union> </simpleType> <complexType name="org.apache.logging.log4j.core.config.AppenderRef"> <annotation> @@ -283,7 +375,7 @@ If the provided value is invalid, then the default destination of standard out w <documentation>Name of the configuration</documentation> </annotation> </attribute> - <attribute name="monitorInterval" type="int"> + <attribute name="monitorInterval" type="log4j:int"> <annotation> <documentation>Number of seconds between polls for configuration changes</documentation> </annotation> @@ -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.</documentation> </annotation> </attribute> - <attribute name="shutdownTimeout" type="int"> + <attribute name="shutdownTimeout" type="log4j:int"> <annotation> <documentation>Timeout in milliseconds of the logger context shut down</documentation> </annotation> @@ -310,7 +402,7 @@ The shutdown hook is enabled by default, unless Log4j detects the presence of th <documentation>Sets the level of the status logger</documentation> </annotation> </attribute> - <attribute name="strict" type="boolean"> + <attribute name="strict" type="log4j:boolean"> <annotation> <documentation>If set to `true` the configuration file will be validated using an XML schema.</documentation> </annotation> @@ -325,7 +417,7 @@ The shutdown hook is enabled by default, unless Log4j detects the presence of th <documentation>The name of the level.</documentation> </annotation> </attribute> - <attribute name="intLevel" type="int"> + <attribute name="intLevel" type="log4j:int"> <annotation> <documentation>An integer determines the strength of the custom level relative to the built-in levels.</documentation> </annotation> @@ -369,7 +461,7 @@ The shutdown hook is enabled by default, unless Log4j detects the presence of th <documentation>The level of the logger.</documentation> </annotation> </attribute> - <attribute name="includeLocation" type="boolean"> + <attribute name="includeLocation" type="log4j:boolean"> <annotation> <documentation>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 <documentation>The level of the logger.</documentation> </annotation> </attribute> - <attribute name="includeLocation" type="boolean"> + <attribute name="includeLocation" type="log4j:boolean"> <annotation> <documentation>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 <documentation>Log events less specific than this level are filtered, while events with level equal or more specific always match.</documentation> </annotation> </attribute> - <attribute name="rate" type="float"> + <attribute name="rate" type="log4j:float"> <annotation> <documentation>Sets the average number of events per second to allow.</documentation> </annotation> </attribute> - <attribute name="maxBurst" type="long"> + <attribute name="maxBurst" type="log4j:long"> <annotation> <documentation>Sets the maximum number of events that can occur before events are filtered for exceeding the average rate.</documentation> </annotation> @@ -505,24 +597,24 @@ A conversion pattern is composed of literal text and format control expressions <annotation> <documentation>Dummy plugin to test all types of builtin XML attributes.</documentation> </annotation> - <attribute name="BigInteger" type="integer"/> - <attribute name="BigDecimal" type="decimal"/> - <attribute name="boolean" type="boolean"/> - <attribute name="Boolean" type="boolean"/> - <attribute name="byte" type="byte"/> - <attribute name="Byte" type="byte"/> - <attribute name="double" type="double"/> - <attribute name="Double" type="double"/> - <attribute name="float" type="float"/> - <attribute name="Float" type="float"/> - <attribute name="int" type="int"/> - <attribute name="Integer" type="int"/> - <attribute name="long" type="long"/> - <attribute name="Long" type="long"/> - <attribute name="short" type="short"/> - <attribute name="Short" type="short"/> + <attribute name="BigInteger" type="log4j:integer"/> + <attribute name="BigDecimal" type="log4j:decimal"/> + <attribute name="boolean" type="log4j:boolean"/> + <attribute name="Boolean" type="log4j:boolean"/> + <attribute name="byte" type="log4j:byte"/> + <attribute name="Byte" type="log4j:byte"/> + <attribute name="double" type="log4j:double"/> + <attribute name="Double" type="log4j:double"/> + <attribute name="float" type="log4j:float"/> + <attribute name="Float" type="log4j:float"/> + <attribute name="int" type="log4j:int"/> + <attribute name="Integer" type="log4j:int"/> + <attribute name="long" type="log4j:long"/> + <attribute name="Long" type="log4j:long"/> + <attribute name="short" type="log4j:short"/> + <attribute name="Short" type="log4j:short"/> <attribute name="String" type="string"/> - <attribute name="URI" type="anyURI"/> - <attribute name="URL" type="anyURI"/> + <attribute name="URI" type="log4j:anyURI"/> + <attribute name="URL" type="log4j:anyURI"/> </complexType> </schema> \ 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 0000000..a9da4ff --- /dev/null +++ b/src/changelog/.0.x.x/136_union-types.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="https://logging.apache.org/xml/ns" + xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd" + type="added"> + <issue id="136" link="https://github.com/apache/logging-log4j-tools/issues/136"/> + <description format="asciidoc">Add support for property substitution expressions in XML attributes.</description> +</entry>
