/**
* Copyright 2009 Google Inc.
*
* Licensed 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.waveprotocol.wave.model.id;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
/**
* Escapes and un-escapes characters by prefixing another character.
*
* @author zdwang@google.com (David Wang)
*/
public class SimplePrefixEscaper {
/**
* This is the default escaper that is used to prefix escape "+", "!" with "~".
*
* TODO: Consider using regex to simplify and optimise the implementation.
*/
public static final SimplePrefixEscaper DEFAULT_ESCAPER = new SimplePrefixEscaper('~', '+', '!');
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private final Set<Character> needsEscaping = new HashSet<Character>();
/** Character used to prefix an escaped character. */
private final char prefix;
public SimplePrefixEscaper(char prefix, char... needsEscaping) {
this.prefix = prefix;
this.needsEscaping.add(prefix);
for (char s : needsEscaping) {
this.needsEscaping.add(s);
}
}
/**
* Escapes instances of a char in a string by prefixing it with another char.
*
* @param toEscape string in which to escape it
* @return the escaped string
*/
public String escape(String toEscape) {
StringBuilder cache = new StringBuilder(toEscape.length());
for (int i = 0; i < toEscape.length(); i++) {
if (needsEscaping.contains(toEscape.charAt(i))) {
cache.append(prefix);
}
cache.append(toEscape.charAt(i));
}
return cache.toString();
}
/**
* Un-escapes instances of a char in a string by replacing prefixed values
* with just the value.
*
* @param toUnescape string from which to un-escape it
* @return the un-escaped string
*/
public String unescape(String toUnescape) {
// At least the string is half as long.
StringBuilder cache = new StringBuilder(toUnescape.length() / 2);
for (int i = 0; i < toUnescape.length(); i++) {
if (toUnescape.charAt(i) == prefix) {
if (i + 1 >= toUnescape.length()) {
throw new IllegalArgumentException("The value to unescape cannot be terminated with " +
"the prefix: " + prefix);
}
if (!needsEscaping.contains(toUnescape.charAt(i + 1))) {
throw new IllegalArgumentException("The value to unescape is not a properly escaped " +
"value. The prefix charater is not followed by a character at needs prefixing: " +
toUnescape);
}
// increment the index to the next character
i++;
} else if (needsEscaping.contains(toUnescape.charAt(i))) {
throw new IllegalArgumentException("The value to unescape is not a properly escaped " +
"value. Some chars are found unescaped: " + toUnescape);
}
cache.append(toUnescape.charAt(i));
}
return cache.toString();
}
/**
* Join tokens together using a separator. If the separator appears in any
* tokens it is escaped.
*
* @param separator the separator to join the tokens.
* @param tokens tokens to join
* @return joined tokens by separator
*/
public String join(char separator, String... tokens) {
if (separator == prefix) {
throw new IllegalArgumentException("It's unsafe to join strings together using the prefix" +
"char.");
}
// Doing this makes it unambiguous that "" is the join of a single empty token,
// not the join of no tokens.
if (tokens.length == 0) {
throw new IllegalArgumentException("Must have at least 1 token to use join.");
}
if (!needsEscaping.contains(separator)) {
throw new IllegalArgumentException("It's unsafe to join strings together using a " +
"[separator:" + separator + "] that is not in the characters that are escaped.");
}
StringBuilder ret = new StringBuilder();
for (int i = 0; i < tokens.length; i++) {
if (i > 0) {
ret.append(separator);
}
ret.append(escape(tokens[i]));
}
return ret.toString();
}
/**
* Splits a string on a separator. Any escaped separator characters appearing
* in tokens are un-escaped. Any separator character by itself is a split
* point.
*
* @param separator separator character
* @param toSplit string to split
* @return a list of unescaped tokens
*/
public String[] split(char separator, String toSplit) {
String[] ret = splitWithoutUnescaping(separator, toSplit);
for (int i = 0; i < ret.length; i++) {
ret[i] = unescape(ret[i]);
}
return ret;
}
/**
* Splits a string on a separator. If the separator is escaped, it's ignored as a split point.
* Any separator character by itself is a split point.
*
* @param separator separator character
* @param toSplit string to split
* @return a list of escaped (untouched) tokens
*/
public String[] splitWithoutUnescaping(char separator, String toSplit) {
if (separator == prefix) {
throw new IllegalArgumentException("It's unsafe to split strings together the prefix char.");
}
ArrayList<String> ret = new ArrayList<String>();
int start = 0;
while (start <= toSplit.length()) {
int end = start;
while (end < toSplit.length() && toSplit.charAt(end) != separator) {
// skip over escaped chars.
end += toSplit.charAt(end) == prefix ? 2 : 1;
}
if (end >= toSplit.length()) {
end = toSplit.length();
}
ret.add(toSplit.substring(start, end));
start = end + 1;
end = end + 1;
}
return ret.toArray(EMPTY_STRING_ARRAY);
}
/**
* @param escapedValue The escaped string.
* @return true if all occurrences of characters in needsEscaping, except the separator character,
* are escaped properly.
*/
public boolean isEscapedProperly(char separator, String escapedValue) {
for (int i = 0; i < escapedValue.length(); i++) {
char c = escapedValue.charAt(i);
if (c == prefix) {
// The next character after the prefix is not a character that needs escaping.
if (i >= escapedValue.length() - 1 || !needsEscaping.contains(escapedValue.charAt(i + 1))) {
return false;
} else {
// skip over the escaped char
i++;
}
} else if (c != separator && needsEscaping.contains(c)) { // found unescaped char
return false;
}
}
return true;
}
/**
* @param unescaped The unescaped value to be tested.
* @return true if there are no characters that in the needsEscaping found in unescaped.
*/
public boolean hasEscapeCharacters(String unescaped) {
for (int i = 0; i < unescaped.length(); i++) {
// THis is a character that needs to be escaped
if (needsEscaping.contains(unescaped.charAt(i))) {
return true;
}
}
return false;
}
}