diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/internal/DefaultSchemaGenerator.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/internal/DefaultSchemaGenerator.java new file mode 100644 index 00000000..0ea942fc --- /dev/null +++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/internal/DefaultSchemaGenerator.java @@ -0,0 +1,305 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.docgen.internal; + +import java.io.IOException; +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.Set; +import java.util.TreeSet; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.xml.XMLConstants; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.apache.logging.log4j.docgen.AbstractType; +import org.apache.logging.log4j.docgen.Description; +import org.apache.logging.log4j.docgen.PluginAttribute; +import org.apache.logging.log4j.docgen.PluginElement; +import org.apache.logging.log4j.docgen.PluginSet; +import org.apache.logging.log4j.docgen.PluginType; +import org.apache.logging.log4j.docgen.ScalarType; +import org.apache.logging.log4j.docgen.ScalarValue; +import org.apache.logging.log4j.docgen.Type; +import org.apache.logging.log4j.docgen.io.stax.PluginBundleStaxReader; +import org.apache.logging.log4j.docgen.util.TypeLookup; +import org.apache.logging.log4j.docgen.xsd.SchemaGenerator; +import org.apache.logging.log4j.docgen.xsd.SchemaGeneratorRequest; + +@Singleton +@Named("default") +public class DefaultSchemaGenerator implements SchemaGenerator { + + private static final String LOG4J_PREFIX = "log4j"; + private static final String LOG4J_NAMESPACE = "http://logging.apache.org/log4j/2.0/config"; + private static final String XSD_NAMESPACE = XMLConstants.W3C_XML_SCHEMA_NS_URI; + private static final String MULTIPLICITY_UNBOUNDED = "*"; + + @Override + public void generateSchema(final SchemaGeneratorRequest request) throws XMLStreamException { + try { + final PluginSet configurationSet = + new PluginBundleStaxReader().read(getClass().getResourceAsStream("configuration.xml")); + final List extendedSets = new ArrayList<>(request.getPluginSets()); + extendedSets.add(configurationSet); + final TypeLookup lookup = TypeLookup.of(extendedSets); + final XMLOutputFactory factory = XMLOutputFactory.newFactory(); + final Path schemaPath = request.getOutputDirectory().resolve(request.getFileName()); + final XMLStreamWriter writer = factory.createXMLStreamWriter(Files.newOutputStream(schemaPath)); + try { + writeSchema(lookup, writer); + } finally { + writer.close(); + } + } catch (IOException e) { + throw new XMLStreamException(e); + } + } + + private static void writeSchema(final TypeLookup lookup, final XMLStreamWriter writer) throws XMLStreamException { + writer.writeStartDocument("UTF-8", "1.0"); + writer.setDefaultNamespace(XSD_NAMESPACE); + writer.writeStartElement(XSD_NAMESPACE, "schema"); + writer.writeDefaultNamespace(XSD_NAMESPACE); + writer.writeNamespace(LOG4J_PREFIX, LOG4J_NAMESPACE); + writer.writeAttribute("elementFormDefault", "qualified"); + writer.writeAttribute("targetNamespace", LOG4J_NAMESPACE); + + // The root element + writer.writeEmptyElement(XSD_NAMESPACE, "element"); + writer.writeAttribute("type", LOG4J_PREFIX + ":org.apache.logging.log4j.core.config.Configuration"); + writer.writeAttribute("name", "Configuration"); + + // Write all types in alphabetical order + writeTypes(lookup, writer); + + writer.writeEndElement(); + writer.writeEndDocument(); + } + + private static void writeTypes(final TypeLookup lookup, final XMLStreamWriter writer) throws XMLStreamException { + for (final Type type : lookup.values()) { + if (isBuiltinXmlType(type.getClassName())) { + continue; + } + if (type instanceof ScalarType) { + writeScalarType((ScalarType) type, writer); + } + if (type instanceof PluginType) { + final PluginType pluginType = (PluginType) type; + writePluginType(lookup, pluginType, writer); + /* + * If a plugin extends another plugin or has multiple aliases + * we also need a element. + */ + if (!pluginType.getAliases().isEmpty() + || !pluginType.getImplementations().isEmpty()) { + writeAbstractType(lookup, pluginType, writer); + } + } else if (type instanceof AbstractType) { + writeAbstractType(lookup, (AbstractType) type, writer); + } + } + } + + 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; + } + } + + 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, "restriction"); + writer.writeAttribute("base", "string"); + + for (final ScalarValue value : type.getValues()) { + writeScalarValue(value, writer); + } + + writer.writeEndElement(); + writer.writeEndElement(); + } + + private static void writePluginType( + final TypeLookup lookup, final PluginType pluginType, final XMLStreamWriter writer) + throws XMLStreamException { + writer.writeStartElement(XSD_NAMESPACE, "complexType"); + writer.writeAttribute("name", pluginType.getClassName()); + + writeDocumentation(pluginType.getDescription(), writer); + + final boolean hasComplexContent = !pluginType.getElements().isEmpty(); + + if (hasComplexContent) { + writer.writeStartElement(XSD_NAMESPACE, "sequence"); + for (final PluginElement element : pluginType.getElements()) { + writePluginElement(lookup, element, writer); + } + writer.writeEndElement(); + } + + for (final PluginAttribute attribute : pluginType.getAttributes()) { + writePluginAttribute(lookup, attribute, writer); + } + + writer.writeEndElement(); + } + + private static void writeAbstractType( + final TypeLookup lookup, final AbstractType abstractType, final XMLStreamWriter writer) + throws XMLStreamException { + writer.writeStartElement(XSD_NAMESPACE, "group"); + writer.writeAttribute("name", abstractType.getClassName()); + final Description description = abstractType.getDescription(); + if (description != null) { + writeDocumentation(description, writer); + } + writer.writeStartElement(XSD_NAMESPACE, "choice"); + + final Set implementations = new TreeSet<>(abstractType.getImplementations()); + if (abstractType instanceof PluginType) { + implementations.add(abstractType.getClassName()); + } + for (final String implementation : implementations) { + final PluginType pluginType = (PluginType) lookup.get(implementation); + final Collection keys = new TreeSet<>(pluginType.getAliases()); + keys.add(pluginType.getName()); + for (final String key : keys) { + writer.writeEmptyElement(XSD_NAMESPACE, "element"); + writer.writeAttribute("name", key); + writer.writeAttribute("type", LOG4J_PREFIX + ":" + pluginType.getClassName()); + } + } + + writer.writeEndElement(); + writer.writeEndElement(); + } + + private static void writeDocumentation(final Description description, final XMLStreamWriter writer) + throws XMLStreamException { + writer.writeStartElement(XSD_NAMESPACE, "annotation"); + writer.writeStartElement(XSD_NAMESPACE, "documentation"); + writer.writeCharacters(description.getText()); + 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(); + } + + private static void writePluginElement( + final TypeLookup lookup, final PluginElement element, final XMLStreamWriter writer) + throws XMLStreamException { + final String type = element.getType(); + final String xmlType = getXmlType(lookup, type); + final AbstractType abstractType = (AbstractType) lookup.get(type); + final PluginType pluginType = abstractType instanceof PluginType ? (PluginType) abstractType : null; + /* + * If a plugin extends another plugin or has multiple aliases + * we use a element. + */ + if (pluginType == null + || !pluginType.getAliases().isEmpty() + || !pluginType.getImplementations().isEmpty()) { + writer.writeStartElement(XSD_NAMESPACE, "group"); + writer.writeAttribute("ref", xmlType); + writeMultiplicity(element.isRequired(), element.getMultiplicity(), writer); + writeDocumentation(element.getDescription(), writer); + writer.writeEndElement(); + } else { + final Collection keys = new TreeSet<>(pluginType.getAliases()); + keys.add(pluginType.getName()); + for (final String key : keys) { + writer.writeStartElement(XSD_NAMESPACE, "element"); + writer.writeAttribute("name", key); + writer.writeAttribute("type", xmlType); + writeMultiplicity(element.isRequired(), element.getMultiplicity(), writer); + writeDocumentation(element.getDescription(), writer); + writer.writeEndElement(); + } + } + } + + private static void writePluginAttribute( + final TypeLookup lookup, final PluginAttribute attribute, final XMLStreamWriter writer) + throws XMLStreamException { + writer.writeStartElement(XSD_NAMESPACE, "attribute"); + writer.writeAttribute("name", attribute.getName()); + writer.writeAttribute("type", getXmlType(lookup, attribute.getType())); + final Description description = attribute.getDescription(); + if (description != null) { + writeDocumentation(description, writer); + } + writer.writeEndElement(); + } + + 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 Type type = lookup.get(className); + if (type != null) { + return LOG4J_PREFIX + ":" + className; + } + throw new IllegalArgumentException("Unknown Java type '" + className + "'."); + } + + private static void writeMultiplicity( + final boolean required, final String multiplicity, final XMLStreamWriter writer) throws XMLStreamException { + if (!required) { + writer.writeAttribute("minOccurs", "0"); + } + if (MULTIPLICITY_UNBOUNDED.equals(multiplicity)) { + writer.writeAttribute("maxOccurs", "unbounded"); + } + } +} diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/xsd/package-info.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/xsd/package-info.java new file mode 100644 index 00000000..2c23fb48 --- /dev/null +++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/xsd/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@Export +@Version("0.8.0") +package org.apache.logging.log4j.docgen.xsd; + +import org.osgi.annotation.bundle.Export; +import org.osgi.annotation.versioning.Version; diff --git a/log4j-docgen/src/main/mdo/plugins-model.xml b/log4j-docgen/src/main/mdo/plugins-model.xml index f3a65c1d..ad1fbd60 100644 --- a/log4j-docgen/src/main/mdo/plugins-model.xml +++ b/log4j-docgen/src/main/mdo/plugins-model.xml @@ -361,6 +361,19 @@ If the type is an array or collection, this returns the type of the element. + + org.apache.logging.log4j.docgen.xsd + SchemaGeneratorRequest + DocumentationRequest + + + fileName + String + log4j-config.xml + The path to the generated schema file. If it is relative, it will be resolved against `outputDirectory`. + + + FreeMarkerGeneratorRequest org.apache.logging.log4j.docgen.freemarker @@ -422,6 +435,18 @@ Template parameters: + + org.apache.logging.log4j.docgen.xsd + SchemaGenerator + + + Generates an XML schema. + +@param request configuration data for the generated schema. + void generateSchema(SchemaGeneratorRequest request) throws javax.xml.stream.XMLStreamException; + + + org.apache.logging.log4j.docgen.freemarker FreeMarkerGenerator diff --git a/log4j-docgen/src/test/java/org/apache/logging/log4j/docgen/internal/SchemaGeneratorTest.java b/log4j-docgen/src/test/java/org/apache/logging/log4j/docgen/internal/SchemaGeneratorTest.java new file mode 100644 index 00000000..3fbbcb8d --- /dev/null +++ b/log4j-docgen/src/test/java/org/apache/logging/log4j/docgen/internal/SchemaGeneratorTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.docgen.internal; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.Source; +import org.apache.logging.log4j.docgen.PluginSet; +import org.apache.logging.log4j.docgen.io.stax.PluginBundleStaxReader; +import org.apache.logging.log4j.docgen.xsd.SchemaGenerator; +import org.apache.logging.log4j.docgen.xsd.SchemaGeneratorRequest; +import org.junit.jupiter.api.Test; +import org.xmlunit.assertj3.XmlAssert; +import org.xmlunit.builder.Input; + +public class SchemaGeneratorTest { + + @Test + void schemaGeneration() throws Exception { + final Source xmlSchema = + Input.fromURI("https://www.w3.org/2001/XMLSchema.xsd").build(); + final URL expectedSchema = SchemaGenerator.class.getResource("/expected/xsd/log4j-config.xsd"); + final Path actualSchema = assertDoesNotThrow(() -> generateSchema()); + XmlAssert.assertThat(actualSchema) + .isValidAgainst(xmlSchema) + .and(expectedSchema) + .ignoreComments() + .ignoreWhitespace() + .areIdentical(); + } + + private static Path generateSchema() throws XMLStreamException, IOException { + final PluginBundleStaxReader reader = new PluginBundleStaxReader(); + final PluginSet set = + reader.read(SchemaGeneratorTest.class.getResourceAsStream("/META-INF/log4j/plugins-sample.xml")); + final SchemaGenerator generator = new DefaultSchemaGenerator(); + final SchemaGeneratorRequest request = new SchemaGeneratorRequest(); + final Path outputDirectory = Paths.get("target/test-site/xsd"); + Files.createDirectories(outputDirectory); + request.addPluginSet(set); + request.setOutputDirectory(outputDirectory); + + generator.generateSchema(request); + return outputDirectory.resolve(request.getFileName()); + } +} diff --git a/log4j-docgen/src/test/resources/expected/xsd/log4j-config.xsd b/log4j-docgen/src/test/resources/expected/xsd/log4j-config.xsd new file mode 100644 index 00000000..02f50c20 --- /dev/null +++ b/log4j-docgen/src/test/resources/expected/xsd/log4j-config.xsd @@ -0,0 +1,519 @@ + + + + + + + + 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. + + + + + All events should be logged. + + + + + + + Appends log events. +An Appender can contain a `Layout` if applicable as well as an `ErrorHandler`. +Typical Appender implementations coordinate with an implementation of `AbstractManager` to handle external resources such as streams, connections, and other shared state. +As Appenders are plugins, concrete implementations need to be annotated with `@Plugin` and need to provide a static factory method annotated with `@PluginFactory`. + +Most core plugins are written using a related Manager class that handle the actual task of serializing a `LogEvent` to some output location. +For instance, many Appenders can take advantage of the `@OutputStreamManager` class. + +It is recommended that Appenders don't do any heavy lifting since there can be many instances of the class being used at any given time. +When resources require locking (e.g., through `@FileLock`), it is important to isolate synchronized code to prevent concurrency issues. + + + + + + + + Interface that must be implemented to allow custom event filtering. +It is highly recommended that applications make use of the filters provided with this Log4j Core before creating their own. + +This interface supports "global" filters (i.e. - all events must pass through them first), attached to specific loggers and associated with Appenders. +It is recommended that, where possible, `Filter` implementations create a generic filtering method that can be called from any of the filter methods. + + + + + + + + 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. + + + + + + + A layout is responsible for formatting a `LogEvent` as a string. + + + + + + + + Write log events to the console. + + + + + A filter to apply to events before sending them to destination. + + + + + The layout to be used with the appender. + + + + + + The name of the appender used in appender references. +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`. + + + + + Specifies the target of the appender. + + + + + + Specifies the target of a console appender. + + + + + Logs to the standard output. + + + + + Logs to the standard error. + + + + + + + Reference to an appender defined in the `Appenders` section. + + + + + The filter to use. + + + + + + The name of an appender. + + + + + The level to filter against. + + + + + + Reference to an appender defined in the `Appenders` section. + + + + + + + + + A wrapper for a list of appenders. + + + + + A list of appender. + + + + + + + A Log4j 2.x configuration contains many components, two of them `Appenders` and `Loggers` are required. + + + + + Wrapper element for a list of properties. + +If present, this element must be the **first** child of the configuration. + + + + + Wrapper element for a list of appenders. + + + + + Wrapper element for a list of custom levels. + + + + + Wrapper element for a list of logger configurations. + + + + + + If set it should contain the name of an `Advertiser` plugin +(cf. https://logging.apache.org/log4j/2.x/manual/configuration.html#chainsaw-can-automatically-process-your-log-files-advertising-ap[documentation] ). +Log4j Core provides a single implementation based on JmDNS called `MulticastDns`. + + + + + Specifies the destination for StatusLogger events. +The possible values are: +* `out` for using standard out (default), +* `err` for using standard error, +* a string that is interpreted in order as URI, URL or the path to a local file. + +If the provided value is invalid, then the default destination of standard out will be used. + + + + + Name of the configuration. + + + + + Number of seconds between polls for configuration changes. + + + + + The name of a classpath resource to use to validate the configuration. + + + + + Specifies whether Log4j should automatically shut down when the JVM shuts down. +Possible values: +* `enable`: to enable unconditionally the hook, +* `disable`: to disable unconditionally the hook. + +The shutdown hook is enabled by default, unless Log4j Core detects the presence of the Servlet API. + + + + + Timeout in milliseconds of the logger context shut down. + + + + + Sets the level of the status logger. + + + + + If set to `true` the configuration file will be validated using an XML schema. + + + + + Specifies the verbosity level for the status logger. +This only applies to classes configured by `StatusConfiguration#setVerboseClasses`. + + + + + + Configures a custom level. + + + + The name of the level. + + + + + An integer determines the strength of the custom level relative to the built-in levels. + + + + + + A wrapper for a list of custom level configurations. + + + + + A list of custom level configurations. + + + + + + + Configures a particular logger + + + + + A list of appenders to use with this logger. + + + + + A filter to filter events, before calling the appenders. + + + + + + The name of the logger. + + + + + The level of the logger. + + + + + When set to `false` location information will **not** be computed. + +The default value depends on the logger context implementation: it is `false` for `AsyncLoggerContext` and `true` for `LoggerContext`. + + + + + + Configures a particular logger + + + + + + + + + Configures the root logger + + + + + A list of appenders to use with this logger. + + + + + A filter to filter events, before calling the appenders. + + + + + + The level of the logger. + + + + + When set to `false` location information will **not** be computed. + +The default value depends on the logger context implementation: it is `false` for `AsyncLoggerContext` and `true` for `LoggerContext`. + + + + + + A wrapper for a list of logger configurations. + + + + + A list of logger configurations. + + + + + + + A wrapper for a list of properties. + + + + + A list of properties. + + + + + + + Represents a key/value pair in the configuration. + + + + Name of the property. + + + + + Value of the property. + + + + + + The BurstFilter is a logging filter that regulates logging traffic. +Use this filter when you want to control the mean rate and maximum burst of log statements that can be sent to an appender. + + + + Action to perform if the filter matches. + + + + + Action to perform if the filter does not match. + + + + + 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. + + + + + + A flexible layout configurable with pattern string. + +The goal of this class is to {@link org.apache.logging.log4j.core.Layout#toByteArray format} a {@link LogEvent} and return the results. +The format of the result depends on the _conversion pattern_. + +The conversion pattern is closely related to the conversion pattern of the printf function in C. +A conversion pattern is composed of literal text and format control expressions called _conversion specifiers_. + + + + The pattern to use to format log events. + + + + \ No newline at end of file