/**
* 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.document.util;
import org.waveprotocol.wave.model.document.MutableAnnotationSet;
import org.waveprotocol.wave.model.document.ReadableAnnotationSet;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.Preconditions;
/**
* Annotation constants and utilities.
*
* Instruction to implementors regarding the prefix constants: With the
* exception of the local prefix (including its special character), these should
* be abstracted away from users of the annotation set interfaces, the
* implementation should silently insert the prefix where appropriate.
*/
public final class Annotations {
/** Static utility class, cannot be constructed. */
private Annotations() {}
/**
* Separator for annotation components
*
* The separator has no real semantic importance, except it is convention,
* and handler registry implementations tokenise based on this separator.
*/
public static final char SEPARATOR = '/';
/**
* All local annotations MUST begin with this prefix, and non-local
* annotations may NOT begin with this prefix.
*/
public static final char LOCAL = '@';
/**
* Convenience, returns a valid local key for a given suffix.
*
* @param suffix should not start with a numeral, to avoid clashing with keys
* generated by {@link #makeUniqueLocal(String)}
* @return the suffix with the local prefix and a slash prepended
*/
public static final String makeLocal(String suffix) {
return LOCAL + suffix;
}
/**
* Generates a globally unique (within the current run of the application)
* local key with the given suffix
*
* @param suffix main purpose is to make introspection of annotation set
* clearer
* @return a unique local key ending with the given suffix
*/
public static final String makeUniqueLocal(String suffix) {
return makeLocal((prefixIncrementor++) + "/" + suffix);
}
private static int prefixIncrementor;
/**
* Efficiently determines if a key is a local key.
*
* @param key the key to check
* @return true if the key is a local key
*/
public static final boolean isLocal(String key) {
return key.charAt(0) == LOCAL;
}
/**
* Prefix appended to keys of annotations that get sent to the server, but are
* not persisted in the database, for example, user selections. As such, this
* prefix merely represents an instruction to the server to act in this
* manner.
*/
public static final char TRANSIENT = '?';
/**
* Does a preconditions style check on a persistent annotation key.
*
* Checks basic properties. Does not do validity checks related to Unicode
* restrictions, those are left to the automaton.
*
* @param key
*/
public static void checkPersistentKey(String key) {
Preconditions.checkNotNull(key, "Annotation key may not be null");
if (isLocal(key)) {
Preconditions.illegalArgument(
"Attempt to use local annotation key '" + key + "' as a persistent key");
}
if (key.indexOf('@') != -1 || key.indexOf('?') != -1) {
Preconditions.illegalArgument(
"Persistent annotation key '" + key + "' contains invalid characters");
}
}
/**
* E.g. join("style", "fontWeight") becomes "style/fontWeight"
*
* @return the components joined by the annotation component separator
*/
public static String join(String first, String ... rest) {
return CollectionUtils.join(SEPARATOR, first, rest);
}
/**
* The same as {@link ReadableAnnotationSet#firstAnnotationChange(int, int, String, Object)},
* except that it returns {@code end} instead of -1 if there is no change
*/
public static <V> int firstAnnotationBoundary(ReadableAnnotationSet<V> annotations,
int start, int end, String key, V fromValue) {
int ret = annotations.firstAnnotationChange(start, end, key, fromValue);
return ret != -1 ? ret : end;
}
/**
* The same as {@link ReadableAnnotationSet#lastAnnotationChange(int, int, String, Object)},
* except that it returns {@code start} instead of -1 if there is no change
*/
public static <V> int lastAnnotationBoundary(ReadableAnnotationSet<V> annotations,
int start, int end, String key, V fromValue) {
int ret = annotations.lastAnnotationChange(start, end, key, fromValue);
return ret != -1 ? ret : start;
}
/**
* Resets an annotation range, but only if it's an actual useful operation.
* Parameters are the same as {@link MutableAnnotationSet#resetAnnotation}.
* @return true if the annotation reset was actually applied.
*/
public static <V> boolean guardedResetAnnotation(MutableAnnotationSet<V> annotations,
int start, int end, String key, V value) {
int size = annotations.size();
// check if there's nothing to do:
boolean isSet = annotations.firstAnnotationChange(0, start, key, null) == -1 &&
annotations.firstAnnotationChange(start, end, key, value) == -1 &&
annotations.firstAnnotationChange(end, size, key, null) == -1;
if (!isSet) {
// and only reset if there is
annotations.resetAnnotation(start, end, key, value);
}
return !isSet;
}
/**
* Gets the value for a particular annotation, on a given side (left/right) of a location.
*
* @param leftAlign whether to get the annotation to the left of the location, or to the right.
* @return the annotation value on one side of the start of a location.
*/
public static <V> V getAlignedAnnotation(MutableAnnotationSet<V> annotations, int location,
String key, boolean leftAlign) {
// split based on alignment:
if (leftAlign) {
if (location == 0) {
return null;
}
location--;
} else {
if (location == annotations.size()) {
return null;
}
}
return annotations.getAnnotation(location, key);
}
}