This is an automated email from the ASF dual-hosted git repository. vy pushed a commit to branch LOG4J2-3628 in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 91eff37b7cc782bb403192cc28c691a8ab42dbd0 Author: Volkan Yazıcı <[email protected]> AuthorDate: Mon Nov 21 22:06:39 2022 +0100 LOG4J2-3628 Initial changelog import & export utilities. --- log4j-internal-util/pom.xml | 34 ++ .../internal/util/PositionalSaxEventHandler.java | 103 ++++++ .../logging/log4j/internal/util/StringUtils.java | 31 ++ .../logging/log4j/internal/util/XmlReader.java | 85 +++++ .../logging/log4j/internal/util/XmlWriter.java | 109 ++++++ .../internal/util/changelog/ChangelogEntry.java | 213 +++++++++++ .../internal/util/changelog/ChangelogFiles.java | 37 ++ .../internal/util/changelog/ChangelogRelease.java | 69 ++++ .../util/changelog/exporter/AsciiDocExporter.java | 394 +++++++++++++++++++++ .../changelog/exporter/AsciiDocExporterArgs.java | 39 ++ .../util/changelog/importer/MavenChanges.java | 210 +++++++++++ .../changelog/importer/MavenChangesImporter.java | 135 +++++++ .../importer/MavenChangesImporterArgs.java | 39 ++ pom.xml | 1 + 14 files changed, 1499 insertions(+) diff --git a/log4j-internal-util/pom.xml b/log4j-internal-util/pom.xml new file mode 100644 index 0000000000..b1af77b4c6 --- /dev/null +++ b/log4j-internal-util/pom.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + <parent> + <artifactId>log4j</artifactId> + <groupId>org.apache.logging.log4j</groupId> + <version>2.19.1-SNAPSHOT</version> + </parent> + + <modelVersion>4.0.0</modelVersion> + + <artifactId>log4j-internal-util</artifactId> + <name>Apache Log4j internal utilities</name> + <description>Internal Log4j utilities for project's build infrastructure.</description> + +</project> diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/PositionalSaxEventHandler.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/PositionalSaxEventHandler.java new file mode 100644 index 0000000000..b74f75018e --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/PositionalSaxEventHandler.java @@ -0,0 +1,103 @@ +/* + * 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.internal.util; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.helpers.DefaultHandler; + +import java.util.Stack; + +/** + * A SAX2 event handler adding the associated line number to each emitted nodes' user data. + * <p> + * The added node user data is keyed with {@code lineNumber}. + * </p> + */ +final class PositionalSaxEventHandler extends DefaultHandler { + + private final Stack<Element> elementStack = new Stack<>(); + + private final StringBuilder textBuffer = new StringBuilder(); + + private final Document document; + + private Locator locator; + + PositionalSaxEventHandler(final Document document) { + this.document = document; + } + + @Override + public void setDocumentLocator(final Locator locator) { + this.locator = locator; + } + + @Override + public void startElement( + final String uri, + final String localName, + final String qName, + final Attributes attributes) { + addTextIfNeeded(); + final Element element = document.createElement(qName); + for (int attributeIndex = 0; attributeIndex < attributes.getLength(); attributeIndex++) { + final String attributeQName = attributes.getQName(attributeIndex); + final String attributeValue = attributes.getValue(attributeIndex); + element.setAttribute(attributeQName, attributeValue); + } + element.setUserData("lineNumber", String.valueOf(locator.getLineNumber()), null); + elementStack.push(element); + } + + @Override + public void endElement( + final String uri, + final String localName, + final String qName) { + addTextIfNeeded(); + final Element closedElement = elementStack.pop(); + final boolean rootElement = elementStack.isEmpty(); + if (rootElement) { + document.appendChild(closedElement); + } else { + final Element parentElement = elementStack.peek(); + parentElement.appendChild(closedElement); + } + } + + @Override + public void characters(final char[] buffer, final int start, final int length) { + textBuffer.append(buffer, start, length); + } + + /** + * Outputs text accumulated under the current node. + */ + private void addTextIfNeeded() { + if (textBuffer.length() > 0) { + final Element element = elementStack.peek(); + final Node textNode = document.createTextNode(textBuffer.toString()); + element.appendChild(textNode); + textBuffer.delete(0, textBuffer.length()); + } + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/StringUtils.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/StringUtils.java new file mode 100644 index 0000000000..93bc1c942a --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/StringUtils.java @@ -0,0 +1,31 @@ +/* + * 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.internal.util; + +public final class StringUtils { + + private StringUtils() {} + + public static String trimNullable(final String input) { + return input != null ? input.trim() : null; + } + + public static boolean isBlank(final String input) { + return input == null || input.matches("\\s*"); + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/XmlReader.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/XmlReader.java new file mode 100644 index 0000000000..f4045315d7 --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/XmlReader.java @@ -0,0 +1,85 @@ +/* + * 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.internal.util; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.FileInputStream; +import java.io.InputStream; +import java.nio.file.Path; + +/** + * A SAX2-based XML reader. + */ +public final class XmlReader { + + private XmlReader() {} + + public static Element readXmlFileRootElement(final Path path, final String rootElementName) { + try (final InputStream inputStream = new FileInputStream(path.toFile())) { + final Document document = readXml(inputStream); + final Element rootElement = document.getDocumentElement(); + if (!rootElementName.equals(rootElement.getNodeName())) { + final String message = String.format( + "was expecting root element to be called `%s`, found: `%s`", + rootElementName, rootElement.getNodeName()); + throw new IllegalArgumentException(message); + } + return rootElement; + } catch (final Exception error) { + final String message = String.format( + "XML read failure for file `%s` and root element `%s`", path, rootElementName); + throw new RuntimeException(message, error); + } + } + + private static Document readXml(final InputStream inputStream) throws Exception { + final SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + final SAXParser parser = parserFactory.newSAXParser(); + final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + final Document document = documentBuilder.newDocument(); + PositionalSaxEventHandler handler = new PositionalSaxEventHandler(document); + parser.parse(inputStream, handler); + return document; + } + + public static RuntimeException failureAtXmlNode( + final Node node, + final String messageFormat, + final Object... messageArgs) { + return failureAtXmlNode(null, node, messageFormat, messageArgs); + } + + public static RuntimeException failureAtXmlNode( + final Throwable cause, + final Node node, + final String messageFormat, + final Object... messageArgs) { + final Object lineNumber = node.getUserData("lineNumber"); + final String messagePrefix = String.format("[line %s] ", lineNumber); + final String message = String.format(messagePrefix + messageFormat, messageArgs); + return new IllegalArgumentException(message, cause); + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/XmlWriter.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/XmlWriter.java new file mode 100644 index 0000000000..b797eb0853 --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/XmlWriter.java @@ -0,0 +1,109 @@ +/* + * 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.internal.util; + +import org.w3c.dom.Comment; +import org.w3c.dom.Document; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.function.Consumer; + +public final class XmlWriter { + + private static final Charset ENCODING = StandardCharsets.UTF_8; + + private XmlWriter() {} + + public static void toFile(final Path filepath, final Consumer<Document> documentConsumer) { + try { + final String xml = toString(documentConsumer); + final byte[] xmlBytes = xml.getBytes(ENCODING); + Files.createDirectories(filepath.getParent()); + Files.write(filepath, xmlBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } catch (final Exception error) { + final String message = String.format("failed writing XML to file `%s`", filepath); + throw new RuntimeException(message, error); + } + } + + public static String toString(final Consumer<Document> documentConsumer) { + try { + + // Create the document. + final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + + // Append the license comment. + final Document document = documentBuilder.newDocument(); + document.setXmlStandalone(true); + final Comment licenseComment = document.createComment("\n" + + " Licensed to the Apache Software Foundation (ASF) under one or more\n" + + " contributor license agreements. See the NOTICE file distributed with\n" + + " this work for additional information regarding copyright ownership.\n" + + " The ASF licenses this file to You under the Apache License, Version 2.0\n" + + " (the \"License\"); you may not use this file except in compliance with\n" + + " the License. You may obtain a copy of the License at\n" + + "\n" + + " http://www.apache.org/licenses/LICENSE-2.0\n" + + "\n" + + " Unless required by applicable law or agreed to in writing, software\n" + + " distributed under the License is distributed on an \"AS IS\" BASIS,\n" + + " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" + + " See the License for the specific language governing permissions and\n" + + " limitations under the License." + + "\n"); + document.appendChild(licenseComment); + + // Execute request changes. + documentConsumer.accept(document); + + // Serialize the document. + return serializeXmlDocument(document); + + } catch (final Exception error) { + throw new RuntimeException("failed writing XML", error); + } + } + + private static String serializeXmlDocument(final Document document) throws Exception { + final Transformer transformer = TransformerFactory.newInstance().newTransformer(); + final StreamResult result = new StreamResult(new StringWriter()); + final DOMSource source = new DOMSource(document); + transformer.setOutputProperty(OutputKeys.ENCODING, ENCODING.name()); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + transformer.transform(source, result); + return result.getWriter().toString() + // Life is too short to solve DOM transformer issues decently. + .replace("?><!--", "?>\n<!--") + .replace("--><", "-->\n<"); + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/ChangelogEntry.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/ChangelogEntry.java new file mode 100644 index 0000000000..f662a75c9b --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/ChangelogEntry.java @@ -0,0 +1,213 @@ +/* + * 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.internal.util.changelog; + +import org.apache.logging.log4j.internal.util.XmlReader; +import org.apache.logging.log4j.internal.util.XmlWriter; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.apache.logging.log4j.internal.util.StringUtils.trimNullable; + +public final class ChangelogEntry { + + public final Type type; + + public final List<Issue> issues; + + public final List<Author> authors; + + public final Description description; + + public enum Type { + + ADDED, + + CHANGED, + + DEPRECATED, + + REMOVED, + + FIXED, + + SECURITY; + + private String toXmlAttribute() { + return toString().toLowerCase(Locale.US); + } + + private static Type fromXmlAttribute(final String attribute) { + final String upperCaseAttribute = attribute != null ? attribute.toUpperCase(Locale.US) : null; + return Type.valueOf(upperCaseAttribute); + } + + } + + public static final class Issue { + + public final String id; + + public final String link; + + public Issue(final String id, final String link) { + this.id = id; + this.link = link; + } + + } + + public static final class Author { + + public final String id; + + public final String name; + + public Author(final String id, final String name) { + this.id = id; + this.name = name; + } + + } + + public static final class Description { + + public final String format; + + public final String text; + + public Description(final String format, final String text) { + this.format = format; + this.text = text; + } + + } + + public ChangelogEntry( + final Type type, + final List<Issue> issues, + final List<Author> authors, + final Description description) { + this.type = type; + this.issues = issues; + this.authors = authors; + this.description = description; + } + + public void writeToXmlFile(final Path path) { + XmlWriter.toFile(path, document -> { + + // Create the `entry` root element. + final Element entryElement = document.createElement("entry"); + entryElement.setAttribute("type", type.toXmlAttribute()); + document.appendChild(entryElement); + + // Create the `issue` elements. + issues.forEach(issue -> { + final Element issueElement = document.createElement("issue"); + issueElement.setAttribute("id", issue.id); + issueElement.setAttribute("link", issue.link); + entryElement.appendChild(issueElement); + }); + + // Create the `author` elements. + authors.forEach(author -> { + final Element authorElement = document.createElement("author"); + if (author.id != null) { + authorElement.setAttribute("id", author.id); + } else { + authorElement.setAttribute("name", author.name); + } + entryElement.appendChild(authorElement); + }); + + // Create the `description` element. + final Element descriptionElement = document.createElement("description"); + if (description.format != null) { + descriptionElement.setAttribute("format", description.format); + } + descriptionElement.setTextContent(description.text); + entryElement.appendChild(descriptionElement); + + }); + } + + public static ChangelogEntry readFromXmlFile(final Path path) { + + // Read the `entry` root element. + final Element entryElement = XmlReader.readXmlFileRootElement(path, "entry"); + final String typeAttribute = entryElement.getAttribute("type"); + final Type type; + try { + type = Type.fromXmlAttribute(typeAttribute); + } catch (final Exception error) { + throw XmlReader.failureAtXmlNode(error, entryElement, "`type` attribute read failure"); + } + + // Read the `issue` elements. + final NodeList issueElements = entryElement.getElementsByTagName("issue"); + final int issueCount = issueElements.getLength(); + final List<Issue> issues = IntStream + .range(0, issueCount) + .mapToObj(issueIndex -> { + final Element issueElement = (Element) issueElements.item(issueIndex); + final String issueId = issueElement.getAttribute("id"); + final String issueLink = issueElement.getAttribute("link"); + return new Issue(issueId, issueLink); + }) + .collect(Collectors.toList()); + + // Read the `author` elements. + final NodeList authorElements = entryElement.getElementsByTagName("author"); + final int authorCount = authorElements.getLength(); + if (authorCount < 1) { + throw XmlReader.failureAtXmlNode(entryElement, "no `author` elements found"); + } + final List<Author> authors = IntStream + .range(0, authorCount) + .mapToObj(authorIndex -> { + final Element authorElement = (Element) authorElements.item(authorIndex); + final String authorId = authorElement.getAttribute("id"); + final String authorName = authorElement.getAttribute("name"); + return new Author(authorId, authorName); + }) + .collect(Collectors.toList()); + + // Read the `description` element. + final NodeList descriptionElements = entryElement.getElementsByTagName("description"); + final int descriptionCount = descriptionElements.getLength(); + if (descriptionCount != 1) { + throw XmlReader.failureAtXmlNode( + entryElement, "was expecting a single `description` element, found: %d", descriptionCount); + } + final Element descriptionElement = (Element) descriptionElements.item(0); + final String descriptionFormat = descriptionElement.getAttribute("format"); + final String descriptionText = trimNullable(descriptionElement.getTextContent()); + final Description description = new Description(descriptionFormat, descriptionText); + + // Create the instance. + return new ChangelogEntry(type, issues, authors, description); + + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/ChangelogFiles.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/ChangelogFiles.java new file mode 100644 index 0000000000..598c6e378a --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/ChangelogFiles.java @@ -0,0 +1,37 @@ +/* + * 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.internal.util.changelog; + +import java.nio.file.Path; + +public final class ChangelogFiles { + + private ChangelogFiles() {} + + public static Path changelogDirectory(final Path projectRootDirectory) { + return projectRootDirectory.resolve("changelog"); + } + + public static Path releaseXmlFile(final Path releaseDirectory) { + return releaseDirectory.resolve(".release.xml"); + } + + public static Path introAsciiDocFile(final Path releaseDirectory) { + return releaseDirectory.resolve(".intro.adoc"); + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/ChangelogRelease.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/ChangelogRelease.java new file mode 100644 index 0000000000..d044f42eca --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/ChangelogRelease.java @@ -0,0 +1,69 @@ +/* + * 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.internal.util.changelog; + +import org.apache.logging.log4j.internal.util.XmlReader; +import org.apache.logging.log4j.internal.util.XmlWriter; +import org.w3c.dom.Element; + +import java.nio.file.Path; + +import static org.apache.logging.log4j.internal.util.StringUtils.trimNullable; + +public final class ChangelogRelease { + + public final String version; + + public final String date; + + public ChangelogRelease(final String version, final String date) { + this.version = version; + this.date = date; + } + + public void writeToXmlFile(final Path path) { + XmlWriter.toFile(path, document -> { + final Element releaseElement = document.createElement("release"); + releaseElement.setAttribute("version", version); + releaseElement.setAttribute("date", date); + document.appendChild(releaseElement); + }); + } + + public static ChangelogRelease readFromXmlFile(final Path path) { + + // Read the XML file. + final Element releaseElement = XmlReader.readXmlFileRootElement(path, "release"); + + // Read the `version` attribute. + final String version = trimNullable(releaseElement.getAttribute("version")); + if (version == null) { + throw new IllegalArgumentException("blank or missing attribute: `version`"); + } + + // Read the `date` attribute. + final String date = trimNullable(releaseElement.getAttribute("date")); + if (date == null) { + throw new IllegalArgumentException("blank or missing attribute: `date`"); + } + + // Create the instance. + return new ChangelogRelease(version, date); + + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/exporter/AsciiDocExporter.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/exporter/AsciiDocExporter.java new file mode 100644 index 0000000000..862642bcf9 --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/exporter/AsciiDocExporter.java @@ -0,0 +1,394 @@ +/* + * 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.internal.util.changelog.exporter; + +import org.apache.logging.log4j.internal.util.changelog.ChangelogEntry; +import org.apache.logging.log4j.internal.util.changelog.ChangelogFiles; +import org.apache.logging.log4j.internal.util.changelog.ChangelogRelease; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileTime; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class AsciiDocExporter { + + private static final String TARGET_RELATIVE_DIRECTORY = "src/site/asciidoc/changelog"; + + private static final Comparator<Path> FILE_MODIFICATION_TIME_COMPARATOR = Comparator.comparing(path -> { + try { + final FileTime fileTime = Files.getLastModifiedTime(path); + return fileTime.toMillis(); + } catch (final IOException error) { + final String message = String.format("failed reading the last-modified time: `%s`", path); + throw new UncheckedIOException(message, error); + } + }); + + private static final String LICENSE_HEADER_ASCIIDOC = "////\n" + + "Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.\n" + + "See the `NOTICE.txt` file distributed with this work for additional information regarding copyright ownership.\n" + + "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.\n" + + "You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0].\n" + + '\n' + + "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.\n" + + "See the License for the specific language governing permissions and limitations under the License.\n" + + "////\n"; + + private static final String AUTO_GENERATION_WARNING_ASCIIDOC = "////\n" + + "*DO NOT EDIT THIS FILE!!*\n" + + "This file is automatically generated from the release changelog directory!\n" + + "////\n"; + + private AsciiDocExporter() {} + + public static void main(final String[] mainArgs) { + + // Read arguments. + final AsciiDocExporterArgs args = AsciiDocExporterArgs.fromMainArgs(mainArgs); + + // Find release directories. + final Path changelogDirectory = ChangelogFiles.changelogDirectory(args.projectRootDirectory); + final List<Path> releaseDirectories = findAdjacentFiles(changelogDirectory) + .sorted(FILE_MODIFICATION_TIME_COMPARATOR) + .collect(Collectors.toList()); + final int releaseDirectoryCount = releaseDirectories.size(); + + // Read the release information files. + final List<ChangelogRelease> changelogReleases = releaseDirectories + .stream() + .map(releaseDirectory -> { + final Path releaseXmlFile = ChangelogFiles.releaseXmlFile(releaseDirectory); + return ChangelogRelease.readFromXmlFile(releaseXmlFile); + }) + .collect(Collectors.toList()); + + // Export releases. + if (releaseDirectoryCount > 0) { + + // Export each release directory. + for (int releaseIndex = 0; releaseIndex < releaseDirectories.size(); releaseIndex++) { + final Path releaseDirectory = releaseDirectories.get(releaseIndex); + final ChangelogRelease changelogRelease = changelogReleases.get(releaseIndex); + try { + exportRelease(args.projectRootDirectory, releaseDirectory, changelogRelease); + } catch (final Exception error) { + final String message = + String.format("failed exporting release from directory `%s`", releaseDirectory); + throw new RuntimeException(message, error); + } + } + + // Report the operation. + if (releaseDirectoryCount == 1) { + System.out.format("exported a single release directory: `%s`%n", releaseDirectories.get(0)); + } else { + System.out.format( + "exported %d release directories: ..., `%s`%n", + releaseDirectories.size(), + releaseDirectories.get(releaseDirectoryCount - 1)); + } + + } + + // Export the release index. + exportReleaseIndex(args.projectRootDirectory, changelogReleases); + + } + + /** + * Finds files non-recursively in the given directory. + * <p> + * Given directory itself and hidden files are filtered out. + * </p> + */ + @SuppressWarnings("RedundantIfStatement") + private static Stream<Path> findAdjacentFiles(final Path directory) { + try { + return Files + .walk(directory, 1) + .filter(path -> { + + // Skip the directory itself. + if (path.equals(directory)) { + return false; + } + + // Skip hidden files. + boolean hiddenFile = path.getFileName().toString().startsWith("."); + if (hiddenFile) { + return false; + } + + // Accept the rest. + return true; + + }); + } catch (final IOException error) { + final String message = String.format("failed walking directory: `%s`", directory); + throw new UncheckedIOException(message, error); + } + } + + private static void exportRelease( + final Path projectRootDirectory, + final Path releaseDirectory, + final ChangelogRelease changelogRelease) { + + // Read the changelog intro. + final String introAsciiDoc = readIntroAsciiDoc(releaseDirectory); + + // Read the changelog entries. + final List<ChangelogEntry> changelogEntries = findAdjacentFiles(releaseDirectory) + .sorted(FILE_MODIFICATION_TIME_COMPARATOR) + .map(ChangelogEntry::readFromXmlFile) + .collect(Collectors.toList()); + + // Export the release. + try { + exportRelease(projectRootDirectory, changelogRelease, introAsciiDoc, changelogEntries); + } catch (final IOException error) { + final String message = String.format("failed exporting release from directory `%s`", releaseDirectory); + throw new UncheckedIOException(message, error); + } + + } + + private static String readIntroAsciiDoc(final Path releaseDirectory) { + final Path introAsciiDocFile = ChangelogFiles.introAsciiDocFile(releaseDirectory); + if (!Files.exists(introAsciiDocFile)) { + return ""; + } + final byte[] introAsciiDocBytes; + try { + introAsciiDocBytes = Files.readAllBytes(introAsciiDocFile); + } catch (final IOException error) { + final String message = String.format("failed reading intro AsciiDoc file: `%s`", introAsciiDocFile); + throw new UncheckedIOException(message, error); + } + return new String(introAsciiDocBytes, StandardCharsets.UTF_8); + } + + private static void exportRelease( + final Path projectRootDirectory, + final ChangelogRelease release, + final String introAsciiDoc, + final List<ChangelogEntry> entries) + throws IOException { + final String asciiDocFilename = changelogReleaseAsciiDocFilename(release); + final Path asciiDocFile = projectRootDirectory + .resolve(TARGET_RELATIVE_DIRECTORY) + .resolve(asciiDocFilename); + Files.createDirectories(asciiDocFile.getParent()); + final String asciiDoc = exportReleaseToAsciiDoc(release, introAsciiDoc, entries); + final byte[] asciiDocBytes = asciiDoc.getBytes(StandardCharsets.UTF_8); + Files.write(asciiDocFile, asciiDocBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + + private static String exportReleaseToAsciiDoc( + final ChangelogRelease release, + final String introAsciiDoc, + final List<ChangelogEntry> entries) { + + // Write the header. + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder + .append(LICENSE_HEADER_ASCIIDOC) + .append('\n') + .append(AUTO_GENERATION_WARNING_ASCIIDOC) + .append('\n') + .append("= ") + .append(release.version) + .append(" (") + .append(release.date) + .append(")\n") + .append(introAsciiDoc) + .append("\n"); + + if (!entries.isEmpty()) { + + stringBuilder.append("== Changes\n"); + + // Group entries by type. + final Map<ChangelogEntry.Type, List<ChangelogEntry>> entriesByType = entries + .stream() + .collect(Collectors.groupingBy(changelogEntry -> changelogEntry.type)); + + // Write entries for each type. + boolean[] firstEntryType = {true}; + entriesByType + .keySet() + .stream() + // Sorting is necessary for a consistent layout across different runs. + .sorted() + .forEach(type -> { + stringBuilder.append('\n'); + appendEntryTypeHeader(stringBuilder, type); + entriesByType.get(type).forEach(entry -> appendEntry(stringBuilder, entry)); + }); + + } + + // Return the accumulated document so far. + return stringBuilder.toString(); + + } + + private static void appendEntryTypeHeader(final StringBuilder stringBuilder, final ChangelogEntry.Type type) { + final String typeName = type.toString().toLowerCase(Locale.US); + final String header = typeName.substring(0, 1).toUpperCase(Locale.US) + typeName.substring(1); + stringBuilder + .append("=== ") + .append(header) + .append("\n\n"); + } + + private static void appendEntry(final StringBuilder stringBuilder, final ChangelogEntry entry) { + stringBuilder.append("* "); + appendEntryDescription(stringBuilder, entry.description); + final boolean containingIssues = !entry.issues.isEmpty(); + final boolean containingAuthors = !entry.authors.isEmpty(); + if (containingIssues || containingAuthors) { + stringBuilder.append(" ("); + if (containingIssues) { + appendEntryIssues(stringBuilder, entry.issues); + } + if (containingIssues && containingAuthors) { + stringBuilder.append(' '); + } + if (containingAuthors) { + appendEntryAuthors(stringBuilder, entry.authors); + } + stringBuilder.append(")"); + } + stringBuilder.append('\n'); + } + + private static void appendEntryDescription( + final StringBuilder stringBuilder, + final ChangelogEntry.Description description) { + if (!"asciidoc".equals(description.format)) { + final String message = String.format("unsupported description format: `%s`", description.format); + throw new RuntimeException(message); + } + stringBuilder.append(description.text); + } + + private static void appendEntryIssues( + final StringBuilder stringBuilder, + final List<ChangelogEntry.Issue> issues) { + stringBuilder.append("for "); + final int issueCount = issues.size(); + for (int issueIndex = 0; issueIndex < issueCount; issueIndex++) { + final ChangelogEntry.Issue issue = issues.get(issueIndex); + appendEntryIssue(stringBuilder, issue); + if ((issueIndex + 1) != issueCount) { + stringBuilder.append(", "); + } + } + } + + private static void appendEntryIssue(final StringBuilder stringBuilder, final ChangelogEntry.Issue issue) { + stringBuilder + .append(issue.link) + .append('[') + .append(issue.id) + .append(']'); + } + + private static void appendEntryAuthors( + final StringBuilder stringBuilder, + final List<ChangelogEntry.Author> authors) { + stringBuilder.append("by "); + final int authorCount = authors.size(); + for (int authorIndex = 0; authorIndex < authors.size(); authorIndex++) { + final ChangelogEntry.Author author = authors.get(authorIndex); + appendEntryAuthor(stringBuilder, author); + if ((authorIndex + 1) != authorCount) { + stringBuilder.append(", "); + } + } + } + + private static void appendEntryAuthor(final StringBuilder stringBuilder, final ChangelogEntry.Author author) { + if (author.id != null) { + stringBuilder + .append('`') + .append(author.id) + .append('`'); + } else { + // Normalize author names written in `Doe, John` form. + if (author.name.contains(",")) { + String[] nameFields = author.name.split(",", 1); + stringBuilder.append(nameFields[1].trim()); + stringBuilder.append(nameFields[0].trim()); + } else { + stringBuilder.append(author.name); + } + } + } + + private static void exportReleaseIndex( + final Path projectRootDirectory, + final List<ChangelogRelease> changelogReleases) { + final String asciiDoc = exportReleaseIndexToAsciiDoc(changelogReleases); + final byte[] asciiDocBytes = asciiDoc.getBytes(StandardCharsets.UTF_8); + final Path asciiDocFile = projectRootDirectory.resolve(TARGET_RELATIVE_DIRECTORY).resolve("index.adoc"); + try { + Files.write(asciiDocFile, asciiDocBytes); + } catch (final IOException error) { + throw new UncheckedIOException(error); + } + } + + private static String exportReleaseIndexToAsciiDoc(final List<ChangelogRelease> changelogReleases) { + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder + .append(LICENSE_HEADER_ASCIIDOC) + .append('\n') + .append(AUTO_GENERATION_WARNING_ASCIIDOC) + .append("\n= Release changelogs\n\n"); + for (int releaseIndex = changelogReleases.size() - 1; releaseIndex >= 0; releaseIndex--) { + final ChangelogRelease changelogRelease = changelogReleases.get(releaseIndex); + final String asciiDocFilename = changelogReleaseAsciiDocFilename(changelogRelease); + final String asciiDocBullet = String.format( + "* [%s] xref:%s[%s]\n", + changelogRelease.date, + asciiDocFilename, + changelogRelease.version); + stringBuilder.append(asciiDocBullet); + } + return stringBuilder.toString(); + } + + private static String changelogReleaseAsciiDocFilename(final ChangelogRelease changelogRelease) { + return String.format( + "%s-%s.adoc", + changelogRelease.date.replaceAll("-", ""), + changelogRelease.version); + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/exporter/AsciiDocExporterArgs.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/exporter/AsciiDocExporterArgs.java new file mode 100644 index 0000000000..fb26a6f10f --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/exporter/AsciiDocExporterArgs.java @@ -0,0 +1,39 @@ +/* + * 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.internal.util.changelog.exporter; + +import java.nio.file.Path; +import java.nio.file.Paths; + +final class AsciiDocExporterArgs { + + final Path projectRootDirectory; + + private AsciiDocExporterArgs(final Path projectRootDirectory) { + this.projectRootDirectory = projectRootDirectory; + } + + static AsciiDocExporterArgs fromMainArgs(final String[] args) { + if (args.length != 1) { + final String message = String.format("invalid number of arguments: %d, was expecting: <projectRootPath>", args.length); + throw new IllegalArgumentException(message); + } + final Path projectRootPath = Paths.get(args[0]); + return new AsciiDocExporterArgs(projectRootPath); + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/importer/MavenChanges.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/importer/MavenChanges.java new file mode 100644 index 0000000000..5200ac1e16 --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/importer/MavenChanges.java @@ -0,0 +1,210 @@ +/* + * 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.internal.util.changelog.importer; + +import org.apache.logging.log4j.internal.util.XmlReader; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.apache.logging.log4j.internal.util.StringUtils.isBlank; +import static org.apache.logging.log4j.internal.util.StringUtils.trimNullable; +import static org.apache.logging.log4j.internal.util.XmlReader.failureAtXmlNode; +import static org.apache.logging.log4j.internal.util.XmlReader.readXmlFileRootElement; + +final class MavenChanges { + + final List<Release> releases; + + private MavenChanges(final List<Release> releases) { + this.releases = releases; + } + + static MavenChanges readFromProjectRootPath(final Path projectRootDirectory) { + + // Read the root element. + final Path xmlPath = projectRootDirectory.resolve("src/changes/changes.xml"); + final Element documentElement = readXmlFileRootElement(xmlPath, "document"); + + // Read the `body` element. + final NodeList bodyElements = documentElement.getElementsByTagName("body"); + final int bodyElementCount = bodyElements.getLength(); + if (bodyElementCount != 1) { + throw XmlReader.failureAtXmlNode( + documentElement, "was expecting a single `body` element, found: %d", bodyElementCount); + } + final Node bodyElement = bodyElements.item(0); + + // Read releases. + final List<Release> releases = new ArrayList<>(); + final NodeList releaseNodes = bodyElement.getChildNodes(); + final int releaseNodeCount = releaseNodes.getLength(); + for (int releaseNodeIndex = 0; releaseNodeIndex < releaseNodeCount; releaseNodeIndex++) { + final Node releaseNode = releaseNodes.item(releaseNodeIndex); + if ("release".equals(releaseNode.getNodeName()) && Node.ELEMENT_NODE == releaseNode.getNodeType()) { + final Element releaseElement = (Element) releaseNode; + final Release release = Release.fromElement(releaseElement); + releases.add(release); + } + } + + // Create the instance. + return new MavenChanges(releases); + + } + + static final class Release { + + final String version; + + final String date; + + final List<Action> actions; + + private Release(final String version, final String date, final List<Action> actions) { + this.version = version; + this.date = date; + this.actions = actions; + } + + private static Release fromElement(final Element element) { + + // Read `version`. + final String version = trimNullable(element.getAttribute("version")); + if (isBlank(version)) { + throw XmlReader.failureAtXmlNode(element, "blank attribute: `version`"); + } + + // Read `date`. + final String date = trimNullable(element.getAttribute("date")); + final String datePattern = "^(TBD|[0-9]{4}-[0-9]{2}-[0-9]{2})$"; + if (!date.matches(datePattern)) { + throw XmlReader.failureAtXmlNode(element, "`date` doesn't match with the `%s` pattern: `%s`", datePattern, date); + } + + // Read actions. + final List<Action> actions = new ArrayList<>(); + final NodeList actionNodes = element.getChildNodes(); + final int actionNodeCount = actionNodes.getLength(); + for (int actionNodeIndex = 0; actionNodeIndex < actionNodeCount; actionNodeIndex++) { + final Node actionNode = actionNodes.item(actionNodeIndex); + if ("action".equals(actionNode.getNodeName()) && Node.ELEMENT_NODE == actionNode.getNodeType()) { + Element actionElement = (Element) actionNode; + Action action = Action.fromElement(actionElement); + actions.add(action); + } + } + + // Create the instance. + return new Release(version, date, actions); + + } + + @Override + public String toString() { + return version + " @ " + date; + } + + } + + static final class Action { + + final String issue; + + final Type type; + + final String dev; + + final String dueTo; + + final String description; + + enum Type {ADD, FIX, UPDATE, REMOVE} + + private Action( + final String issue, + final Type type, + final String dev, + final String dueTo, + final String description) { + this.issue = issue; + this.type = type; + this.dev = dev; + this.dueTo = dueTo; + this.description = description; + } + + private static Action fromElement(final Element element) { + + // Read `issue`. + String issue = trimNullable(element.getAttribute("issue")); + final String issuePattern = "^LOG4J2-[0-9]+$"; + if (isBlank(issue)) { + issue = null; + } else if (!issue.matches(issuePattern)) { + throw XmlReader.failureAtXmlNode(element, "`issue` doesn't match with the `%s` pattern: `%s`", issuePattern, issue); + } + + // Read `type`. + final String typeString = trimNullable(element.getAttribute("type")); + final Type type; + if (isBlank(typeString)) { + type = Type.UPDATE; + } else { + try { + type = Type.valueOf(typeString.toUpperCase(Locale.US)); + } catch (IllegalArgumentException error) { + throw failureAtXmlNode(error, element, "invalid type: `%s`", typeString); + } + } + + // Read `dev`. + final String dev = trimNullable(element.getAttribute("dev")); + if (isBlank(dev)) { + throw XmlReader.failureAtXmlNode(element, "blank attribute: `dev`"); + } + + // Read `dueTo`. + String dueTo = trimNullable(element.getAttribute("dueTo")); + if (isBlank(dueTo)) { + dueTo = null; + } + + // Read `description`. + final String description = trimNullable(element.getTextContent()); + if (isBlank(description)) { + throw XmlReader.failureAtXmlNode(element, "blank `description`"); + } + + // Create the instance. + return new Action(issue, type, dev, dueTo, description); + + } + + @Override + public String toString() { + return issue != null ? issue : "unknown"; + } + + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/importer/MavenChangesImporter.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/importer/MavenChangesImporter.java new file mode 100644 index 0000000000..61e83cee6a --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/importer/MavenChangesImporter.java @@ -0,0 +1,135 @@ +/* + * 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.internal.util.changelog.importer; + +import org.apache.logging.log4j.internal.util.changelog.ChangelogEntry; +import org.apache.logging.log4j.internal.util.changelog.ChangelogFiles; +import org.apache.logging.log4j.internal.util.changelog.ChangelogRelease; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +public final class MavenChangesImporter { + + private MavenChangesImporter() {} + + public static void main(final String[] mainArgs) { + final MavenChangesImporterArgs args = MavenChangesImporterArgs.fromMainArgs(mainArgs); + final MavenChanges mavenChanges = MavenChanges.readFromProjectRootPath(args.projectRootDirectory); + mavenChanges.releases.forEach(release -> { + if ("TBD".equals(release.date)) { + writeUnreleased(args.projectRootDirectory, release); + } else { + writeReleased(args.projectRootDirectory, release); + } + }); + } + + private static void writeUnreleased(final Path projectRootDirectory, final MavenChanges.Release release) { + final Path releaseDirectory = ChangelogFiles + .changelogDirectory(projectRootDirectory) + .resolve(".unreleased"); + release.actions.forEach(action -> writeAction(releaseDirectory, action)); + } + + private static void writeReleased(final Path projectRootDirectory, final MavenChanges.Release release) { + + // Determine the directory for this particular release. + final Path releaseDirectory = ChangelogFiles + .changelogDirectory(projectRootDirectory) + .resolve(String.format("%s-%s", release.date.replaceAll("-", ""), release.version)); + + // Write release information. + final Path releaseFile = ChangelogFiles.releaseXmlFile(releaseDirectory); + final ChangelogRelease changelogRelease = new ChangelogRelease(release.version, release.date); + changelogRelease.writeToXmlFile(releaseFile); + + // Write release actions. + release.actions.forEach(action -> writeAction(releaseDirectory, action)); + + } + + private static void writeAction(final Path releaseDirectory, final MavenChanges.Action action) { + final ChangelogEntry changelogEntry = changelogEntry(action); + final String changelogEntryFilename = changelogEntryFilename(action); + final Path changelogEntryFile = releaseDirectory.resolve(changelogEntryFilename); + changelogEntry.writeToXmlFile(changelogEntryFile); + } + + private static String changelogEntryFilename(final MavenChanges.Action action) { + final StringBuilder actionRelativeFileBuilder = new StringBuilder(); + if (action.issue != null) { + actionRelativeFileBuilder + .append(action.issue) + .append('_'); + } + final String sanitizedDescription = action + .description + .substring(0, Math.min(action.description.length(), 60)) + .replaceAll("[^A-Za-z0-9]", "_") + .replaceAll("_+", "_") + .replaceAll("[^A-Za-z0-9]$", ""); + actionRelativeFileBuilder.append(sanitizedDescription); + actionRelativeFileBuilder.append(".xml"); + return actionRelativeFileBuilder.toString(); + } + + private static ChangelogEntry changelogEntry(final MavenChanges.Action action) { + + // Create the `type`. + final ChangelogEntry.Type type = changelogType(action.type); + + // Create the `issue`s. + final List<ChangelogEntry.Issue> issues = new ArrayList<>(1); + if (action.issue != null) { + final String issueLink = String.format("https://issues.apache.org/jira/browse/%s", action.issue); + final ChangelogEntry.Issue issue = new ChangelogEntry.Issue(action.issue, issueLink); + issues.add(issue); + } + + // Create the `author`s. + final List<ChangelogEntry.Author> authors = new ArrayList<>(2); + authors.add(new ChangelogEntry.Author(action.dev, null)); + if (action.dueTo != null) { + authors.add(new ChangelogEntry.Author(null, action.dueTo)); + } + + // Create the `description`. + final ChangelogEntry.Description description = new ChangelogEntry.Description("asciidoc", action.description); + + // Create the instance. + return new ChangelogEntry(type, issues, authors, description); + + } + + /** + * Maps `maven-changes-plugin` action types to their `Keep a Changelog` equivalents. + */ + private static ChangelogEntry.Type changelogType(final MavenChanges.Action.Type type) { + if (MavenChanges.Action.Type.ADD.equals(type)) { + return ChangelogEntry.Type.ADDED; + } else if (MavenChanges.Action.Type.FIX.equals(type)) { + return ChangelogEntry.Type.FIXED; + } else if (MavenChanges.Action.Type.REMOVE.equals(type)) { + return ChangelogEntry.Type.REMOVED; + } else { + return ChangelogEntry.Type.CHANGED; + } + } + +} diff --git a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/importer/MavenChangesImporterArgs.java b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/importer/MavenChangesImporterArgs.java new file mode 100644 index 0000000000..7e01cd1e45 --- /dev/null +++ b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/importer/MavenChangesImporterArgs.java @@ -0,0 +1,39 @@ +/* + * 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.internal.util.changelog.importer; + +import java.nio.file.Path; +import java.nio.file.Paths; + +final class MavenChangesImporterArgs { + + final Path projectRootDirectory; + + private MavenChangesImporterArgs(final Path projectRootDirectory) { + this.projectRootDirectory = projectRootDirectory; + } + + static MavenChangesImporterArgs fromMainArgs(final String[] args) { + if (args.length != 1) { + final String s = String.format("invalid number of arguments: %d, was expecting: <projectRootPath>", args.length); + throw new IllegalArgumentException(s); + } + final Path projectRootPath = Paths.get(args[0]); + return new MavenChangesImporterArgs(projectRootPath); + } + +} diff --git a/pom.xml b/pom.xml index 1d70a46ca0..654254d8d8 100644 --- a/pom.xml +++ b/pom.xml @@ -1745,6 +1745,7 @@ <module>log4j-to-slf4j</module> <module>log4j-to-jul</module> <module>log4j-web</module> + <module>log4j-internal-util</module> </modules> <profiles> <profile>
