From 9f6c29faf4a5205c6f41876a6b72c50f848bc8d3 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 6 Nov 2026 13:29:15 +0300 Subject: [PATCH 1/3] Preserve documentation site versions in PDF links --- commons/doc-maven-plugin/README.md | 57 +++++++++ .../doc/maven/AsciidocToPdfMojo.java | 62 ++++++++-- .../doc/maven/AsciidocToPdfMojoTest.java | 114 ++++++++++++++++++ 3 files changed, 222 insertions(+), 11 deletions(-) diff --git a/commons/doc-maven-plugin/README.md b/commons/doc-maven-plugin/README.md index c815c66b6..8f867fa55 100644 --- a/commons/doc-maven-plugin/README.md +++ b/commons/doc-maven-plugin/README.md @@ -757,3 +757,60 @@ Header, with the fields enclosed by brackets [] replaced by your own identifying information: "Portions copyright [year] [name of copyright owner]". Copyright 2012-2015 ForgeRock AS. + + +## Documentation site versions in AsciiDoc PDF links + +The `asciidoc-to-pdf` goal converts cross-guide and cross-component xrefs to +links on `siteUrl` (default `https://doc.openidentityplatform.org`). By default, +these links use the unversioned current documentation. To build a PDF for +archived documentation, configure the published site version paths explicitly: + +```xml + + openam + 15.2 + + 4.10 + + +``` + +`docVersion` is also available as the Maven property `-DdocVersion=15.2`. +Omit it for current documentation. `componentDocVersions` is a Maven map keyed +by Antora component name; configure it in the POM. Each value is the exact +version directory published by the site, not a release tag or Maven version. +For example, the site's `versions-from-tags.js` maps `OpenAM-15.2.2` to `15.2` +and `OpenDJ-4.10.2` to `4.10`; its branch sources have no version directory. +Neither `projectVersion` nor `releaseVersion` determines the site path. + +For `xref:reference:chap-auth-modules.adoc#data-store-module-ref[Data Store]`, +the configuration above produces +`https://doc.openidentityplatform.org/openam/15.2/reference/chap-auth-modules.html#data-store-module-ref`. +For `xref:opendj::index.adoc[OpenDJ]`, it produces +`https://doc.openidentityplatform.org/opendj/4.10/index.html`. +The `ROOT` module has no module directory. Relative cross-guide xrefs also use +the current component's configured version; fragments and same-guide xrefs are +preserved. Only the PDF source copy is rewritten; Antora sources are unchanged. + +Version selection, in order: + +1. An explicit Antora version in the xref, such as + `xref:4.9@opendj:install-guide:index.adoc[Install]`. +2. An entry for the target component in `componentDocVersions`. +3. `docVersion`, only when the target is this project's component. +4. The unversioned current documentation. + +An empty map entry or an explicit empty version (`xref:@opendj::index.adoc[Current]`) +selects the unversioned site. A component without an entry never inherits +another component's version. + +When a release moves from current to archived documentation, first publish and +verify its historical site paths, then rebuild its PDF from the immutable +release sources using the corresponding `docVersion` and independently chosen +component versions. For a POM with the AsciiDoc preprocessing and PDF goals +bound to the site lifecycle, use `mvn -DdocVersion=15.2 site`; otherwise run the +project's normal PDF build with that property. Replace the archived PDF with +the rebuilt artifact. Previously distributed PDFs retain their original links; +this converter cannot update them. Do not select a historical version path +before that version is published on the site. diff --git a/commons/doc-maven-plugin/src/main/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojo.java b/commons/doc-maven-plugin/src/main/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojo.java index e4adcaea6..919e24fa9 100644 --- a/commons/doc-maven-plugin/src/main/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojo.java +++ b/commons/doc-maven-plugin/src/main/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojo.java @@ -11,8 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024-2026 3A Systems LLC. - * Portions Copyright 2026 3A Systems, LLC. + * Copyright 2024-2026 3A Systems, LLC. */ package org.openidentityplatform.doc.maven; @@ -35,8 +34,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; import java.util.HashSet; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -79,6 +80,21 @@ public AsciidocToPdfMojo() { @Parameter(property = "antoraComponent") private String antoraComponent; + /** + * Site version path for this component, such as {@code 15.2}. Empty means the + * unversioned current documentation. This is independent of the Maven project version. + */ + @Parameter(property = "docVersion") + private String docVersion; + + /** + * Site version paths by Antora component name. Entries override {@code docVersion} + * for that component; other components never inherit this project's version. + * An empty entry selects the unversioned current documentation. + */ + @Parameter + private Map componentDocVersions = Collections.emptyMap(); + public File getPdfOutputDirectory() { return new File(buildDirectory, "/pdf"); } @@ -93,7 +109,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { String component = antoraComponent != null && !antoraComponent.isEmpty() ? antoraComponent : projectName.toLowerCase(Locale.ROOT); try { - preparePdfSource(getAsciidocBuildSourceDirectory(), getPdfSourceDirectory(), siteUrl, component); + preparePdfSource(getAsciidocBuildSourceDirectory(), getPdfSourceDirectory(), siteUrl, component, + docVersion, componentDocVersions); } catch (IOException e) { throw new MojoExecutionException("error preparing PDF sources", e); } @@ -151,6 +168,11 @@ public void execute() throws MojoExecutionException, MojoFailureException { * so that the sources the {@code antora} goal reads stay as they are. */ static void preparePdfSource(File source, File target, String siteUrl, String component) throws IOException { + preparePdfSource(source, target, siteUrl, component, null, Collections.emptyMap()); + } + + static void preparePdfSource(File source, File target, String siteUrl, String component, + String docVersion, Map componentDocVersions) throws IOException { if (target.exists()) { FileUtils.deleteDirectory(target); } @@ -158,7 +180,7 @@ static void preparePdfSource(File source, File target, String siteUrl, String co for (File adocFile : FileUtils.listFiles(target, new String[] {"adoc"}, true)) { String module = adocFile.getParentFile().getName(); String adoc = FileUtils.readFileToString(adocFile, StandardCharsets.UTF_8); - String converted = convertXrefsForPdf(adoc, siteUrl, component, module); + String converted = convertXrefsForPdf(adoc, siteUrl, component, module, docVersion, componentDocVersions); if (!converted.equals(adoc)) { FileUtils.writeStringToFile(adocFile, converted, StandardCharsets.UTF_8); } @@ -166,7 +188,7 @@ static void preparePdfSource(File source, File target, String siteUrl, String co } private static final Pattern XREF_PATTERN = Pattern.compile("xref:([^\\[\\s]+)\\["); - private static final Pattern RESOURCE_ID_PATTERN = Pattern.compile("(?:([^:/@$]+):)?([^:/@$]*):([^:@$]+)"); + private static final Pattern RESOURCE_ID_PATTERN = Pattern.compile("(?:([^:/@$]*)@)?(?:([^:/@$]+):)?([^:/@$]*):([^:@$]+)"); /** * Plain Asciidoctor knows neither the other guides nor the other Antora components, so it @@ -176,27 +198,37 @@ static void preparePdfSource(File source, File target, String siteUrl, String co *
  • {@code xref:component:module:page.adoc#anchor[text]} and {@code xref:module:page.adoc#anchor[text]}
  • *
  • {@code xref:../module/page.adoc#anchor[text]}
  • * + * Explicit {@code version@component:module:page.adoc} resource IDs override configured + * component versions; an empty explicit version selects the unversioned site. * A relative xref into {@code currentModule} becomes a same-guide xref. Other xrefs are left as they are. */ static String convertXrefsForPdf(String adoc, String siteUrl, String component, String currentModule) { + return convertXrefsForPdf(adoc, siteUrl, component, currentModule, null, Collections.emptyMap()); + } + + static String convertXrefsForPdf(String adoc, String siteUrl, String component, String currentModule, + String docVersion, Map componentDocVersions) { String site = siteUrl.replaceAll("/+$", ""); Matcher m = XREF_PATTERN.matcher(adoc); StringBuilder builder = new StringBuilder(); int i = 0; while (m.find()) { builder.append(adoc, i, m.start()); - builder.append(convertXrefForPdf(m.group(1), site, component, currentModule)).append("["); + builder.append(convertXrefForPdf(m.group(1), site, component, currentModule, + docVersion, componentDocVersions)).append("["); i = m.end(); } builder.append(adoc.substring(i)); return builder.toString(); } - private static String convertXrefForPdf(String target, String site, String component, String currentModule) { + private static String convertXrefForPdf(String target, String site, String component, String currentModule, + String docVersion, Map componentDocVersions) { int hash = target.indexOf('#'); String path = hash < 0 ? target : target.substring(0, hash); String fragment = hash < 0 ? "" : target.substring(hash); + String explicitVersion = null; String targetComponent; String module; String page; @@ -217,16 +249,24 @@ private static String convertXrefForPdf(String target, String site, String compo if (!id.matches()) { return "xref:" + target; } - targetComponent = id.group(1) != null ? id.group(1) : component; - module = id.group(2).isEmpty() ? "ROOT" : id.group(2); - page = id.group(3); + explicitVersion = id.group(1); + targetComponent = id.group(2) != null ? id.group(2) : component; + module = id.group(3).isEmpty() ? "ROOT" : id.group(3); + page = id.group(4); } if (page.isEmpty()) { return "xref:" + target; } + String version = explicitVersion; + if (version == null) { + version = componentDocVersions.containsKey(targetComponent) + ? componentDocVersions.get(targetComponent) + : targetComponent.equals(component) ? docVersion : null; + } + String versionPrefix = version == null || version.isEmpty() ? "" : version + "/"; page = page.replaceAll("\\.adoc$", ""); // Antora publishes the pages of the ROOT module directly under the component String modulePrefix = module.equals("ROOT") ? "" : module + "/"; - return "link:" + site + "/" + targetComponent + "/" + modulePrefix + page + ".html" + fragment; + return "link:" + site + "/" + targetComponent + "/" + versionPrefix + modulePrefix + page + ".html" + fragment; } } diff --git a/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoTest.java b/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoTest.java index ca7a21140..f1918230c 100644 --- a/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoTest.java +++ b/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoTest.java @@ -22,7 +22,12 @@ import org.junit.rules.TemporaryFolder; import java.io.File; +import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @@ -37,6 +42,115 @@ private static String convert(String adoc) { return AsciidocToPdfMojo.convertXrefsForPdf(adoc, SITE, "openidm", "getting-started"); } + @Test + public void testExplicitVersionedComponentXref() { + assertThat(convert("xref:4.8@opendj:admin-guide:chap-replication.adoc#replication[Replication]")) + .isEqualTo("link:" + SITE + "/opendj/4.8/admin-guide/chap-replication.html#replication[Replication]"); + } + + @Test + public void testHistoricalCrossGuideLinksPreserveFragments() { + String adoc = "xref:reference:chap-auth-modules.adoc#data-store-module-ref[Data Store] " + + "xref:../reference/chap-auth-modules.adoc#data-store-module-ref[Data Store]"; + assertThat(AsciidocToPdfMojo.convertXrefsForPdf(adoc, SITE, "openam", "how-to", "15.2", + Collections.emptyMap())).isEqualTo( + "link:" + SITE + "/openam/15.2/reference/chap-auth-modules.html#data-store-module-ref[Data Store] " + + "link:" + SITE + "/openam/15.2/reference/chap-auth-modules.html#data-store-module-ref[Data Store]"); + } + + @Test + public void testOtherComponentDoesNotInheritProjectVersion() { + assertThat(AsciidocToPdfMojo.convertXrefsForPdf("xref:opendj::index.adoc#overview[OpenDJ]", + SITE, "openam", "how-to", "15.2", Collections.emptyMap())) + .isEqualTo("link:" + SITE + "/opendj/index.html#overview[OpenDJ]"); + } + + @Test + public void testComponentVersionsAreIndependentAndRootHasNoModuleSegment() { + Map versions = Collections.singletonMap("opendj", "4.8"); + assertThat(AsciidocToPdfMojo.convertXrefsForPdf( + "xref:opendj::index.adoc#overview[OpenDJ] xref:openam:ROOT:index.adoc[OpenAM] " + + "xref:opendj:install-guide:index.adoc[Install]", + SITE, "openam", "how-to", "15.2", versions)).isEqualTo( + "link:" + SITE + "/opendj/4.8/index.html#overview[OpenDJ] " + + "link:" + SITE + "/openam/15.2/index.html[OpenAM] " + + "link:" + SITE + "/opendj/4.8/install-guide/index.html[Install]"); + } + + @Test + public void testExplicitVersionOverridesConfiguredComponentVersion() { + Map versions = new HashMap<>(); + versions.put("opendj", "4.8"); + versions.put("openam", "15.1"); + assertThat(AsciidocToPdfMojo.convertXrefsForPdf( + "xref:4.7@opendj:ROOT:index.adoc[Older] xref:15.0@reference:page.adoc#a[Reference] " + + "xref:reference:page.adoc[Configured] xref:@opendj::index.adoc[Current]", + SITE, "openam", "how-to", "15.2", versions)).isEqualTo( + "link:" + SITE + "/opendj/4.7/index.html[Older] " + + "link:" + SITE + "/openam/15.0/reference/page.html#a[Reference] " + + "link:" + SITE + "/openam/15.1/reference/page.html[Configured] " + + "link:" + SITE + "/opendj/index.html[Current]"); + } + + @Test + public void testHistoricalSameGuideLinksStayInternal() { + String adoc = "xref:../how-to/page.adoc#a[A] xref:#local[Local] xref:page.adoc[Page]"; + assertThat(AsciidocToPdfMojo.convertXrefsForPdf(adoc, SITE, "openam", "how-to", "15.2", + Collections.emptyMap())).isEqualTo("xref:page.adoc#a[A] xref:#local[Local] xref:page.adoc[Page]"); + } + + @Test + public void testHistoricalPreparePdfSourceUsesConfiguredVersions() throws Exception { + File source = tmp.newFolder("historical-source"); + File chapter = new File(source, "how-to/page.adoc"); + String original = "xref:reference:chap-auth-modules.adoc#data-store-module-ref[Data Store] " + + "xref:opendj::index.adoc[OpenDJ] xref:#local[Local]\n"; + FileUtils.writeStringToFile(chapter, original, StandardCharsets.UTF_8); + File target = new File(tmp.getRoot(), "historical-pdf-source"); + FileUtils.writeStringToFile(new File(target, "stale.adoc"), "obsolete", StandardCharsets.UTF_8); + + AsciidocToPdfMojo.preparePdfSource(source, target, SITE + "/", "openam", "15.2", + Collections.singletonMap("opendj", "4.8")); + + assertThat(FileUtils.readFileToString(chapter, StandardCharsets.UTF_8)).isEqualTo(original); + assertThat(new File(target, "how-to/page.adoc")) + .hasContent("link:" + SITE + + "/openam/15.2/reference/chap-auth-modules.html#data-store-module-ref[Data Store] " + + "link:" + SITE + "/opendj/4.8/index.html[OpenDJ] xref:#local[Local]"); + assertThat(new File(target, "stale.adoc")).doesNotExist(); + } + + @Test + public void testExecutePassesDocumentationVersionConfigurationToSourcePreparation() throws Exception { + AsciidocToPdfMojo mojo = new AsciidocToPdfMojo() { + @Override + protected List getDocuments() { + return Collections.singletonList("another-guide"); + } + }; + mojo.buildDirectory = tmp.newFolder("execute-build"); + mojo.projectName = "OpenAM"; + setParameter(mojo, "siteUrl", SITE); + setParameter(mojo, "docVersion", "15.2"); + setParameter(mojo, "componentDocVersions", Collections.singletonMap("opendj", "4.10")); + FileUtils.writeStringToFile(new File(mojo.getAsciidocBuildSourceDirectory(), "how-to/page.adoc"), + "xref:reference:chap-auth-modules.adoc#data-store-module-ref[Data Store] " + + "xref:opendj::index.adoc[OpenDJ]", StandardCharsets.UTF_8); + + mojo.execute(); + + assertThat(new File(mojo.getPdfSourceDirectory(), "how-to/page.adoc")) + .hasContent("link:" + SITE + + "/openam/15.2/reference/chap-auth-modules.html#data-store-module-ref[Data Store] " + + "link:" + SITE + "/opendj/4.10/index.html[OpenDJ]"); + } + + private static void setParameter(AsciidocToPdfMojo mojo, String name, Object value) throws Exception { + Field parameter = AsciidocToPdfMojo.class.getDeclaredField(name); + parameter.setAccessible(true); + parameter.set(mojo, value); + } + @Test public void testCrossComponentXref() { assertThat(convert("see xref:opendj:install-guide:index.adoc[Install Guide].")) From 092fc3ef2fe52d4e47d15b6454e74a2171cb3a17 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 6 Nov 2026 14:56:49 +0300 Subject: [PATCH 2/3] Fix Antora version selection in PDF cross-references --- commons/doc-maven-plugin/README.md | 48 +++++---- .../doc/maven/AsciidocToPdfMojo.java | 10 +- .../AsciidocToPdfMojoConfigurationTest.java | 98 +++++++++++++++++++ .../doc/maven/AsciidocToPdfMojoTest.java | 39 +++++++- .../test/resources/asciidoc-to-pdf/pom.xml | 43 ++++++++ 5 files changed, 214 insertions(+), 24 deletions(-) create mode 100644 commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoConfigurationTest.java create mode 100644 commons/doc-maven-plugin/src/test/resources/asciidoc-to-pdf/pom.xml diff --git a/commons/doc-maven-plugin/README.md b/commons/doc-maven-plugin/README.md index 8f867fa55..5cdb37cfd 100644 --- a/commons/doc-maven-plugin/README.md +++ b/commons/doc-maven-plugin/README.md @@ -742,24 +742,7 @@ The following example shows the full configuration to use the 2.1.3 version. If you need to create your own shared content artifact, see the `forgerock-doc-common-content` project as an example. -* * * - -The contents of this file are subject to the terms of the Common Development and -Distribution License (the License). You may not use this file except in compliance with the -License. - -You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the -specific language governing permission and limitations under the License. - -When distributing Covered Software, include this CDDL Header Notice in each file and include -the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL -Header, with the fields enclosed by brackets [] replaced by your own identifying -information: "Portions copyright [year] [name of copyright owner]". - -Copyright 2012-2015 ForgeRock AS. - - -## Documentation site versions in AsciiDoc PDF links +# Documentation site versions in AsciiDoc PDF links The `asciidoc-to-pdf` goal converts cross-guide and cross-component xrefs to links on `siteUrl` (default `https://doc.openidentityplatform.org`). By default, @@ -795,16 +778,22 @@ preserved. Only the PDF source copy is rewritten; Antora sources are unchanged. Version selection, in order: -1. An explicit Antora version in the xref, such as +1. An explicit Antora version in an xref that names a module, such as `xref:4.9@opendj:install-guide:index.adoc[Install]`. 2. An entry for the target component in `componentDocVersions`. 3. `docVersion`, only when the target is this project's component. 4. The unversioned current documentation. -An empty map entry or an explicit empty version (`xref:@opendj::index.adoc[Current]`) -selects the unversioned site. A component without an entry never inherits +An empty map entry or Antora's versionless token `_` (`xref:_@opendj::index.adoc[Current]`) +selects the unversioned site. The empty version form `xref:@opendj::index.adoc[Current]` +is not a valid Antora resource ID and is not converted. A component without an entry never inherits another component's version. +Version-only resource IDs such as `xref:15.1@page.adoc[Older]` are left unchanged. +To generate a site link in the PDF, name the target module explicitly, for example +`xref:15.1@how-to:page.adoc[Older]`. This also avoids depending on the including +page's module when the xref occurs in a shared partial. + When a release moves from current to archived documentation, first publish and verify its historical site paths, then rebuild its PDF from the immutable release sources using the corresponding `docVersion` and independently chosen @@ -814,3 +803,20 @@ project's normal PDF build with that property. Replace the archived PDF with the rebuilt artifact. Previously distributed PDFs retain their original links; this converter cannot update them. Do not select a historical version path before that version is published on the site. + +* * * + +The contents of this file are subject to the terms of the Common Development and +Distribution License (the License). You may not use this file except in compliance with the +License. + +You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the +specific language governing permission and limitations under the License. + +When distributing Covered Software, include this CDDL Header Notice in each file and include +the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL +Header, with the fields enclosed by brackets [] replaced by your own identifying +information: "Portions copyright [year] [name of copyright owner]". + +Copyright 2012-2015 ForgeRock AS. +Portions copyright 2026 3A Systems, LLC. diff --git a/commons/doc-maven-plugin/src/main/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojo.java b/commons/doc-maven-plugin/src/main/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojo.java index 919e24fa9..6bfae7031 100644 --- a/commons/doc-maven-plugin/src/main/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojo.java +++ b/commons/doc-maven-plugin/src/main/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojo.java @@ -188,7 +188,8 @@ static void preparePdfSource(File source, File target, String siteUrl, String co } private static final Pattern XREF_PATTERN = Pattern.compile("xref:([^\\[\\s]+)\\["); - private static final Pattern RESOURCE_ID_PATTERN = Pattern.compile("(?:([^:/@$]*)@)?(?:([^:/@$]+):)?([^:/@$]*):([^:@$]+)"); + private static final Pattern RESOURCE_ID_PATTERN = + Pattern.compile("(?:([^:/@$]+)@)?(?:([^:/@$]+):)?([^:/@$]*):([^:@$]+)"); /** * Plain Asciidoctor knows neither the other guides nor the other Antora components, so it @@ -199,7 +200,8 @@ static void preparePdfSource(File source, File target, String siteUrl, String co *
  • {@code xref:../module/page.adoc#anchor[text]}
  • * * Explicit {@code version@component:module:page.adoc} resource IDs override configured - * component versions; an empty explicit version selects the unversioned site. + * component versions; an explicit {@code _} version selects the unversioned site. + * Resource IDs without a module coordinate, such as {@code version@page.adoc}, are left unchanged. * A relative xref into {@code currentModule} becomes a same-guide xref. Other xrefs are left as they are. */ static String convertXrefsForPdf(String adoc, String siteUrl, String component, String currentModule) { @@ -250,6 +252,10 @@ private static String convertXrefForPdf(String target, String site, String compo return "xref:" + target; } explicitVersion = id.group(1); + // Antora uses _ to explicitly select the versionless component version. + if ("_".equals(explicitVersion)) { + explicitVersion = ""; + } targetComponent = id.group(2) != null ? id.group(2) : component; module = id.group(3).isEmpty() ? "ROOT" : id.group(3); page = id.group(4); diff --git a/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoConfigurationTest.java b/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoConfigurationTest.java new file mode 100644 index 000000000..95a3a337d --- /dev/null +++ b/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoConfigurationTest.java @@ -0,0 +1,98 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ + +package org.openidentityplatform.doc.maven; + +import org.apache.commons.io.FileUtils; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.plugin.testing.AbstractMojoTestCase; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.xml.Xpp3Dom; + +import java.io.File; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AsciidocToPdfMojoConfigurationTest extends AbstractMojoTestCase { + + private File buildDirectory; + + @Override + protected void setUp() throws Exception { + super.setUp(); + buildDirectory = Files.createTempDirectory("pdf-configuration").toFile(); + } + + @Override + protected void tearDown() throws Exception { + try { + FileUtils.deleteDirectory(buildDirectory); + } finally { + super.tearDown(); + } + } + + public void testEmptyPomEntryOverridesProjectVersion() throws Exception { + MavenProject project = readProject(); + AsciidocToPdfMojo mojo = (AsciidocToPdfMojo) lookupConfiguredMojo(project, "asciidoc-to-pdf"); + assertPreparedLinks(mojo, ""); + } + + public void testPomVersionsReachPreparedPdfSource() throws Exception { + MavenProject project = readProject(); + Xpp3Dom configuration = (Xpp3Dom) project.getBuildPlugins().get(0).getConfiguration(); + Xpp3Dom versions = configuration.getChild("componentDocVersions"); + versions.removeChild(versions.getChild("openam")); + AsciidocToPdfMojo mojo = (AsciidocToPdfMojo) lookupConfiguredMojo(project, "asciidoc-to-pdf"); + assertPreparedLinks(mojo, "15.2/"); + } + + public void testMavenUserPropertySelectsHistoricalVersion() throws Exception { + MavenProject project = readProject(); + Xpp3Dom configuration = (Xpp3Dom) project.getBuildPlugins().get(0).getConfiguration(); + configuration.removeChild(configuration.getChild("docVersion")); + Xpp3Dom versions = configuration.getChild("componentDocVersions"); + versions.removeChild(versions.getChild("openam")); + MavenSession session = newMavenSession(project); + session.getUserProperties().setProperty("docVersion", "15.2"); + AsciidocToPdfMojo mojo = (AsciidocToPdfMojo) lookupConfiguredMojo(session, + newMojoExecution("asciidoc-to-pdf")); + assertPreparedLinks(mojo, "15.2/"); + } + + private MavenProject readProject() throws Exception { + File pom = getTestFile("src/test/resources/asciidoc-to-pdf/pom.xml"); + try (Reader reader = Files.newBufferedReader(pom.toPath(), StandardCharsets.UTF_8)) { + MavenProject project = new MavenProject(new MavenXpp3Reader().read(reader)); + project.setFile(pom); + project.getBuild().setDirectory(buildDirectory.getAbsolutePath()); + return project; + } + } + + private void assertPreparedLinks(AsciidocToPdfMojo mojo, String versionPrefix) throws Exception { + FileUtils.writeStringToFile(new File(mojo.getAsciidocBuildSourceDirectory(), "how-to/page.adoc"), + "xref:reference:page.adoc#a[A] xref:opendj::index.adoc[B]", StandardCharsets.UTF_8); + mojo.execute(); + assertThat(new File(mojo.getPdfSourceDirectory(), "how-to/page.adoc")).hasContent( + "link:https://doc.openidentityplatform.org/openam/" + versionPrefix + "reference/page.html#a[A] " + + "link:https://doc.openidentityplatform.org/opendj/4.10/index.html[B]"); + } +} diff --git a/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoTest.java b/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoTest.java index f1918230c..30f0cde1b 100644 --- a/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoTest.java +++ b/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoTest.java @@ -84,7 +84,7 @@ public void testExplicitVersionOverridesConfiguredComponentVersion() { versions.put("openam", "15.1"); assertThat(AsciidocToPdfMojo.convertXrefsForPdf( "xref:4.7@opendj:ROOT:index.adoc[Older] xref:15.0@reference:page.adoc#a[Reference] " - + "xref:reference:page.adoc[Configured] xref:@opendj::index.adoc[Current]", + + "xref:reference:page.adoc[Configured] xref:_@opendj::index.adoc[Current]", SITE, "openam", "how-to", "15.2", versions)).isEqualTo( "link:" + SITE + "/opendj/4.7/index.html[Older] " + "link:" + SITE + "/openam/15.0/reference/page.html#a[Reference] " @@ -92,6 +92,43 @@ public void testExplicitVersionOverridesConfiguredComponentVersion() { + "link:" + SITE + "/opendj/index.html[Current]"); } + @Test + public void testEmptyExplicitVersionIsNotAnAntoraResourceId() { + String adoc = "xref:@opendj::index.adoc[Current] xref:@reference:page.adoc[A] xref:@page.adoc[B]"; + assertThat(AsciidocToPdfMojo.convertXrefsForPdf(adoc, SITE, "openam", "how-to", "15.2", + Collections.emptyMap())).isEqualTo(adoc); + } + + @Test + public void testExplicitVersionWithoutModuleIsUnchanged() { + String adoc = "xref:15.1@page.adoc#a[Older] xref:_@page.adoc#b[Current]"; + assertThat(AsciidocToPdfMojo.convertXrefsForPdf(adoc, SITE, "openam", "how-to", "15.2", + Collections.singletonMap("openam", "15.0"))).isEqualTo(adoc); + } + + @Test + public void testEmptyComponentEntriesOverrideProjectVersion() { + Map versions = new HashMap<>(); + for (String empty : new String[] {null, ""}) { + versions.put("openam", empty); + versions.put("opendj", empty); + assertThat(AsciidocToPdfMojo.convertXrefsForPdf( + "xref:reference:page.adoc[A] xref:opendj::index.adoc[B]", + SITE, "openam", "how-to", "15.2", versions)).isEqualTo( + "link:" + SITE + "/openam/reference/page.html[A] " + + "link:" + SITE + "/opendj/index.html[B]"); + } + } + + @Test + public void testConfiguredVersionsAreLiteralSitePaths() { + assertThat(AsciidocToPdfMojo.convertXrefsForPdf( + "xref:reference:page.adoc[A] xref:opendj::index.adoc[B]", + SITE, "openam", "how-to", "_", Collections.singletonMap("opendj", "_"))).isEqualTo( + "link:" + SITE + "/openam/_/reference/page.html[A] " + + "link:" + SITE + "/opendj/_/index.html[B]"); + } + @Test public void testHistoricalSameGuideLinksStayInternal() { String adoc = "xref:../how-to/page.adoc#a[A] xref:#local[Local] xref:page.adoc[Page]"; diff --git a/commons/doc-maven-plugin/src/test/resources/asciidoc-to-pdf/pom.xml b/commons/doc-maven-plugin/src/test/resources/asciidoc-to-pdf/pom.xml new file mode 100644 index 000000000..d5833d77d --- /dev/null +++ b/commons/doc-maven-plugin/src/test/resources/asciidoc-to-pdf/pom.xml @@ -0,0 +1,43 @@ + + + + + 4.0.0 + org.openidentityplatform.commons + pdf-configuration-test + 1.0.0 + pom + + + + org.openidentityplatform.commons + doc-maven-plugin + + OpenAM + 15.2 + + 4.10 + + + + another-guide + + + + + + From 4dd0ff22f9d8ea92fa4a52de1612a697be890575 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 6 Nov 2026 15:58:14 +0300 Subject: [PATCH 3/3] Strengthen PDF version configuration coverage and archive guidance --- commons/doc-maven-plugin/README.md | 18 +++++++++++++++-- .../AsciidocToPdfMojoConfigurationTest.java | 20 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/commons/doc-maven-plugin/README.md b/commons/doc-maven-plugin/README.md index 5cdb37cfd..b811a7495 100644 --- a/commons/doc-maven-plugin/README.md +++ b/commons/doc-maven-plugin/README.md @@ -759,7 +759,8 @@ archived documentation, configure the published site version paths explicitly: ``` -`docVersion` is also available as the Maven property `-DdocVersion=15.2`. +`docVersion` is also available as the Maven property `-DdocVersion=15.2`; a +`docVersion` project property, including one inherited from a parent POM, sets it too. Omit it for current documentation. `componentDocVersions` is a Maven map keyed by Antora component name; configure it in the POM. Each value is the exact version directory published by the site, not a release tag or Maven version. @@ -779,11 +780,17 @@ preserved. Only the PDF source copy is rewritten; Antora sources are unchanged. Version selection, in order: 1. An explicit Antora version in an xref that names a module, such as - `xref:4.9@opendj:install-guide:index.adoc[Install]`. + `xref:4.10@opendj:install-guide:index.adoc[Install]`. 2. An entry for the target component in `componentDocVersions`. 3. `docVersion`, only when the target is this project's component. 4. The unversioned current documentation. +Rule 3 also applies when the xref explicitly names this project's component, +such as `xref:openam:ROOT:index.adoc[OpenAM]` in an OpenAM PDF. With +`docVersion=15.2`, the PDF deliberately links to `/openam/15.2/index.html`. +Antora instead resolves a component-qualified xref without an explicit version +to the latest version of that component, so the HTML link can differ. + An empty map entry or Antora's versionless token `_` (`xref:_@opendj::index.adoc[Current]`) selects the unversioned site. The empty version form `xref:@opendj::index.adoc[Current]` is not a valid Antora resource ID and is not converted. A component without an entry never inherits @@ -794,6 +801,13 @@ To generate a site link in the PDF, name the target module explicitly, for examp `xref:15.1@how-to:page.adoc[Older]`. This also avoids depending on the including page's module when the xref occurs in a shared partial. +Rebuilding requires a doc-maven-plugin release that supports `docVersion` and +`componentDocVersions`; these options are unavailable in 3.2.2 and earlier. +Release POMs that pin the plugin version, such as OpenDJ 4.10.2 and 5.2.0 through +`${commons.version}`, must select a supporting plugin version for the rebuild. +Passing `-DdocVersion` alone does not upgrade the plugin; an older plugin ignores +the property and keeps unversioned links. + When a release moves from current to archived documentation, first publish and verify its historical site paths, then rebuild its PDF from the immutable release sources using the corresponding `docVersion` and independently chosen diff --git a/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoConfigurationTest.java b/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoConfigurationTest.java index 95a3a337d..31e6eadc7 100644 --- a/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoConfigurationTest.java +++ b/commons/doc-maven-plugin/src/test/java/org/openidentityplatform/doc/maven/AsciidocToPdfMojoConfigurationTest.java @@ -77,6 +77,26 @@ public void testMavenUserPropertySelectsHistoricalVersion() throws Exception { assertPreparedLinks(mojo, "15.2/"); } + public void testComponentDocVersionsIsADescriptorParameter() throws Exception { + assertTrue(newMojoExecution("asciidoc-to-pdf").getMojoDescriptor() + .getParameterMap().containsKey("componentDocVersions")); + } + + public void testAbsentMapKeepsOtherComponentsUnversioned() throws Exception { + MavenProject project = readProject(); + Xpp3Dom configuration = (Xpp3Dom) project.getBuildPlugins().get(0).getConfiguration(); + configuration.removeChild(configuration.getChild("componentDocVersions")); + AsciidocToPdfMojo mojo = (AsciidocToPdfMojo) lookupConfiguredMojo(project, "asciidoc-to-pdf"); + FileUtils.writeStringToFile(new File(mojo.getAsciidocBuildSourceDirectory(), "how-to/page.adoc"), + "xref:reference:page.adoc[A] xref:opendj::index.adoc[B]", StandardCharsets.UTF_8); + + mojo.execute(); + + assertThat(new File(mojo.getPdfSourceDirectory(), "how-to/page.adoc")).hasContent( + "link:https://doc.openidentityplatform.org/openam/15.2/reference/page.html[A] " + + "link:https://doc.openidentityplatform.org/opendj/index.html[B]"); + } + private MavenProject readProject() throws Exception { File pom = getTestFile("src/test/resources/asciidoc-to-pdf/pom.xml"); try (Reader reader = Files.newBufferedReader(pom.toPath(), StandardCharsets.UTF_8)) {