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 83e99b8d885401c20a2b68de14bf4efa1b0285a5
Author: Volkan Yazıcı <[email protected]>
AuthorDate: Wed Nov 23 19:19:57 2022 +0100

    LOG4J2-3628 Add releaser.
---
 .../logging/log4j/internal/util/AsciiDocUtils.java | 40 ++++++++++
 .../logging/log4j/internal/util/XmlReader.java     | 28 +++++++
 .../internal/util/changelog/ChangelogEntry.java    | 34 +++-----
 .../internal/util/changelog/ChangelogFiles.java    | 22 +++++
 .../util/changelog/exporter/AsciiDocExporter.java  | 87 ++++++++++----------
 .../util/changelog/importer/MavenChanges.java      |  8 +-
 .../changelog/importer/MavenChangesImporter.java   |  7 +-
 .../util/changelog/releaser/ChangelogReleaser.java | 93 ++++++++++++++++++++++
 .../ChangelogReleaserArgs.java}                    | 24 +++---
 src/site/asciidoc/changelog/index.adoc             |  1 +
 src/site/asciidoc/changelog/unreleased.adoc        | 35 ++++++++
 11 files changed, 294 insertions(+), 85 deletions(-)

diff --git 
a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/AsciiDocUtils.java
 
b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/AsciiDocUtils.java
new file mode 100644
index 0000000000..91cfb41ac1
--- /dev/null
+++ 
b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/AsciiDocUtils.java
@@ -0,0 +1,40 @@
+/*
+ * 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
+ *
+ *      https://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 AsciiDocUtils {
+
+    public static final String LICENSE_COMMENT_BLOCK = "////\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" +
+            "         https://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" +
+            "////\n";
+
+    private AsciiDocUtils() {}
+
+}
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
index 35650ed1ba..33c649b4cf 100644
--- 
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
@@ -19,6 +19,7 @@ package org.apache.logging.log4j.internal.util;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
@@ -27,6 +28,10 @@ import javax.xml.parsers.SAXParserFactory;
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 /**
  * A SAX2-based XML reader.
@@ -64,6 +69,29 @@ public final class XmlReader {
         return document;
     }
 
+    public static Stream<Element> childElementsMatchingName(final Element 
parentElement, final String childElementName) {
+        final NodeList childNodes = parentElement.getChildNodes();
+        return IntStream
+                .range(0, childNodes.getLength())
+                .mapToObj(childNodes::item)
+                .filter(childNode -> childNode.getNodeType() == 
Node.ELEMENT_NODE && childElementName.equals(childNode.getNodeName()))
+                .map(childNode -> (Element) childNode);
+    }
+
+    public static Element childElementMatchingName(final Element 
parentElement, final String childElementName) {
+        final List<Element> childElements = 
childElementsMatchingName(parentElement, childElementName)
+                .collect(Collectors.toList());
+        final int childElementCount = childElements.size();
+        if (childElementCount != 1) {
+            throw failureAtXmlNode(
+                    parentElement,
+                    "was expecting a single `%s` element, found: %d",
+                    childElementName,
+                    childElementCount);
+        }
+        return childElements.get(0);
+    }
+
     public static String requireAttribute(final Element element, final String 
attributeName) {
         if (!element.hasAttribute(attributeName)) {
             throw failureAtXmlNode(element, "missing attribute: `%s`", 
attributeName);
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
index fb2bac38d2..f04f2a59a4 100644
--- 
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
@@ -19,13 +19,11 @@ 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;
 
@@ -165,12 +163,9 @@ public final class ChangelogEntry {
         }
 
         // 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 List<Issue> issues = XmlReader
+                .childElementsMatchingName(entryElement, "issue")
+                .map(issueElement -> {
                     final String issueId = 
XmlReader.requireAttribute(issueElement, "id");
                     final String issueLink = 
XmlReader.requireAttribute(issueElement, "link");
                     return new Issue(issueId, issueLink);
@@ -178,15 +173,9 @@ public final class ChangelogEntry {
                 .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 List<Author> authors = XmlReader
+                .childElementsMatchingName(entryElement, "author")
+                .map(authorElement -> {
                     final String authorId = authorElement.hasAttribute("id")
                             ? authorElement.getAttribute("id")
                             : null;
@@ -196,15 +185,12 @@ public final class ChangelogEntry {
                     return new Author(authorId, authorName);
                 })
                 .collect(Collectors.toList());
+        if (authors.isEmpty()) {
+            throw XmlReader.failureAtXmlNode(entryElement, "no `author` 
elements found");
+        }
 
         // 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 Element descriptionElement = 
XmlReader.childElementMatchingName(entryElement, "description");
         final String descriptionFormat = 
XmlReader.requireAttribute(descriptionElement, "format");
         final String descriptionText = 
trimNullable(descriptionElement.getTextContent());
         final Description description = new Description(descriptionFormat, 
descriptionText);
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
index fc0b05aab6..92a2b94bef 100644
--- 
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
@@ -26,6 +26,28 @@ public final class ChangelogFiles {
         return projectRootDirectory.resolve("changelog");
     }
 
+    public static Path unreleasedDirectory(final Path projectRootDirectory) {
+        return changelogDirectory(projectRootDirectory).resolve(".unreleased");
+    }
+
+    public static Path releaseDirectory(
+            final Path projectRootDirectory,
+            final String releaseDate,
+            final String releaseVersion) {
+        if (!releaseDate.matches("^\\d{8}$")) {
+            final String message = String.format(
+                    "was expecting release date to be formatted as `YYYYmmdd`, 
found: `%s`", releaseDate);
+            throw new IllegalArgumentException(message);
+        }
+        if (!releaseVersion.matches("^\\d+\\.\\d+\\.\\d+$")) {
+            final String message = String.format(
+                    "was expecting release version to be formatted as `0.1.2`, 
found: `%s`", releaseVersion);
+            throw new IllegalArgumentException(message);
+        }
+        final String releaseDirectoryName = String.format("%s-%s", 
releaseDate, releaseVersion);
+        return 
changelogDirectory(projectRootDirectory).resolve(releaseDirectoryName);
+    }
+
     public static Path releaseXmlFile(final Path releaseDirectory) {
         return releaseDirectory.resolve(".release.xml");
     }
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
index 3311fe78b7..380c6f8f57 100644
--- 
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
@@ -16,6 +16,7 @@
  */
 package org.apache.logging.log4j.internal.util.changelog.exporter;
 
+import org.apache.logging.log4j.internal.util.AsciiDocUtils;
 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;
@@ -48,23 +49,6 @@ public final class AsciiDocExporter {
         }
     });
 
-    private static final String LICENSE_HEADER_ASCIIDOC = "////\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" +
-            "         https://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" +
-            "////\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" +
@@ -80,6 +64,7 @@ public final class AsciiDocExporter {
         // Find release directories.
         final Path changelogDirectory = 
ChangelogFiles.changelogDirectory(args.projectRootDirectory);
         final List<Path> releaseDirectories = 
findAdjacentFiles(changelogDirectory)
+                .filter(file -> file.toFile().isDirectory())
                 .sorted()
                 .collect(Collectors.toList());
         final int releaseDirectoryCount = releaseDirectories.size();
@@ -121,6 +106,9 @@ public final class AsciiDocExporter {
 
         }
 
+        // Export unreleased.
+        exportUnreleased(args.projectRootDirectory);
+
         // Export the release index.
         exportReleaseIndex(args.projectRootDirectory, changelogReleases);
 
@@ -164,24 +152,14 @@ public final class AsciiDocExporter {
             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.
+        final List<ChangelogEntry> changelogEntries = 
readChangelogEntries(releaseDirectory);
         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) {
@@ -218,6 +196,13 @@ public final class AsciiDocExporter {
 
     }
 
+    private static List<ChangelogEntry> readChangelogEntries(final Path 
releaseDirectory) {
+        return findAdjacentFiles(releaseDirectory)
+                .sorted(FILE_MODIFICATION_TIME_COMPARATOR)
+                .map(ChangelogEntry::readFromXmlFile)
+                .collect(Collectors.toList());
+    }
+
     private static void exportRelease(
             final Path projectRootDirectory,
             final ChangelogRelease release,
@@ -242,17 +227,24 @@ public final class AsciiDocExporter {
         // Write the header.
         final StringBuilder stringBuilder = new StringBuilder();
         stringBuilder
-                .append(LICENSE_HEADER_ASCIIDOC)
+                .append(AsciiDocUtils.LICENSE_COMMENT_BLOCK)
                 .append('\n')
                 .append(AUTO_GENERATION_WARNING_ASCIIDOC)
                 .append('\n')
-                .append("= ")
-                .append(release.version)
-                .append(" (")
-                .append(release.date)
-                .append(")\n")
-                .append(introAsciiDoc)
-                .append("\n");
+                .append("= ");
+        if (release.version != null) {
+            stringBuilder
+                    .append(release.version)
+                    .append(" (")
+                    .append(release.date)
+                    .append(")\n")
+                    .append(introAsciiDoc)
+                    .append("\n");
+        } else {
+            stringBuilder
+                    .append("Unreleased\n\n")
+                    .append("Changes staged for the next version that is yet 
to be released.\n\n");
+        }
 
         if (!entries.isEmpty()) {
 
@@ -376,12 +368,24 @@ public final class AsciiDocExporter {
         }
     }
 
+    private static void exportUnreleased(final Path projectRootDirectory) {
+        final Path unreleasedDirectory = 
ChangelogFiles.unreleasedDirectory(projectRootDirectory);
+        final List<ChangelogEntry> changelogEntries = 
readChangelogEntries(unreleasedDirectory);
+        final ChangelogRelease changelogRelease = new ChangelogRelease(null, 
null);
+        try {
+            exportRelease(projectRootDirectory, changelogRelease, null, 
changelogEntries);
+        } catch (final IOException error) {
+            throw new UncheckedIOException("failed exporting unreleased 
changes", error);
+        }
+    }
+
     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");
+        System.out.format("exporting release index to `%s`%n", asciiDocFile);
         try {
             Files.write(asciiDocFile, asciiDocBytes);
         } catch (final IOException error) {
@@ -392,10 +396,11 @@ public final class AsciiDocExporter {
     private static String exportReleaseIndexToAsciiDoc(final 
List<ChangelogRelease> changelogReleases) {
         final StringBuilder stringBuilder = new StringBuilder();
         stringBuilder
-                .append(LICENSE_HEADER_ASCIIDOC)
+                .append(AsciiDocUtils.LICENSE_COMMENT_BLOCK)
                 .append('\n')
                 .append(AUTO_GENERATION_WARNING_ASCIIDOC)
-                .append("\n= Release changelogs\n\n");
+                .append("\n= Release changelogs\n\n")
+                .append("* [????-??-??] xref:unreleased.adoc[Unreleased]\n");
         for (int releaseIndex = changelogReleases.size() - 1; releaseIndex >= 
0; releaseIndex--) {
             final ChangelogRelease changelogRelease = 
changelogReleases.get(releaseIndex);
             final String asciiDocFilename = 
changelogReleaseAsciiDocFilename(changelogRelease);
@@ -410,8 +415,10 @@ public final class AsciiDocExporter {
     }
 
     private static String changelogReleaseAsciiDocFilename(final 
ChangelogRelease changelogRelease) {
-        // Using only the version (that is, avoiding the date) in the filename 
so that one can determine the link to the changelog of a particular release 
with only version information.
-        return String.format("%s.adoc", changelogRelease.version);
+        return changelogRelease.version == null
+                ? "unreleased.adoc"
+                // Using only the version (that is, avoiding the date) in the 
filename so that one can determine the link to the changelog of a particular 
release with only version information.
+                : String.format("%s.adoc", changelogRelease.version);
     }
 
 }
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
index d107fcc314..5af2afd0b0 100644
--- 
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
@@ -46,13 +46,7 @@ final class MavenChanges {
         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);
+        final Element bodyElement = 
XmlReader.childElementMatchingName(documentElement, "body");
 
         // Read releases.
         final List<Release> releases = new ArrayList<>();
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
index 660bc4a516..15d16901db 100644
--- 
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
@@ -52,9 +52,10 @@ public final class MavenChangesImporter {
     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));
+        final Path releaseDirectory = ChangelogFiles.releaseDirectory(
+                projectRootDirectory,
+                release.date.replaceAll("-", ""),
+                release.version);
 
         // Write release information.
         final Path releaseFile = 
ChangelogFiles.releaseXmlFile(releaseDirectory);
diff --git 
a/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/releaser/ChangelogReleaser.java
 
b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/releaser/ChangelogReleaser.java
new file mode 100644
index 0000000000..5865e5970c
--- /dev/null
+++ 
b/log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/releaser/ChangelogReleaser.java
@@ -0,0 +1,93 @@
+/*
+ * 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
+ *
+ *      https://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.releaser;
+
+import org.apache.logging.log4j.internal.util.AsciiDocUtils;
+import org.apache.logging.log4j.internal.util.XmlReader;
+import org.apache.logging.log4j.internal.util.changelog.ChangelogFiles;
+import org.apache.logging.log4j.internal.util.changelog.ChangelogRelease;
+import org.w3c.dom.Element;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDate;
+
+import static java.time.format.DateTimeFormatter.BASIC_ISO_DATE;
+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.changelog.ChangelogFiles.releaseDirectory;
+
+public final class ChangelogReleaser {
+
+    private ChangelogReleaser() {}
+
+    public static void main(final String[] mainArgs) throws Exception {
+
+        // Read arguments.
+        final ChangelogReleaserArgs args = 
ChangelogReleaserArgs.fromMainArgs(mainArgs);
+
+        // Read the release date and version.
+        final String releaseDate = BASIC_ISO_DATE.format(LocalDate.now());
+        final String releaseVersion = 
readReleaseVersion(args.projectRootDirectory);
+        System.out.format(
+                "using `%s` and `%s` for release date and version, 
respectively%n",
+                releaseDate, releaseVersion);
+
+        // Move unreleased directory to a release directory.
+        final Path releaseDirectory = 
releaseDirectory(args.projectRootDirectory, releaseDate, releaseVersion);
+        final Path unreleasedDirectory = 
ChangelogFiles.unreleasedDirectory(args.projectRootDirectory);
+        if (!Files.exists(unreleasedDirectory)) {
+            final String message = String.format(
+                    "`%s` does not exist! A release without any changelogs 
don't make sense!",
+                    unreleasedDirectory);
+            throw new IllegalStateException(message);
+        }
+        System.out.format("moving `%s` to `%s`%n", unreleasedDirectory, 
releaseDirectory);
+        Files.move(unreleasedDirectory, releaseDirectory);
+        Files.createDirectories(unreleasedDirectory);
+
+        // Write the release information.
+        final Path releaseXmlFile = 
ChangelogFiles.releaseXmlFile(releaseDirectory);
+        System.out.format("writing release information to `%s`%n", 
releaseXmlFile);
+        final ChangelogRelease changelogRelease = new 
ChangelogRelease(releaseVersion, releaseDate);
+        changelogRelease.writeToXmlFile(releaseXmlFile);
+
+        // Write the release introduction.
+        final Path introAsciiDocFile = 
ChangelogFiles.introAsciiDocFile(releaseDirectory);
+        Files.write(introAsciiDocFile, 
AsciiDocUtils.LICENSE_COMMENT_BLOCK.getBytes(StandardCharsets.UTF_8));
+        System.out.format("created intro file at `%s`%n", introAsciiDocFile);
+
+    }
+
+    private static String readReleaseVersion(final Path projectRootDirectory) {
+
+        // Read the root `project` element.
+        final Path rootPomFile = projectRootDirectory.resolve("pom.xml");
+        final Element projectElement = 
XmlReader.readXmlFileRootElement(rootPomFile, "project");
+
+        // Read the `version` element.
+        final Element versionElement = 
XmlReader.childElementMatchingName(projectElement, "version");
+        final String version = trimNullable(versionElement.getTextContent());
+        if (isBlank(version)) {
+            throw XmlReader.failureAtXmlNode(versionElement, "blank `version`: 
%s", version);
+        }
+        return version;
+
+    }
+
+}
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/releaser/ChangelogReleaserArgs.java
similarity index 55%
copy from 
log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/ChangelogFiles.java
copy to 
log4j-internal-util/src/main/java/org/apache/logging/log4j/internal/util/changelog/releaser/ChangelogReleaserArgs.java
index fc0b05aab6..df89934424 100644
--- 
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/releaser/ChangelogReleaserArgs.java
@@ -14,24 +14,26 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-package org.apache.logging.log4j.internal.util.changelog;
+package org.apache.logging.log4j.internal.util.changelog.releaser;
 
 import java.nio.file.Path;
+import java.nio.file.Paths;
 
-public final class ChangelogFiles {
+final class ChangelogReleaserArgs {
 
-    private ChangelogFiles() {}
+    final Path projectRootDirectory;
 
-    public static Path changelogDirectory(final Path projectRootDirectory) {
-        return projectRootDirectory.resolve("changelog");
+    private ChangelogReleaserArgs(final Path projectRootDirectory) {
+        this.projectRootDirectory = projectRootDirectory;
     }
 
-    public static Path releaseXmlFile(final Path releaseDirectory) {
-        return releaseDirectory.resolve(".release.xml");
-    }
-
-    public static Path introAsciiDocFile(final Path releaseDirectory) {
-        return releaseDirectory.resolve(".intro.adoc");
+    static ChangelogReleaserArgs 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 ChangelogReleaserArgs(projectRootPath);
     }
 
 }
diff --git a/src/site/asciidoc/changelog/index.adoc 
b/src/site/asciidoc/changelog/index.adoc
index 1f5e926824..f695267463 100644
--- a/src/site/asciidoc/changelog/index.adoc
+++ b/src/site/asciidoc/changelog/index.adoc
@@ -22,6 +22,7 @@ This file is automatically generated from the release 
changelog directory!
 
 = Release changelogs
 
+* xref:unreleased.adoc[Unreleased]
 * [2022-09-09] xref:2.19.0.adoc[2.19.0]
 * [2022-06-28] xref:2.18.0.adoc[2.18.0]
 * [2022-02-23] xref:2.17.2.adoc[2.17.2]
diff --git a/src/site/asciidoc/changelog/unreleased.adoc 
b/src/site/asciidoc/changelog/unreleased.adoc
new file mode 100644
index 0000000000..20c6697be8
--- /dev/null
+++ b/src/site/asciidoc/changelog/unreleased.adoc
@@ -0,0 +1,35 @@
+////
+    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
+
+         https://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.
+////
+
+////
+*DO NOT EDIT THIS FILE!!*
+This file is automatically generated from the release changelog directory!
+////
+
+= Unreleased
+
+Changes staged for the next version that is yet to be released.
+
+== Changes
+
+=== Changed
+
+* Add LogEvent timestamp to ProducerRecord in KafkaAppender. (for 
https://issues.apache.org/jira/browse/LOG4J2-2678[LOG4J2-2678] by `pkarwasz`, 
Federico D'Ambrosio)
+
+=== Fixed
+
+* Fix `Configurator#setLevel` for internal classes. (for 
https://issues.apache.org/jira/browse/LOG4J2-3631[LOG4J2-3631] by `pkarwasz`, 
Jeff Thomas)

Reply via email to