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
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.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 @@
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 @@
}
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 @@
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 @@
writer.writeEndElement();
writer.writeEndElement();
+ writer.writeEndElement();
+ writer.writeEndElement();
}
private static void writePluginType(
@@ -240,22 +321,30 @@
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 writePluginAttribute(
final TypeLookup lookup, final PluginAttribute attribute, final XMLStreamWriter writer)
throws XMLStreamException {
- @Nullable final String xmlType = getXmlType(lookup, attribute.getType());
- if (xmlType == null) {
- return;
- }
- writer.writeStartElement(XSD_NAMESPACE, "attribute");
- writer.writeAttribute("name", attribute.getName());
- writer.writeAttribute("type", xmlType);
+ final String xmlType = getXmlType(lookup, attribute.getType());
final Description description = attribute.getDescription();
if (description != null) {
- writeDocumentation(description, writer);
+ writer.writeStartElement(XSD_NAMESPACE, "attribute");
+ } else {
+ writer.writeEmptyElement(XSD_NAMESPACE, "attribute");
}
- writer.writeEndElement();
+ writer.writeAttribute("name", attribute.getName());
+ // If the type is unknown, use `string`
+ writer.writeAttribute("type", xmlType != null ? xmlType : "string");
+ if (description != null) {
+ writeDocumentation(description, writer);
+ 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,54 +22,137 @@
<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="property-substitution">
+ <annotation>
+ <documentation>A string with a property substitution expression.</documentation>
+ </annotation>
+ <restriction base="string">
+ <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>
+NOTE: The Log4j API supports custom levels, the following list contains only the standard ones.
+ </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>
- </restriction>
+ <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>
@@ -104,23 +187,28 @@
<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 @@
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 @@
<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 @@
<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 @@
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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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 @@
<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>