/** * 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); } }