mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Fabio Pistolesi
05.43.2016 1404def3f16710d36a874d819613f7f0c4e12ee7
OPENDJ-2748 OPENDJ-1667 dsconfig --displayCommand should print a usable command and parse it correctly afterwards

Printing the command should not wrap it on the terminal, otherwise embedded spaces could be lost.
Parsing the command back, either batch file or stdin, requires handling escape sequencing and double quotes around multiword values.
Add unescaping of sequences even in multiword values, to take into account shell escapes.
1 files added
2 files modified
139 ■■■■ changed files
opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java 12 ●●●●● patch | view | raw | blame | history
opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java 79 ●●●●● patch | view | raw | blame | history
opendj-config/src/test/java/org/forgerock/opendj/config/dsconfig/DSConfigParseTest.java 48 ●●●●● patch | view | raw | blame | history
opendj-cli/src/main/java/com/forgerock/opendj/cli/ConsoleApplication.java
@@ -281,6 +281,18 @@
    }
    /**
     * Displays a message to the output stream without wrapping.
     *
     * @param msg
     *            The message.
     */
    public final void printlnNoWrap(final LocalizableMessage msg) {
        if (!isQuiet()) {
            out.println(msg);
        }
    }
    /**
     * Prints a progress bar on the same output stream line if not in quiet mode.
     *
     * <pre>
opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java
@@ -1341,7 +1341,7 @@
        if (displayEquivalentArgument.isPresent()) {
            println();
            // We assume that the app we are running is this one.
            println(INFO_DSCFG_NON_INTERACTIVE.get(commandBuilder));
            printlnNoWrap(INFO_DSCFG_NON_INTERACTIVE.get(commandBuilder));
        }
        if (equivalentCommandFileArgument.isPresent()) {
            String file = equivalentCommandFileArgument.getValue();
@@ -1422,13 +1422,7 @@
                command += line;
                command = command.trim();
                // string between quotes support
                command = replaceSpacesInQuotes(command);
                // "\ " support
                command = command.replace("\\ ", "##");
                String displayCommand = command.replace("\\ ", " ");
                println(LocalizableMessage.raw(displayCommand));
                printlnNoWrap(LocalizableMessage.raw(command));
                // Append initial arguments to the file line
                final String[] allArgsArray = buildCommandArgs(initialArgs, command);
@@ -1448,20 +1442,54 @@
    }
    private String[] buildCommandArgs(List<String> initialArgs, String batchCommand) {
        final String[] commandArgs = toCommandArgs(batchCommand);
        final int length = commandArgs.length + initialArgs.size();
        final Collection<String> commandArgs = toCommandArgs(batchCommand);
        final int length = commandArgs.size() + initialArgs.size();
        final List<String> allArguments = new ArrayList<>(length);
        Collections.addAll(allArguments, commandArgs);
        allArguments.addAll(commandArgs);
        allArguments.addAll(initialArgs);
        return allArguments.toArray(new String[length]);
    }
    private String[] toCommandArgs(String command) {
        String[] fileArguments = command.split("\\s+");
        for (int ii = 0; ii < fileArguments.length; ii++) {
            fileArguments[ii] = fileArguments[ii].replace("##", " ");
    static Collection<String> toCommandArgs(String command) {
        Collection<String> commandArgs = new ArrayList<>();
        StringBuilder builder = new StringBuilder();
        boolean inQuotes = false;
        for (int i = 0; i < command.length(); i++) {
            final char c = command.charAt(i);
            switch (c) {
            default:
                builder.append(c);
                break;
            case '\\':
                builder.append(command.charAt(++i));
                break;
            case '"':
                if (inQuotes) {
                    builder = newArgumentString(commandArgs, builder);
                    inQuotes = false;
                } else {
                    inQuotes = true;
        }
        return fileArguments;
                break;
            case ' ':
                if (inQuotes) {
                    builder.append(c);
                } else {
                    builder = newArgumentString(commandArgs, builder);
                }
                break;
            }
        }
        newArgumentString(commandArgs, builder);
        return commandArgs;
    }
    private static StringBuilder newArgumentString(Collection<String> commandArgs, StringBuilder stringBuilder) {
        if (stringBuilder.length() > 0) {
            commandArgs.add(stringBuilder.toString());
            stringBuilder = new StringBuilder();
        }
        return stringBuilder;
    }
    private List<String> removeBatchArgs(String[] args) {
@@ -1487,23 +1515,4 @@
        }
        return initialArgs;
    }
    /** Replace spaces in quotes by "\ ". */
    private String replaceSpacesInQuotes(final String line) {
        StringBuilder newLine = new StringBuilder();
        boolean inQuotes = false;
        for (int ii = 0; ii < line.length(); ii++) {
            char ch = line.charAt(ii);
            if (ch == '\"' || ch == '\'') {
                inQuotes = !inQuotes;
                continue;
            }
            if (inQuotes && ch == ' ') {
                newLine.append("\\ ");
            } else {
                newLine.append(ch);
            }
        }
        return newLine.toString();
    }
}
opendj-config/src/test/java/org/forgerock/opendj/config/dsconfig/DSConfigParseTest.java
New file
@@ -0,0 +1,48 @@
/*
 * 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]".
 *
 * Portions copyright 2016 ForgeRock AS.
 */
package org.forgerock.opendj.config.dsconfig;
import org.forgerock.testng.ForgeRockTestCase;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.Collection;
@Test(groups = { "precommit", "config" })
public class DSConfigParseTest extends ForgeRockTestCase {
    @DataProvider
    public Object[][] escapeSequences() {
        return new Object[][] {
            {"global-aci:\\(targetattr=\\\"userPassword\\|\\|authPassword\\\"\\)"
                    + "\\(version\\ 3.0\\;\\ acl\\ \\\"Self\\ entry\\ read\\'\\\"\\;"
                    + "\\ allow\\ \\(read,search,compare\\)\\ userdn=\\\"ldap:///self\\\"\\;\\)",
                "global-aci:(targetattr=\"userPassword||authPassword\")"
                    + "(version 3.0; acl \"Self entry read'\";"
                    + " allow (read,search,compare) userdn=\"ldap:///self\";)"
            },
            {"cn=\"admin data\"", "cn=admin data"},
            {"\"cn=admin data\"", "cn=admin data"},
            {"cn=\\\"admin", "cn=\"admin"}
        };
    }
    @Test(dataProvider = "escapeSequences")
    public void testEscapeSequenceInCommandArgument(String arg, String value) throws Exception {
        Collection<String> cmdLine = DSConfig.toCommandArgs(arg);
        Assert.assertEquals(cmdLine.iterator().next(), value);
    }
}