Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand All @@ -37,6 +39,13 @@ private DocGenerationHelper() {
/** FreeMarker template configuration. */
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.
*
Expand Down Expand Up @@ -71,12 +80,29 @@ public static void applyTemplate(StringBuilder builder, final String template, f
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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading