3232import java .util .List ;
3333import java .util .Map ;
3434import java .util .Set ;
35+ import java .util .TreeSet ;
3536import java .util .stream .Collectors ;
3637import java .util .stream .Stream ;
3738import 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