Skip to content

Commit 8ca8984

Browse files
committed
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
1 parent c2af617 commit 8ca8984

File tree

3 files changed

+314
-122
lines changed

3 files changed

+314
-122
lines changed

log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/generator/SchemaGenerator.java

Lines changed: 111 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.List;
3333
import java.util.Map;
3434
import java.util.Set;
35+
import java.util.TreeSet;
3536
import java.util.stream.Collectors;
3637
import java.util.stream.Stream;
3738
import javax.inject.Named;
@@ -67,6 +68,24 @@ public final class SchemaGenerator {
6768

6869
private static final String CHARSET_NAME = "UTF-8";
6970

71+
private static final String PROPERTY_SUBSTITUTION_TYPE = "property-substitution";
72+
private static final String BOOLEAN_TYPE = "boolean";
73+
private static final String STRING_TYPE = "string";
74+
private static final ScalarType BOOLEAN_SCALAR_TYPE = new ScalarType();
75+
76+
static {
77+
BOOLEAN_SCALAR_TYPE.setClassName(BOOLEAN_TYPE);
78+
final Description description = new Description();
79+
description.setText(
80+
"A custom boolean type that allows `true`, `false`, or a property substitution expression.");
81+
BOOLEAN_SCALAR_TYPE.setDescription(description);
82+
for (final Boolean value : new Boolean[] {true, false}) {
83+
final ScalarValue scalarValue = new ScalarValue();
84+
scalarValue.setName(value.toString());
85+
BOOLEAN_SCALAR_TYPE.addValue(scalarValue);
86+
}
87+
}
88+
7089
private static final Map<String, String> XML_BUILTIN_TYPES = Map.ofEntries(
7190
entry(BigDecimal.class.getName(), "decimal"),
7291
entry(BigInteger.class.getName(), "integer"),
@@ -138,6 +157,12 @@ private static void writeSchema(final String version, final TypeLookup lookup, f
138157
}
139158

140159
private static void writeTypes(final TypeLookup lookup, final XMLStreamWriter writer) throws XMLStreamException {
160+
writePropertySubstitutionType(writer);
161+
// A union with member types `xsd:boolean` and `log4j:property-substitution` does not allow auto-completion
162+
// in IDEs. This is why we define a `log4j:boolean` type from scratch.
163+
writeScalarType(BOOLEAN_SCALAR_TYPE, writer);
164+
writeUnionBuiltinTypes(writer);
165+
141166
for (final ArtifactSourcedType sourcedType : lookup.values()) {
142167
final Type type = sourcedType.type;
143168
if (isBuiltinXmlType(type.getClassName())) {
@@ -167,12 +192,66 @@ private static boolean isBuiltinXmlType(final String className) {
167192
return XML_BUILTIN_TYPES.containsKey(className);
168193
}
169194

195+
/**
196+
* A restriction of {@code string} that requires at least one property substitution expression {@code ${...}}.
197+
*/
198+
private static void writePropertySubstitutionType(final XMLStreamWriter writer) throws XMLStreamException {
199+
writer.writeStartElement(XSD_NAMESPACE, "simpleType");
200+
writer.writeAttribute("name", PROPERTY_SUBSTITUTION_TYPE);
201+
202+
writeDocumentation("A string with a property substitution expression.", writer);
203+
204+
writer.writeStartElement(XSD_NAMESPACE, "restriction");
205+
writer.writeAttribute("base", "string");
206+
207+
writer.writeEmptyElement(XSD_NAMESPACE, "pattern");
208+
writer.writeAttribute("value", ".*\\$\\{.*\\}.*");
209+
210+
writer.writeEndElement();
211+
writer.writeEndElement();
212+
}
213+
214+
/**
215+
* Define types that are the union of a builtin type and {@value PROPERTY_SUBSTITUTION_TYPE}.
216+
* <p>
217+
* IDEs don't propose auto-completion for these types.
218+
* </p>
219+
*/
220+
private static void writeUnionBuiltinTypes(final XMLStreamWriter writer) throws XMLStreamException {
221+
final Collection<String> types = new TreeSet<>(XML_BUILTIN_TYPES.values());
222+
// `xsd:string` is a superset of PROPERTY_SUBSTITUTION_TYPE, so no union is needed.
223+
types.remove(STRING_TYPE);
224+
// The union of `xsd:boolean` with PROPERTY_SUBSTITUTION_TYPE does not show auto-completion in IDEs.
225+
// `log4j:boolean` will be generated from an _ad-hoc_ ScalarType definition in `base-log4j-types.xml`.
226+
types.remove(BOOLEAN_TYPE);
227+
for (final String type : types) {
228+
writeUnionBuiltinType(type, writer);
229+
}
230+
}
231+
232+
private static void writeUnionBuiltinType(final String type, final XMLStreamWriter writer)
233+
throws XMLStreamException {
234+
writer.writeStartElement(XSD_NAMESPACE, "simpleType");
235+
writer.writeAttribute("name", type);
236+
237+
writeDocumentation("Union of `xsd:" + type + "` and ` " + PROPERTY_SUBSTITUTION_TYPE + "`.", writer);
238+
239+
writer.writeEmptyElement(XSD_NAMESPACE, "union");
240+
writer.writeAttribute("memberTypes", type + " log4j:" + PROPERTY_SUBSTITUTION_TYPE);
241+
242+
writer.writeEndElement();
243+
}
244+
170245
private static void writeScalarType(final ScalarType type, final XMLStreamWriter writer) throws XMLStreamException {
171246
writer.writeStartElement(XSD_NAMESPACE, "simpleType");
172247
writer.writeAttribute("name", type.getClassName());
173248

174249
writeDocumentation(type.getDescription(), writer);
175250

251+
writer.writeStartElement(XSD_NAMESPACE, "union");
252+
writer.writeAttribute("memberTypes", "log4j:" + PROPERTY_SUBSTITUTION_TYPE);
253+
writer.writeStartElement(XSD_NAMESPACE, "simpleType");
254+
176255
writer.writeStartElement(XSD_NAMESPACE, "restriction");
177256
writer.writeAttribute("base", "string");
178257

@@ -182,6 +261,8 @@ private static void writeScalarType(final ScalarType type, final XMLStreamWriter
182261

183262
writer.writeEndElement();
184263
writer.writeEndElement();
264+
writer.writeEndElement();
265+
writer.writeEndElement();
185266
}
186267

187268
private static void writePluginType(
@@ -240,22 +321,30 @@ private static void writeAbstractType(
240321
private static void writeDocumentation(@Nullable final Description description, final XMLStreamWriter writer)
241322
throws XMLStreamException {
242323
if (description != null) {
243-
writer.writeStartElement(XSD_NAMESPACE, "annotation");
244-
writer.writeStartElement(XSD_NAMESPACE, "documentation");
245-
writer.writeCharacters(description.getText());
246-
writer.writeEndElement();
247-
writer.writeEndElement();
324+
writeDocumentation(description.getText(), writer);
248325
}
249326
}
250327

328+
private static void writeDocumentation(final String text, final XMLStreamWriter writer) throws XMLStreamException {
329+
writer.writeStartElement(XSD_NAMESPACE, "annotation");
330+
writer.writeStartElement(XSD_NAMESPACE, "documentation");
331+
writer.writeCharacters(text);
332+
writer.writeEndElement();
333+
writer.writeEndElement();
334+
}
335+
251336
private static void writeScalarValue(final ScalarValue value, final XMLStreamWriter writer)
252337
throws XMLStreamException {
253-
writer.writeStartElement(XSD_NAMESPACE, "enumeration");
254-
writer.writeAttribute("value", value.getName());
255-
256-
writeDocumentation(value.getDescription(), writer);
257-
258-
writer.writeEndElement();
338+
final Description description = value.getDescription();
339+
if (description != null) {
340+
writer.writeStartElement(XSD_NAMESPACE, "enumeration");
341+
writer.writeAttribute("value", value.getName());
342+
writeDocumentation(value.getDescription(), writer);
343+
writer.writeEndElement();
344+
} else {
345+
writer.writeEmptyElement(XSD_NAMESPACE, "enumeration");
346+
writer.writeAttribute("value", value.getName());
347+
}
259348
}
260349

261350
private static void writePluginElement(
@@ -303,25 +392,28 @@ private static void writePluginElement(
303392
private static void writePluginAttribute(
304393
final TypeLookup lookup, final PluginAttribute attribute, final XMLStreamWriter writer)
305394
throws XMLStreamException {
306-
@Nullable final String xmlType = getXmlType(lookup, attribute.getType());
307-
if (xmlType == null) {
308-
return;
395+
final String xmlType = getXmlType(lookup, attribute.getType());
396+
final Description description = attribute.getDescription();
397+
if (description != null) {
398+
writer.writeStartElement(XSD_NAMESPACE, "attribute");
399+
} else {
400+
writer.writeEmptyElement(XSD_NAMESPACE, "attribute");
309401
}
310-
writer.writeStartElement(XSD_NAMESPACE, "attribute");
311402
writer.writeAttribute("name", attribute.getName());
312-
writer.writeAttribute("type", xmlType);
313-
final Description description = attribute.getDescription();
403+
// If the type is unknown, use `string`
404+
writer.writeAttribute("type", xmlType != null ? xmlType : "string");
314405
if (description != null) {
315406
writeDocumentation(description, writer);
407+
writer.writeEndElement();
316408
}
317-
writer.writeEndElement();
318409
}
319410

320411
@Nullable
321412
private static String getXmlType(final TypeLookup lookup, final String className) {
322413
final String builtinType = XML_BUILTIN_TYPES.get(className);
323414
if (builtinType != null) {
324-
return builtinType;
415+
// Use the union types for all built-in types, except `string`.
416+
return STRING_TYPE.equals(builtinType) ? STRING_TYPE : LOG4J_PREFIX + ":" + builtinType;
325417
}
326418
final ArtifactSourcedType type = lookup.get(className);
327419
return type != null ? LOG4J_PREFIX + ":" + className : null;

0 commit comments

Comments
 (0)