/** * Copyright 2010 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.client.common.util; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.util.Collection; /** * Encodes a string sequence as a single string. As per the {@link Sequence} * interface, repeated strings are not supported. * <p> * The single string that holds the sequences is a comma-delimited list. There * are no restrictions on the strings that can be placed in this sequence - * strings are encoded and decoded appropriately. For example, * * <pre> * ["foo", "bar"] --> ",foo,bar," * ["foo,bar", "foo&baz"] --> ",foo&,bar,foo&&baz," * </pre> * * All random-access methods in this implementation are linear time, so a linear * number of accesses is quadratic in the worst case. This class expects a * sequential access pattern, and is implemented so that <em>m</em> sequential * queries on an <em>n</em>-sized sequence is only <em>O(n + m)</em>. This is * not true for mutations, however: a series of <em>m</em> mutations, sequential * or not, will be <em>O(nm)</em>. For a sequence implementation that provides * expected constant time complexity for all methods, see {@link LinkedSequence}. * */ // // Unescaped version of the example above: // ["foo", "bar"] --> ",foo,bar," // ["foo,bar", "baz&quux"] --> ",foo&cbar,baz&&quux," // public final class StringSequence implements Sequence<String> { /** Codec to use for encoding / decoding values in the list. */ private static final StringCodec CODEC = StringCodec.INSTANCE; /** String to demarcate items in the list. */ private static final String DELIMITER = CODEC.free().substring(0, 1); /** * Embedded list. Never null, and is always is of the form: DELIMITER * (NONDELIMITER+ DELIMITER)*. */ // Note: composite linear complexity for sequential writes (i.e., m sequential // writes is only O(n + m)) could be achieved by alternating between a // serialized string and a stringbuffer of pending sequential mutations. This // is not implemented in order to keep this implementation very lightweight. private String data; /** * Last index used in a reference search. This is used to optimize for * sequential access, so that m queries cost (n + m) rather than O(nm). */ private int recentIndex; @VisibleForTesting StringSequence(String data) { this.data = data; } /** Creates an empty string sequence. */ public static StringSequence create() { return new StringSequence(DELIMITER); } /** Creates a string sequence on a string from another {@code StringSequence}. */ public static StringSequence create(String serializedSequence) { Preconditions.checkArgument(serializedSequence.startsWith(DELIMITER) && serializedSequence.endsWith(DELIMITER)); return new StringSequence(serializedSequence); } /** Creates a string sequence with an initial state. */ public static StringSequence of(Collection<String> xs) { StringBuilder data = new StringBuilder(); data.append(DELIMITER); for (String x : xs) { data.append(CODEC.encode(x)); data.append(DELIMITER); } return new StringSequence(data.toString()); } // JS does not do automatic builderization of composite concatentaions. /** Concatenates strings. */ private static String concat(String s1, String s2, String s3) { StringBuilder s = new StringBuilder(); s.append(s1); s.append(s2); s.append(s3); return s.toString(); } /** Concatenates strings. */ private static String concat(String s1, String s2, String s3, String s4) { StringBuilder s = new StringBuilder(); s.append(s1); s.append(s2); s.append(s3); s.append(s4); return s.toString(); } @Override public boolean contains(String x) { return data.contains(concat(DELIMITER, CODEC.encode(x), DELIMITER)); } /** * Finds a term in the data string. The search is initiated from the location * of the most recent hit. * * @param term (coded) term to search for * @param forwardFirst if true, the search is performed with a forward search * then a backward search; if false, the search is performed with a * backward search then a forward search; * @return the index after {@code term} for a forward search; the index of * {@code} term for a backward search. * @throws IllegalArgumentException if {@code term} is not found. */ private int find(String term, boolean forwardFirst) { int index; if (forwardFirst) { // Search forward, leaving cursor after the find. index = data.indexOf(term, recentIndex); if (index >= 0) { return recentIndex = index + term.length(); } // Search backward, leaving cursor after the find. index = data.lastIndexOf(term, recentIndex); if (index >= 0) { return recentIndex = index + term.length(); } } else { // Search backward, leaving cursor before the find. index = data.lastIndexOf(term, recentIndex); if (index >= 0) { return recentIndex = index; } // Search forward, leaving cursor before the find. index = data.indexOf(term, recentIndex); if (index >= 0) { return recentIndex = index; } } // Miss. throw new IllegalArgumentException("Item not found: " + CODEC.decode(term.substring(DELIMITER.length(), term.length() - DELIMITER.length()))); } private int findForward(String term) { return find(term, true); } private int findBackward(String term) { return find(term, false); } @Override public String getFirst() { // // , f o o , ... , b a r , // . ^ first // recentIndex = DELIMITER.length(); return recentIndex == data.length() ? null // \u2620 : CODEC.decode(data.substring(recentIndex, data.indexOf(DELIMITER, recentIndex))); } @Override public String getLast() { // // , f o o , ... , b a r , // 0 1 . . . ... . . . . ^ last // recentIndex = data.length() - DELIMITER.length(); return recentIndex == 0 ? null // \u2620 : CODEC.decode(data.substring(data.lastIndexOf(DELIMITER, recentIndex - 1) + DELIMITER.length(), recentIndex)); } @Override public String getNext(String x) { if (x == null) { return getFirst(); } // // if x = "foobar", then // , ... , f o o b a r , b a z q u u x , ... , // . . . ^ index . . . . ^ nextStart . ^ nextEnd // String coded = concat(DELIMITER, CODEC.encode(x), DELIMITER); int nextStart = findForward(coded); if (nextStart == data.length()) { return null; } else { int nextEnd = data.indexOf(DELIMITER, nextStart); return CODEC.decode(data.substring(nextStart, nextEnd)); } } @Override public String getPrevious(String x) { if (x == null) { return getLast(); } // // if x = "foobar", then // , ... , b a z q u u x , f o o b a r , ... , // . . . . ^ prevStart . ^ index / prevEnd // String coded = concat(DELIMITER, CODEC.encode(x), DELIMITER); int prevEnd = findBackward(coded); if (prevEnd == 0) { return null; } else { int prevStart = data.lastIndexOf(DELIMITER, prevEnd - 1) + DELIMITER.length(); return CODEC.decode(data.substring(prevStart, prevEnd)); } } @Override public boolean isEmpty() { // Being shorter than DELIMITER is impossible, due to data's invariant. return data.length() == DELIMITER.length(); } /** * Inserts a value before a reference item. * * @param ref reference value (or {@code null} for append) * @param x value to insert * @throws IllegalArgumentException if {@code x} is null, or {@code ref} is * non-null and not in this sequence. */ public void insertBefore(String ref, String x) { Preconditions.checkArgument(x != null, "null item"); if (ref == null) { data += CODEC.encode(x) + DELIMITER; } else { // if ref = "foobar", then // , ... , b a z q u u x , f o o b a r , ... , // . . . . . . . . . . . ^ refIndex String codedRef = concat(DELIMITER, CODEC.encode(ref), DELIMITER); int refIndex = findBackward(codedRef); data = concat(data.substring(0, refIndex), DELIMITER, CODEC.encode(x), data.substring(refIndex)); } } /** * Inserts a value after a reference item. * * @param ref reference value (or {@code null} for prepend) * @param x value to insert * @throws IllegalArgumentException if {@code x} is null, or {@code ref} is * non-null and not in this sequence. */ public void insertAfter(String ref, String x) { Preconditions.checkArgument(x != null, "null item"); if (ref == null) { data = DELIMITER + CODEC.encode(x) + data; } else { // if ref = "foobar", then // , ... , f o o b a r , b a z q u u x , ... , // . . . ^refIndex . . . ^refIndex' String codedRef = concat(DELIMITER, CODEC.encode(ref), DELIMITER); int refIndex = findForward(codedRef); data = concat(data.substring(0, refIndex), CODEC.encode(x), DELIMITER, data.substring(refIndex)); } } /** * Removes a value from this sequence. * * @param x * @throws IllegalArgumentException if {@code x} is null or not in this * sequence. */ public void remove(String x) { // if ref = "foobar", then // , ... , b a r , f o o , b a z , ... , // . . . . . . . ^refIndex Preconditions.checkArgument(x != null, "null item"); String coded = concat(DELIMITER, CODEC.encode(x), DELIMITER); int index = findBackward(coded); data = data.substring(0, index) + data.substring(index + coded.length() - DELIMITER.length()); // Reminder that the trailing delimiter is there. assert data.startsWith(DELIMITER, index); } /** * Clears all entries from this sequence. */ public void clear() { data = DELIMITER; recentIndex = 0; } /** @return the underyling data in which strings are embedded. */ public String getRaw() { return data; } }