You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by dk...@apache.org on 2022/01/19 14:49:41 UTC
[sling-org-apache-sling-repoinit-parser] branch master updated: SLING-10952 - Adding support for whitespace characters in group names (#14)
This is an automated email from the ASF dual-hosted git repository.
dklco pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-repoinit-parser.git
The following commit(s) were added to refs/heads/master by this push:
new 8e60c3c SLING-10952 - Adding support for whitespace characters in group names (#14)
8e60c3c is described below
commit 8e60c3c5d9a0a70f6e39d4555decee74fec804f2
Author: Dan Klco <kl...@users.noreply.github.com>
AuthorDate: Wed Jan 19 09:49:33 2022 -0500
SLING-10952 - Adding support for whitespace characters in group names (#14)
* SLING-10952 - Adding support for whitespace characters in group names
* Removing backtracking regex for determining if string contains whitespace
* SLING-11051 - Fixing JavaDoc badge
* Renaming to quotableString and adding some more test coverage
* Adding missing classes
* Reverting unrelated changes
---
.../repoinit/parser/impl/QuotableStringUtil.java | 57 +++++++++++
.../repoinit/parser/operations/AclGroupBase.java | 4 +-
.../parser/operations/AddGroupMembers.java | 4 +-
.../repoinit/parser/operations/CreateGroup.java | 3 +-
.../repoinit/parser/operations/DeleteGroup.java | 3 +-
.../repoinit/parser/operations/Operation.java | 4 +
.../parser/operations/RemoveGroupMembers.java | 4 +-
.../parser/operations/SetAclPrincipalBased.java | 4 +-
.../parser/operations/SetAclPrincipals.java | 7 +-
src/main/javacc/RepoInitGrammar.jjt | 22 +++--
.../repoinit/parser/test/ParsingErrorsTest.java | 12 ++-
.../parser/test/QuotableStringParserTest.java | 105 +++++++++++++++++++++
.../parser/test/QuotableStringUtilTest.java | 76 +++++++++++++++
src/test/resources/testcases/test-71-output.txt | 26 +++++
src/test/resources/testcases/test-71.txt | 36 +++++++
15 files changed, 349 insertions(+), 18 deletions(-)
diff --git a/src/main/java/org/apache/sling/repoinit/parser/impl/QuotableStringUtil.java b/src/main/java/org/apache/sling/repoinit/parser/impl/QuotableStringUtil.java
new file mode 100644
index 0000000..1c0d327
--- /dev/null
+++ b/src/main/java/org/apache/sling/repoinit/parser/impl/QuotableStringUtil.java
@@ -0,0 +1,57 @@
+/*
+ * 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
+ *
+ * http://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.sling.repoinit.parser.impl;
+
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.jetbrains.annotations.NotNull;
+
+public class QuotableStringUtil {
+
+ private static final Pattern REQUIRES_QUOTES = Pattern.compile("[\\s|\\\\]");
+
+ private QuotableStringUtil() {
+ // hidden
+ }
+
+ /**
+ * Gets the string handling cases where the value should be quoted
+ * (i.e. the string contains whitespace)
+ *
+ * @param string the string to get in repoinit compatible form
+ * @return the (potentially quoted) string
+ */
+ @NotNull
+ public static final String forRepoInitString(@NotNull String string) {
+ return REQUIRES_QUOTES.matcher(string).find() ? "\"" + string + "\""
+ : string;
+ }
+
+ /**
+ * Gets the strings handling cases where the value should be quoted
+ * (i.e. the string contains whitespace)
+ *
+ * @param strings the strings to get
+ * @return the list of (potentially quoted) strings
+ */
+ @NotNull
+ public static final List<String> forRepoInitString(@NotNull List<String> strings) {
+ return strings.stream().map(QuotableStringUtil::forRepoInitString).collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/AclGroupBase.java b/src/main/java/org/apache/sling/repoinit/parser/operations/AclGroupBase.java
index 434a1f0..8f1bd85 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/AclGroupBase.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/AclGroupBase.java
@@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.Formatter;
import java.util.List;
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
import org.apache.sling.repoinit.parser.operations.AclLine.Action;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ProviderType;
@@ -74,7 +75,8 @@ abstract class AclGroupBase extends Operation {
String pathStr = pathsToString(line.getProperty(AclLine.PROP_PATHS));
onOrFor = (pathStr.isEmpty()) ? "" : " on " + pathStr;
} else {
- onOrFor = " for " + listToString(line.getProperty(AclLine.PROP_PRINCIPALS));
+ onOrFor = " for " + listToString(
+ QuotableStringUtil.forRepoInitString(line.getProperty(AclLine.PROP_PRINCIPALS)));
}
formatter.format(" %s %s%s%s%s%n", action, privileges, onOrFor,
nodetypesToString(line.getProperty(AclLine.PROP_NODETYPES)),
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/AddGroupMembers.java b/src/main/java/org/apache/sling/repoinit/parser/operations/AddGroupMembers.java
index 5ccb49f..4b5383c 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/AddGroupMembers.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/AddGroupMembers.java
@@ -19,6 +19,7 @@ package org.apache.sling.repoinit.parser.operations;
import java.util.List;
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ProviderType;
@@ -55,7 +56,8 @@ public class AddGroupMembers extends Operation {
@NotNull
@Override
public String asRepoInitString() {
- return String.format("add %s to group %s%n", listToString(members), groupname);
+ return String.format("add %s to group %s%n", listToString(QuotableStringUtil.forRepoInitString(members)),
+ QuotableStringUtil.forRepoInitString(groupname));
}
public String getGroupname() {
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/CreateGroup.java b/src/main/java/org/apache/sling/repoinit/parser/operations/CreateGroup.java
index 3384c46..56e1f69 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/CreateGroup.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/CreateGroup.java
@@ -17,6 +17,7 @@
package org.apache.sling.repoinit.parser.operations;
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
import org.apache.sling.repoinit.parser.impl.WithPathOptions;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ProviderType;
@@ -63,7 +64,7 @@ public class CreateGroup extends OperationWithPathOptions {
@NotNull
@Override
public String asRepoInitString() {
- return asRepoInitString("group", groupname);
+ return asRepoInitString("group", QuotableStringUtil.forRepoInitString(groupname));
}
public String getGroupname() {
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/DeleteGroup.java b/src/main/java/org/apache/sling/repoinit/parser/operations/DeleteGroup.java
index 1fcb807..3fc1d85 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/DeleteGroup.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/DeleteGroup.java
@@ -17,6 +17,7 @@
package org.apache.sling.repoinit.parser.operations;
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ProviderType;
@@ -46,7 +47,7 @@ public class DeleteGroup extends Operation {
@NotNull
@Override
public String asRepoInitString() {
- return String.format("delete group %s%n", groupname);
+ return String.format("delete group %s", QuotableStringUtil.forRepoInitString(groupname));
}
public String getGroupname() {
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/Operation.java b/src/main/java/org/apache/sling/repoinit/parser/operations/Operation.java
index e1b1d80..9aa4157 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/Operation.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/Operation.java
@@ -17,6 +17,7 @@
package org.apache.sling.repoinit.parser.operations;
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ProviderType;
@@ -72,6 +73,9 @@ public abstract class Operation {
if (s.startsWith(":") && s.contains("#")) {
String func = s.substring(1, s.indexOf(":",1));
String s2 = s.substring(func.length()+2, s.lastIndexOf('#'));
+ if ("authorizable".equals(func)) {
+ s2 = QuotableStringUtil.forRepoInitString(s2);
+ }
String trailingPath = (s.endsWith("#")) ? "" : s.substring(s.indexOf("#")+1);
return func + "(" + s2 +")" + trailingPath;
} else {
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/RemoveGroupMembers.java b/src/main/java/org/apache/sling/repoinit/parser/operations/RemoveGroupMembers.java
index 320e647..5d2b7f7 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/RemoveGroupMembers.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/RemoveGroupMembers.java
@@ -19,6 +19,7 @@ package org.apache.sling.repoinit.parser.operations;
import java.util.List;
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ProviderType;
@@ -55,7 +56,8 @@ public class RemoveGroupMembers extends Operation {
@NotNull
@Override
public String asRepoInitString() {
- return String.format("remove %s from group %s%n", listToString(members), groupname);
+ return String.format("remove %s from group %s%n", listToString(QuotableStringUtil.forRepoInitString(members)),
+ QuotableStringUtil.forRepoInitString(groupname));
}
public String getGroupname() {
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipalBased.java b/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipalBased.java
index da4b8b3..5f8b937 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipalBased.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipalBased.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ProviderType;
@@ -52,7 +53,8 @@ public class SetAclPrincipalBased extends AclGroupBase {
@NotNull
@Override
public String asRepoInitString() {
- String topline = String.format("set principal ACL for %s%s%n", listToString(principals), getAclOptionsString());
+ String topline = String.format("set principal ACL for %s%s%n",
+ listToString(QuotableStringUtil.forRepoInitString(principals)), getAclOptionsString());
return asRepoInit(topline, true);
}
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipals.java b/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipals.java
index fb96bc3..6abc2be 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipals.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipals.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ProviderType;
@@ -56,10 +57,12 @@ public class SetAclPrincipals extends AclGroupBase {
List<String> paths = line.getProperty(AclLine.PROP_PATHS);
return paths == null || paths.isEmpty();
})) {
- String topline = String.format("set repository ACL for %s%s%n", listToString(principals), getAclOptionsString());
+ String topline = String.format("set repository ACL for %s%s%n",
+ listToString(QuotableStringUtil.forRepoInitString(principals)), getAclOptionsString());
return asRepoInit(topline, true);
} else {
- String topline = String.format("set ACL for %s%s%n", listToString(principals), getAclOptionsString());
+ String topline = String.format("set ACL for %s%s%n",
+ listToString(QuotableStringUtil.forRepoInitString(principals)), getAclOptionsString());
return asRepoInit(topline, true);
}
}
diff --git a/src/main/javacc/RepoInitGrammar.jjt b/src/main/javacc/RepoInitGrammar.jjt
index 1e4236d..3b035d6 100644
--- a/src/main/javacc/RepoInitGrammar.jjt
+++ b/src/main/javacc/RepoInitGrammar.jjt
@@ -178,8 +178,8 @@ List<String> principalsList() :
List<String> principals = new ArrayList<String>();
}
{
- t = <STRING> { principals.add(t.image); }
- ( <COMMA> t = <STRING> { principals.add(t.image); } )*
+ t = quotableString() { principals.add(t.image); }
+ ( <COMMA> t = quotableString() { principals.add(t.image); } )*
{ return principals; }
}
@@ -247,7 +247,7 @@ String usernameList() :
Token t = null;
}
{
- ( t = <STRING> ) { names.add(t.image); }
+ ( t = quotableString() ) { names.add(t.image); }
// disable lists for now, not supported downstream
// ( <COMMA> t = <STRING> { names.add(t.image); }) *
@@ -654,7 +654,7 @@ void createGroupStatement(List<Operation> result) :
}
{
<CREATE> <GROUP>
- ( group = <STRING> )
+ ( group = quotableString() )
( wpopt = withPathStatement() ) ?
{
@@ -668,7 +668,7 @@ void deleteGroupStatement(List<Operation> result) :
}
{
<DELETE> <GROUP>
- ( group = <STRING> )
+ ( group = quotableString() )
{
result.add(new DeleteGroup(group.image));
@@ -754,7 +754,7 @@ void addToGroupStatement(List<Operation> result) :
<ADD>
members = principalsList()
<TO> <GROUP>
- group = <STRING>
+ group = quotableString()
{
result.add(new AddGroupMembers(members, group.image));
@@ -771,13 +771,21 @@ void removeFromGroupStatement(List<Operation> result) :
<REMOVE>
members = principalsList()
<FROM> <GROUP>
- group = <STRING>
+ group = quotableString()
{
result.add(new RemoveGroupMembers(members, group.image));
}
}
+Token quotableString() :
+{
+ Token t = null;
+}
+{
+ (t = <STRING> | t = quotedString() ) { return t; }
+}
+
Token propertyValue() :
{
Token t = null;
diff --git a/src/test/java/org/apache/sling/repoinit/parser/test/ParsingErrorsTest.java b/src/test/java/org/apache/sling/repoinit/parser/test/ParsingErrorsTest.java
index 58565b3..002e398 100644
--- a/src/test/java/org/apache/sling/repoinit/parser/test/ParsingErrorsTest.java
+++ b/src/test/java/org/apache/sling/repoinit/parser/test/ParsingErrorsTest.java
@@ -129,6 +129,11 @@ public class ParsingErrorsTest {
// SLING-6219 - delete user does not support lists
add(new Object[] { "delete user alice,bob", ParseException.class });
+
+ // SLING-10952 - Support quoted group names
+ add(new Object[] { "create group My Group", ParseException.class });
+ add(new Object[] { "create group My\tGroup", ParseException.class });
+ add(new Object[] { "create group \"My\u200bGroup\"", ParseException.class });
}};
return result;
}
@@ -151,8 +156,9 @@ public class ParsingErrorsTest {
public void checkResult() throws ParseException, IOException {
final StringReader r = new StringReader(input);
boolean noException = false;
+ String parsed = null;
try {
- new RepoInitParserImpl(r).parse();
+ parsed = new RepoInitParserImpl(r).parse().toString();
noException = true;
} catch(Exception e) {
assertEquals(getInfo(input, e), expected, e.getClass());
@@ -162,8 +168,8 @@ public class ParsingErrorsTest {
r.close();
}
- if(noException && expected != null) {
- fail("Expected a " + expected.getSimpleName() + " for [" + input + "]");
+ if (noException && expected != null) {
+ fail("Expected a " + expected.getSimpleName() + " for [" + input + "] parsed to [" + parsed + "]");
}
}
}
diff --git a/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringParserTest.java b/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringParserTest.java
new file mode 100644
index 0000000..8685943
--- /dev/null
+++ b/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringParserTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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
+ *
+ * http://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.sling.repoinit.parser.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.sling.repoinit.parser.impl.RepoInitParserImpl;
+import org.apache.sling.repoinit.parser.impl.ParseException;
+import org.apache.sling.repoinit.parser.operations.Operation;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class QuotableStringParserTest {
+
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> data() throws IOException {
+
+ List<Object[]> testCases = new ArrayList<>();
+
+ testCases.add(new Object[] {
+ "backslashes",
+ "create group \"a\\group\"",
+ "CreateGroup a\\group",
+ true
+ });
+ testCases.add(new Object[] {
+ "space",
+ "create group \"a group\"",
+ "CreateGroup a group",
+ true
+ });
+ testCases.add(new Object[] {
+ "unnecessarily quoted",
+ "create group \"unnecessarily-quoted\"",
+ "CreateGroup unnecessarily-quoted",
+ false
+ });
+
+ return testCases;
+ }
+
+ private final String name;
+ private final String statement;
+ private final String expected;
+ private final boolean reproducable;
+
+ public QuotableStringParserTest(String name, String statement, String expected, boolean reproducable) {
+ this.name = name;
+ this.statement = statement;
+ this.expected = expected;
+ this.reproducable = reproducable;
+ }
+
+ @Test
+ public void testParse() throws ParseException, IOException {
+ Operation op = parseSingleStatement(statement);
+ assertEquals("Test " + name + " failed", expected, op.toString());
+ }
+
+ @Test
+ public void testReproduceParse() throws ParseException, IOException {
+ Operation op = parseSingleStatement(statement);
+ String expected = String.format("%s%n", statement);
+ if (reproducable) {
+ assertEquals(expected, op.asRepoInitString());
+ } else {
+ assertNotEquals(expected, op.asRepoInitString());
+ }
+ }
+
+ private Operation parseSingleStatement(String statement) throws ParseException, IOException {
+ final StringReader r = new StringReader(statement);
+ try {
+ List<Operation> operations = new RepoInitParserImpl(r).parse();
+ assertEquals(1, operations.size());
+ return operations.get(0);
+ } finally {
+ r.close();
+ }
+ }
+}
diff --git a/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringUtilTest.java b/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringUtilTest.java
new file mode 100644
index 0000000..bee6d1b
--- /dev/null
+++ b/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringUtilTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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
+ *
+ * http://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.sling.repoinit.parser.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
+import org.junit.Test;
+
+public class QuotableStringUtilTest {
+
+ @Test
+ public void testAllAlphaNumeric() {
+ assertEquals("administrators", QuotableStringUtil.forRepoInitString("administrators"));
+ assertEquals("administrators1", QuotableStringUtil.forRepoInitString("administrators1"));
+ }
+
+ @Test
+ public void testWithCommonSeparators() {
+ assertEquals("sling-search-path-reader", QuotableStringUtil.forRepoInitString("sling-search-path-reader"));
+ assertEquals("sling__search_path-reader", QuotableStringUtil.forRepoInitString("sling__search_path-reader"));
+ }
+
+ @Test
+ public void testWithDomain() {
+ assertEquals("auser@adomain.com", QuotableStringUtil.forRepoInitString("auser@adomain.com"));
+ }
+
+ @Test
+ public void testWithWhitespace() {
+ assertEquals("\"A User\"", QuotableStringUtil.forRepoInitString("A User"));
+ assertEquals("\"A\tGroup\"", QuotableStringUtil.forRepoInitString("A\tGroup"));
+ }
+
+ @Test
+ public void testWithNewLines() {
+ assertEquals("\"A\nUser\"", QuotableStringUtil.forRepoInitString("A\nUser"));
+ assertEquals("\"A\rGroup\"", QuotableStringUtil.forRepoInitString("A\rGroup"));
+ }
+
+ @Test
+ public void testWithBackslashOnly() {
+ assertEquals("\"A\\User\"", QuotableStringUtil.forRepoInitString("A\\User"));
+ }
+
+ @Test
+ public void testList() {
+ List<String> input = new ArrayList<>();
+ input.add("a-user");
+ input.add("A Group");
+
+ List<String> expected = new ArrayList<>();
+ expected.add("a-user");
+ expected.add("\"A Group\"");
+
+ assertEquals(expected, QuotableStringUtil.forRepoInitString(input));
+ }
+
+}
diff --git a/src/test/resources/testcases/test-71-output.txt b/src/test/resources/testcases/test-71-output.txt
new file mode 100644
index 0000000..992db86
--- /dev/null
+++ b/src/test/resources/testcases/test-71-output.txt
@@ -0,0 +1,26 @@
+CreateGroup Test Group
+CreateGroup Test Group With Spaces with path /thePathF
+DeleteGroup Test Group
+SetAclPaths on /content
+ AclLine ALLOW {principals=[Test Group, user1], privileges=[jcr:read]}
+SetAclPaths on /content
+ AclLine ALLOW {principals=[Test Group- Cool People, Test Group, user1], privileges=[jcr:read]}
+SetAclPrincipals for user1 Test Group u2
+ AclLine ALLOW {paths=[/content], privileges=[jcr:read]}
+SetAclPrincipalBased for user1 Test Group ACLOptions=[mergePreserve]
+ AclLine REMOVE_ALL {paths=[/libs, /apps]}
+ AclLine ALLOW {paths=[/content], privileges=[jcr:read]}
+SetAclPaths on /test ACLOptions=[merge]
+ AclLine REMOVE_ALL {principals=[user1, Test Group, user2]}
+SetProperties on :authorizable:bob# :authorizable:Test Group#
+ PropertyLine stringProp{String}=[{String}hello, you again!]
+SetProperties on :authorizable:bob#/nested :authorizable:Test Group#/nested
+ PropertyLine stringProp{String}=[{String}hello, you nested again!]
+AddGroupMembers user1 Test Group 2000 user2 in group Parent Group
+RemoveGroupMembers user1 Test Group 2000 user2 in group Parent Group
+CreateGroup Tab Group
+CreateGroup Untrimmed Group
+CreateGroup Really Untrimmed Group
+CreateGroup Group\With\Backslash
+CreateGroup Group
+Newline
\ No newline at end of file
diff --git a/src/test/resources/testcases/test-71.txt b/src/test/resources/testcases/test-71.txt
new file mode 100644
index 0000000..eda04fc
--- /dev/null
+++ b/src/test/resources/testcases/test-71.txt
@@ -0,0 +1,36 @@
+# Support quoted Group IDs
+create group "Test Group"
+create group "Test Group With Spaces" with path /thePathF
+delete group "Test Group"
+set ACL on /content
+ allow jcr:read for "Test Group",user1
+end
+set ACL on /content
+ allow jcr:read for "Test Group- Cool People","Test Group",user1
+end
+set ACL for user1,"Test Group",u2
+ allow jcr:read on /content
+end
+set principal ACL for user1,"Test Group" (ACLOptions=mergePreserve)
+ remove * on /libs,/apps
+ allow jcr:read on /content
+end
+set ACL on /test (ACLOptions=merge)
+ remove * for user1,"Test Group",user2
+end
+set properties on authorizable(bob), authorizable("Test Group")
+ set stringProp to "hello, you again!"
+end
+set properties on authorizable(bob)/nested, authorizable("Test Group")/nested
+ set stringProp to "hello, you nested again!"
+end
+add user1,"Test Group 2000",user2 to group "Parent Group"
+remove user1,"Test Group 2000",user2 from group "Parent Group"
+
+# Test other escaped characters
+create group "Tab Group"
+create group "Untrimmed Group "
+create group " Really Untrimmed Group "
+create group "Group\With\Backslash"
+create group "Group
+Newline"
\ No newline at end of file