From a9eb94474ab4b8478da8d55e2d15dfb5bef2ab0c Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 01 Oct 2026 10:18:13 +0000
Subject: [PATCH] [#1131] Escape the option placeholders of the generated reference, so AsciiDoc no longer reads them as missing attributes (#1142)

---
 opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java         |   28 +++++++++
 opendj-cli/src/test/java/com/forgerock/opendj/cli/DocGenerationHelperTestCase.java |  107 +++++++++++++++++++++++++++++++++++
 2 files changed, 134 insertions(+), 1 deletions(-)

diff --git a/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java b/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java
index b8f410a..5a49e17 100644
--- a/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java
+++ b/opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java
@@ -12,6 +12,7 @@
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
  * Copyright 2015 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package com.forgerock.opendj.cli;
 
@@ -23,6 +24,7 @@
 import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.util.Map;
+import java.util.regex.Pattern;
 
 /**
  * This class provides utility functions to help generate reference documentation.
@@ -38,6 +40,13 @@
     private static Configuration configuration;
 
     /**
+     * An AsciiDoc attribute reference, such as {@code {name}}, that no backslash escapes.
+     * Word characters are Unicode ones, as in Asciidoctor.
+     */
+    private static final Pattern ATTRIBUTE_REFERENCE =
+            Pattern.compile("(?<!\\\\)(\\{\\w[\\w-]*\\})", Pattern.UNICODE_CHARACTER_CLASS);
+
+    /**
      * Gets a FreeMarker configuration for applying templates.
      *
      * @return              A FreeMarker configuration.
@@ -71,13 +80,30 @@
             Writer writer = new OutputStreamWriter(outputStream)) {
             Template configurationTemplate = configuration.getTemplate(template);
             configurationTemplate.process(map, writer);
-            builder.append(outputStream.toString());
+            builder.append(escapeAttributeReferences(outputStream.toString()));
         } catch (Exception e) {
             throw new RuntimeException(e.getMessage(), e);
         }
     }
 
     /**
+     * Escapes the AsciiDoc attribute references in generated reference text.
+     *
+     * <br>
+     *
+     * The generated reference refers to no AsciiDoc attribute: a {@code {name}} in it is the placeholder
+     * of an option value, written as is in the messages, which AsciiDoc would read as a reference
+     * to a missing attribute. A reference that is already escaped is left as is,
+     * so the result of a template can go through another template that includes it.
+     *
+     * @param text  The generated AsciiDoc text.
+     * @return      The text with each attribute reference escaped by a backslash.
+     */
+    static String escapeAttributeReferences(final String text) {
+        return ATTRIBUTE_REFERENCE.matcher(text).replaceAll("\\\\$1");
+    }
+
+    /**
      * Returns an option synopsis.
      *
      * <br>
diff --git a/opendj-cli/src/test/java/com/forgerock/opendj/cli/DocGenerationHelperTestCase.java b/opendj-cli/src/test/java/com/forgerock/opendj/cli/DocGenerationHelperTestCase.java
new file mode 100644
index 0000000..1ddf1e5
--- /dev/null
+++ b/opendj-cli/src/test/java/com/forgerock/opendj/cli/DocGenerationHelperTestCase.java
@@ -0,0 +1,107 @@
+/*
+ * 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 com.forgerock.opendj.cli;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * Tests that the generated AsciiDoc reference writes the value placeholders of the options
+ * as text, not as AsciiDoc attribute references.
+ */
+@SuppressWarnings("javadoc")
+public final class DocGenerationHelperTestCase extends CliTestCase {
+
+    private static final String GENDOC = "org.forgerock.opendj.gendoc";
+
+    /** An AsciiDoc attribute reference that no backslash escapes. */
+    private static final Pattern ATTRIBUTE_REFERENCE = Pattern.compile("(?<!\\\\)\\{\\w[\\w-]*\\}");
+
+    private String scriptName;
+    private String gendoc;
+
+    @BeforeClass
+    public void enableDocGeneration() {
+        scriptName = System.setProperty(ArgumentParser.PROPERTY_SCRIPT_NAME, "test-tool");
+        gendoc = System.setProperty(GENDOC, "true");
+    }
+
+    @AfterClass(alwaysRun = true)
+    public void restoreProperties() {
+        restore(ArgumentParser.PROPERTY_SCRIPT_NAME, scriptName);
+        restore(GENDOC, gendoc);
+    }
+
+    private static void restore(final String name, final String value) {
+        if (value != null) {
+            System.setProperty(name, value);
+        } else {
+            System.clearProperty(name);
+        }
+    }
+
+    @Test
+    public void toolReferenceEscapesPlaceholders() throws Exception {
+        final ArgumentParser parser =
+                new ArgumentParser(getClass().getName(), LocalizableMessage.raw("Reads the {path} you give."), false);
+        nameArgument().buildAndAddToParser(parser);
+
+        final String doc = parser.getUsage();
+
+        assertThat(doc).contains("Reads the \\{path} you give.");
+        assertThat(doc).contains("`--backend-name \\{name}`::");
+        assertThat(doc).contains("Depends on the \\{name} you provide, as {PROP:VALUE} or {name=value} do not.");
+        assertNoAttributeReference(doc);
+    }
+
+    @Test
+    public void subcommandReferenceEscapesPlaceholdersOnce() throws Exception {
+        final SubCommandArgumentParser parser =
+                new SubCommandArgumentParser(getClass().getName(), LocalizableMessage.raw("A tool."), false);
+        final SubCommand subCommand =
+                new SubCommand(parser, "get-backend-prop", LocalizableMessage.raw("Shows the {name} backend."));
+        nameArgument().buildAndAddToSubCommand(subCommand);
+
+        final String doc = parser.getUsage();
+
+        assertThat(doc).contains("Shows the \\{name} backend.");
+        assertThat(doc).contains("`--backend-name \\{name}`::");
+        assertThat(doc).contains("Depends on the \\{name} you provide, as {PROP:VALUE} or {name=value} do not.");
+        assertThat(doc).doesNotContain("\\\\{");
+        assertNoAttributeReference(doc);
+    }
+
+    private static StringArgument.Builder nameArgument() {
+        return StringArgument.builder("backend-name")
+                .description(LocalizableMessage.raw(
+                        "Depends on the {name} you provide, as {PROP:VALUE} or {name=value} do not"))
+                .valuePlaceholder(LocalizableMessage.raw("{name}"));
+    }
+
+    private static void assertNoAttributeReference(final String doc) {
+        final Matcher matcher = ATTRIBUTE_REFERENCE.matcher(doc);
+        if (matcher.find()) {
+            throw new AssertionError("Unescaped attribute reference " + matcher.group() + " in:\n" + doc);
+        }
+    }
+}

--
Gitblit v1.10.0