/* Copyright (c) 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 com.google.wave.api;
import com.google.wave.api.Function.BlipContentFunction;
import com.google.wave.api.Function.MapFunction;
import com.google.wave.api.JsonRpcConstant.ParamsProperty;
import com.google.wave.api.OperationRequest.Parameter;
import com.google.wave.api.impl.DocumentModifyAction;
import com.google.wave.api.impl.DocumentModifyQuery;
import com.google.wave.api.impl.DocumentModifyAction.BundledAnnotation;
import com.google.wave.api.impl.DocumentModifyAction.ModifyHow;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* A class that represents a set of references to contents in a blip.
*
* A {@link BlipContentRefs} instance for example can represent the
* results of a search, an explicitly set range, a regular expression, or refer
* to the entire blip.
*
* {@link BlipContentRefs} are used to express operations on a blip in a
* consistent way that can be easily transfered to the server.
*/
public class BlipContentRefs implements Iterable<Range> {
/** The blip that this blip references are pointing to. */
private final Blip blip;
/** The iterator to iterate over the blip content. */
private final BlipIterator<?> iterator;
/** The additional parameters that need to be supplied in the outgoing op. */
private final List<Parameter> parameters;
/**
* Constructs an instance representing the search for text {@code target}.
*
* @param blip the blip to find {@code target} in.
* @param target the target to search.
* @param maxResult the maximum number of results.
* @return an instance of blip references.
*/
public static BlipContentRefs all(Blip blip, String target, int maxResult) {
return new BlipContentRefs(blip,
new BlipIterator.TextIterator(blip, target, maxResult),
Parameter.of(ParamsProperty.MODIFY_QUERY, new DocumentModifyQuery(target, maxResult)));
}
/**
* Constructs an instance representing the search for element
* {@code ElementType}, that has the properties specified in
* {@code restrictions}.
*
* @param blip the blip to find {@code target} in.
* @param target the element type to search.
* @param maxResult the maximum number of results.
* @param restrictions the additional properties filter that need to be
* matched.
* @return an instance of blip references.
*/
public static BlipContentRefs all(Blip blip, ElementType target, int maxResult,
Restriction... restrictions) {
Map<String, String> restrictionsAsMap = new HashMap<String, String>(restrictions.length);
for (Restriction restriction : restrictions) {
restrictionsAsMap.put(restriction.getKey(), restriction.getValue());
}
return new BlipContentRefs(blip,
new BlipIterator.ElementIterator(blip, target, restrictionsAsMap, maxResult),
Parameter.of(ParamsProperty.MODIFY_QUERY,
new DocumentModifyQuery(target, restrictionsAsMap, maxResult)));
}
/**
* Constructs an instance representing the entire blip content.
*
* @param blip the blip to represent.
* @return an instance of blip references.
*/
public static BlipContentRefs all(Blip blip) {
return new BlipContentRefs(blip,
new BlipIterator.SingleshotIterator(blip, 0, blip.getContent().length()));
}
/**
* Constructs an instance representing an explicitly set range.
*
* @param blip the blip to represent.
* @param start the start index of the range.
* @param end the end index of the range.
* @return an instance of blip references.
*/
public static BlipContentRefs range(Blip blip, int start, int end) {
return new BlipContentRefs(blip,
new BlipIterator.SingleshotIterator(blip, start, end),
Parameter.of(ParamsProperty.RANGE, new Range(start, end)));
}
/**
* Private constructor.
*
* @param blip the blip to navigate.
* @param iterator the iterator to iterate over blip content.
* @param parameters the additional parameters to be passed in the outgoing
* operation.
*/
private BlipContentRefs(Blip blip, BlipIterator<?> iterator, Parameter... parameters) {
this.blip = blip;
this.iterator = iterator;
this.parameters = Arrays.asList(parameters);
}
/**
* Executes this BlipRefs object.
*
* @param modifyHow the operation to be executed.
* @param bundledAnnotations optional list of annotations to immediately
* apply to newly added text.
* @param arguments a list of arguments for the operation. The arguments vary
* depending on the operation:
* <ul>
* <li>For DELETE: none (the supplied arguments will be ignored)</li>
* <li>For ANNOTATE: a list of {@link Annotation} objects</li>
* <li>For CLEAR_ANNOTATION: the key of the annotation to be
* cleared. Only the first argument will be used.</li>
* <li>For UPDATE_ELEMENT: a list of {@link Map}, each represents
* new element properties.</li>
* <li>For INSERT, INSERT_AFTER, or REPLACE: a list of
* {@link BlipContent}s.
* </ul>
* For operations that take a list of entities as the argument, once
* this method hits the end of the argument list, it will wrap around.
* For example, if this {@link BlipContentRefs} object has 5 hits, when
* applying an ANNOTATE operation with 4 arguments, the first argument
* will be applied to the 5th hit.
* @return this instance of blip references, for chaining.
*/
@SuppressWarnings({"unchecked", "fallthrough"})
private BlipContentRefs execute(
ModifyHow modifyHow, List<BundledAnnotation> bundledAnnotations, Object... arguments) {
// If there is no match found, return immediately without generating op.
if (!iterator.hasNext()) {
return this;
}
int nextIndex = 0;
Object next = null;
List<BlipContent> computed = new ArrayList<BlipContent>();
List<Element> updatedElements = new ArrayList<Element>();
boolean useMarkup = false;
while (iterator.hasNext()) {
Range range = iterator.next();
int start = range.getStart();
int end = range.getEnd();
if (blip.length() == 0 && (start != 0 || end != 0)) {
throw new IndexOutOfBoundsException("Start and end have to be 0 for empty blip.");
} else if (start < 0 || end < 1) {
throw new IndexOutOfBoundsException("Position outside the blip.");
} else if ((start >= blip.length() || end > blip.length()) &&
modifyHow != ModifyHow.INSERT) {
throw new IndexOutOfBoundsException("Position outside the blip.");
} else if (start > blip.length() && modifyHow == ModifyHow.INSERT) {
throw new IndexOutOfBoundsException("Position outside the blip.");
} else if (start >= end){
throw new IndexOutOfBoundsException("Start has to be less than end.");
}
// Get the next argument.
if (nextIndex < arguments.length) {
next = arguments[nextIndex];
// If the next argument is a function, call the function.
if (next instanceof Function) {
// Get the matched content.
BlipContent source;
if (end - start == 1 && blip.getElements().containsKey(start)) {
source = blip.getElements().get(start);
} else {
source = Plaintext.of(blip.getContent().substring(start, end));
}
// Compute the new content.
next = ((Function) next).call(source);
if (next instanceof BlipContent) {
computed.add((BlipContent) next);
}
}
nextIndex = ++nextIndex % arguments.length;
}
switch (modifyHow) {
case DELETE:
// You can't delete the first newline.
if (start == 0) {
start = 1;
}
// Delete all elements that fall into this range.
Iterator<Integer> elementIterator =
blip.getElements().subMap(start, end).keySet().iterator();
while(elementIterator.hasNext()) {
elementIterator.next();
elementIterator.remove();
}
blip.deleteAnnotations(start, end);
blip.shift(end, start - end);
iterator.shift(-1);
blip.setContent(blip.getContent().substring(0, start) +
blip.getContent().substring(end));
break;
case ANNOTATE:
Annotation annotation = Annotation.class.cast(next);
blip.getAnnotations().add(annotation.getName(), annotation.getValue(), start, end);
break;
case CLEAR_ANNOTATION:
String annotationName = arguments[0].toString();
blip.getAnnotations().delete(annotationName, start, end);
break;
case UPDATE_ELEMENT:
Element existingElement = blip.getElements().get(start);
if (existingElement == null) {
throw new IllegalArgumentException("No element found at index " + start + ".");
}
Map<String, String> properties = Map.class.cast(next);
updatedElements.add(new Element(existingElement.getType(), properties));
for (Entry<String, String> entry : properties.entrySet()) {
existingElement.setProperty(entry.getKey(), entry.getValue());
}
break;
case INSERT:
end = start;
case INSERT_AFTER:
start = end;
case REPLACE:
// Get the plain-text version of next.
String text = BlipContent.class.cast(next).getText();
// Compute the shift amount for the iterator.
int iteratorShiftAmount = text.length() - 1;
if (end == start) {
iteratorShiftAmount += range.getEnd() - range.getStart();
}
iterator.shift(iteratorShiftAmount);
// In the case of a replace, and the replacement text is shorter,
// delete the delta.
if (start != end && text.length() < end - start) {
blip.deleteAnnotations(start + text.length(), end);
}
blip.shift(end, text.length() + start - end);
blip.setContent(blip.getContent().substring(0, start) + text +
blip.getContent().substring(end));
if (next instanceof Element) {
blip.getElements().put(start, Element.class.cast(next));
} else if (bundledAnnotations != null) {
for (BundledAnnotation bundled : bundledAnnotations) {
blip.getAnnotations().add(bundled.key, bundled.value, start, start + text.length());
}
}
break;
}
}
OperationRequest op = blip.getOperationQueue().modifyDocument(blip);
for (Parameter parameter : parameters) {
op.addParameter(parameter);
}
// Prepare the operation parameters.
List<String> values = null;
String annotationName = null;
List<Element> elements = null;
switch (modifyHow) {
case UPDATE_ELEMENT:
elements = updatedElements;
break;
case ANNOTATE:
values = new ArrayList<String>(arguments.length);
for (Object item : arguments) {
values.add(Annotation.class.cast(item).getValue());
}
annotationName = Annotation.class.cast(arguments[0]).getName();
break;
case CLEAR_ANNOTATION:
annotationName = arguments[0].toString();
break;
case INSERT:
case INSERT_AFTER:
case REPLACE:
values = new ArrayList<String>(arguments.length);
elements = new ArrayList<Element>(arguments.length);
Object[] toBeAdded = arguments;
if (arguments[0] instanceof Function) {
toBeAdded = computed.toArray();
}
for (Object argument : toBeAdded) {
if (argument instanceof Element) {
elements.add(Element.class.cast(argument));
values.add(null);
} else if (argument instanceof Plaintext){
values.add(BlipContent.class.cast(argument).getText());
elements.add(null);
}
}
break;
}
op.addParameter(Parameter.of(ParamsProperty.MODIFY_ACTION,
new DocumentModifyAction(
modifyHow, values, annotationName, elements, bundledAnnotations, useMarkup)));
iterator.reset();
return this;
}
/**
* Inserts the given arguments at the matched positions.
*
* @param arguments the new contents to be inserted.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insert(BlipContent... arguments) {
return insert(null, arguments);
}
/**
* Inserts computed contents at the matched positions.
*
* @param functions the functions to compute the new contents based on the
* matched contents.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insert(BlipContentFunction... functions) {
return insert(null, functions);
}
/**
* Inserts the given strings at the matched positions.
*
* @param arguments the new strings to be inserted.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insert(String... arguments) {
return insert(null, arguments);
}
/**
* Inserts the given arguments at the matched positions.
*
* @param bundledAnnotations annotations to immediately apply to the inserted
* text.
* @param arguments the new contents to be inserted.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insert(
List<BundledAnnotation> bundledAnnotations, BlipContent... arguments) {
return execute(ModifyHow.INSERT, bundledAnnotations, ((Object[]) arguments));
}
/**
* Inserts computed contents at the matched positions.
*
* @param bundledAnnotations annotations to immediately apply to the inserted
* text.
* @param functions the functions to compute the new contents based on the
* matched contents.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insert(
List<BundledAnnotation> bundledAnnotations, BlipContentFunction... functions) {
return execute(ModifyHow.INSERT, bundledAnnotations, ((Object[]) functions));
}
/**
* Inserts the given strings at the matched positions.
*
* @param bundledAnnotations annotations to immediately apply to the inserted
* text.
* @param arguments the new strings to be inserted.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insert(List<BundledAnnotation> bundledAnnotations, String... arguments) {
Object[] array = new Plaintext[arguments.length];
for (int i = 0; i < arguments.length; ++i) {
array[i] = Plaintext.of(arguments[i]);
}
return execute(ModifyHow.INSERT, bundledAnnotations, array);
}
/**
* Inserts the given arguments just after the matched positions.
*
* @param arguments the new contents to be inserted.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insertAfter(BlipContent... arguments) {
return insertAfter(null, arguments);
}
/**
* Inserts computed contents just after the matched positions.
*
* @param functions the functions to compute the new contents based on the
* matched contents.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insertAfter(BlipContentFunction... functions) {
return insertAfter(null, functions);
}
/**
* Inserts the given strings just after the matched positions.
*
* @param arguments the new strings to be inserted.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insertAfter(String... arguments) {
return insertAfter(null, arguments);
}
/**
* Inserts the given arguments just after the matched positions.
*
* @param bundledAnnotations annotations to immediately apply to the inserted
* text.
* @param arguments the new contents to be inserted.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insertAfter(
List<BundledAnnotation> bundledAnnotations, BlipContent... arguments) {
return execute(ModifyHow.INSERT_AFTER, bundledAnnotations, (Object[]) arguments);
}
/**
* Inserts computed contents just after the matched positions.
*
* @param bundledAnnotations annotations to immediately apply to the inserted
* text.
* @param functions the functions to compute the new contents based on the
* matched contents.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insertAfter(
List<BundledAnnotation> bundledAnnotations, BlipContentFunction... functions) {
return execute(ModifyHow.INSERT_AFTER, bundledAnnotations, (Object[]) functions);
}
/**
* Inserts the given strings just after the matched positions.
*
* @param bundledAnnotations annotations to immediately apply to the inserted
* text.
* @param arguments the new strings to be inserted.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs insertAfter(
List<BundledAnnotation> bundledAnnotations, String... arguments) {
Object[] array = new Plaintext[arguments.length];
for (int i = 0; i < arguments.length; ++i) {
array[i] = Plaintext.of(arguments[i]);
}
return execute(ModifyHow.INSERT_AFTER, bundledAnnotations, array);
}
/**
* Replaces the matched positions with the given arguments.
*
* @param arguments the new contents to replace the original contents.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs replace(BlipContent... arguments) {
return replace(null, arguments);
}
/**
* Replaces the matched positions with computed contents.
*
* @param functions the functions to compute the new contents.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs replace(BlipContentFunction... functions) {
return replace(null, functions);
}
/**
* Replaces the matched positions with the given strings.
*
* @param arguments the new strings to replace the original contents.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs replace(String... arguments) {
return replace(null, arguments);
}
/**
* Replaces the matched positions with the given arguments.
*
* @param bundledAnnotations annotations to immediately apply to the inserted
* text.
* @param arguments the new contents to replace the original contents.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs replace(
List<BundledAnnotation> bundledAnnotations, BlipContent... arguments) {
return execute(ModifyHow.REPLACE, bundledAnnotations, (Object[]) arguments);
}
/**
* Replaces the matched positions with computed contents.
*
* @param bundledAnnotations annotations to immediately apply to the inserted
* text.
* @param functions the functions to compute the new contents.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs replace(
List<BundledAnnotation> bundledAnnotations, BlipContentFunction... functions) {
return execute(ModifyHow.REPLACE, bundledAnnotations, (Object[]) functions);
}
/**
* Replaces the matched positions with the given strings.
*
* @param bundledAnnotations annotations to immediately apply to the inserted
* text.
* @param arguments the new strings to replace the original contents.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs replace(List<BundledAnnotation> bundledAnnotations, String... arguments) {
Object[] array = new Plaintext[arguments.length];
for (int i = 0; i < arguments.length; ++i) {
array[i] = Plaintext.of(arguments[i]);
}
return execute(ModifyHow.REPLACE, bundledAnnotations, array);
}
/**
* Deletes the contents at the matched positions.
*
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs delete() {
return execute(ModifyHow.DELETE, null);
}
/**
* Annotates the contents at the matched positions.
*
* @param key the annotation key.
* @param values the annotation values.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs annotate(String key, String... values) {
if (values.length == 0) {
values = new String[]{key};
}
Annotation[] annotations = new Annotation[values.length];
for (int i = 0; i < values.length; ++i) {
annotations[i] = new Annotation(key, values[i], 0, 1);
}
return execute(ModifyHow.ANNOTATE, null, (Object[]) annotations);
}
/**
* Clears the annotations at the matched positions.
*
* @param key the annotation key to be cleared.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs clearAnnotation(String key) {
return execute(ModifyHow.CLEAR_ANNOTATION, null, key);
}
/**
* Updates the properties of all elements at the matched positions with the
* given properties map.
*
* Note: The purpose of this overloaded version is because the version that
* takes a var-args generates compiler warning due to the way generics and
* var-args are implemented in Java. Most of the time, robot only needs to
* update one gadget at at time, and it can use this version to avoid the
* compiler warning.
*
* @param newProperties the new properties map.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs updateElement(Map<String, String> newProperties) {
return execute(ModifyHow.UPDATE_ELEMENT, null, new Object[] {newProperties});
}
/**
* Updates the properties of all elements at the matched positions with
* computed properties map.
*
* Note: The purpose of this overloaded version is because the version that
* takes a var-args generates compiler warning due to the way generics and
* var-args are implemented in Java. Most of the time, robot only needs to
* update one gadget at at time, and it can use this version to avoid the
* compiler warning.
*
* @param function the function to compute the new properties map.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs updateElement(MapFunction function) {
return execute(ModifyHow.UPDATE_ELEMENT, null, new Object[] {function});
}
/**
* Updates the properties of all elements at the matched positions with the
* given properties maps.
*
* @param newProperties an array of new properties map.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs updateElement(Map<String, String>... newProperties) {
return execute(ModifyHow.UPDATE_ELEMENT, null, (Object[]) newProperties);
}
/**
* Updates the properties of all elements at the matched positions with
* computed properties maps.
*
* @param functions an array of function to compute new properties maps.
* @return an instance of this blip references, for chaining.
*/
public BlipContentRefs updateElement(MapFunction... functions) {
return execute(ModifyHow.UPDATE_ELEMENT, null, (Object[]) functions);
}
/**
* Checks whether this blip references contains any matches or not.
*
* @return {@code true} if it has any more matches. Otherwise, returns
* {@code false}.
*/
public boolean isEmpty() {
return iterator.hasNext();
}
/**
* Returns all matches.
*
* @return a list of {@link BlipContent} that represents the hits.
*/
public List<BlipContent> values() {
List<BlipContent> result = new ArrayList<BlipContent>();
while (iterator.hasNext()) {
Range range = iterator.next();
if (range.getEnd() - range.getStart() == 1 &&
blip.getElements().containsKey(range.getStart())) {
result.add(blip.getElements().get(range.getStart()));
} else {
result.add(Plaintext.of(blip.getContent().substring(range.getStart(), range.getEnd())));
}
}
iterator.reset();
return result;
}
/**
* Returns the first hit.
*
* @return an instance of {@link BlipContent}, that represents the first hit.
*/
public BlipContent value() {
BlipContent result = null;
if (iterator.hasNext()) {
Range range = iterator.next();
if (range.getEnd() - range.getStart() == 1 &&
blip.getElements().containsKey(range.getStart())) {
result = blip.getElements().get(range.getStart());
} else {
result = Plaintext.of(blip.getContent().substring(range.getStart(), range.getEnd()));
}
}
iterator.reset();
return result;
}
@Override
public Iterator<Range> iterator() {
iterator.reset();
return iterator;
}
}