/** * 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.editor.extract; import org.waveprotocol.wave.model.document.AnnotationBehaviour; import org.waveprotocol.wave.model.document.AnnotationBehaviour.AnnotationFamily; import org.waveprotocol.wave.model.document.AnnotationBehaviour.BiasDirection; import org.waveprotocol.wave.model.document.AnnotationBehaviour.ContentType; import org.waveprotocol.wave.model.document.RangedAnnotation; import org.waveprotocol.wave.model.document.ReadableAnnotationSet; import org.waveprotocol.wave.model.document.ReadableWDocument; import org.waveprotocol.wave.model.document.operation.Nindo.Builder; import org.waveprotocol.wave.model.document.util.AnnotationRegistry; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.RangedAnnotationImpl; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.ReadableStringSet; import org.waveprotocol.wave.model.util.ReadableStringSet.Proc; import org.waveprotocol.wave.model.util.StringMap; import org.waveprotocol.wave.model.util.StringSet; import org.waveprotocol.wave.model.util.ValueUtils; import java.util.ArrayList; import java.util.List; /** * Handles various parts of annotation logic in terms of behaviour during paste operations. * This comprimises the following abilities: * - strip/unstrip annotation keys, given a current document state and position * where a content will be inserted by a paste. * - normalising annotations within a section of the document * */ public class PasteAnnotationLogic<N, E extends N, T extends N> { // Document where annotations are to be extracted. private final ReadableWDocument<N, E, T> doc; // All annotation logic private final AnnotationRegistry annotationLogic; /** * Constructs an AnnotationExtractor where normalized annotations can be * extracted. * * @param doc */ public PasteAnnotationLogic(ReadableWDocument<N, E, T> doc, AnnotationRegistry annotationLogic) { this.doc = doc; this.annotationLogic = annotationLogic; } // Logic for stripping keys for content about to be pasted /** * Strip keys at the start of a paste event, based on annotation behaviour defined. * Also returns the set of changed keys so they can be closed at the end of the paste. * * @param annotationSet Known annotations * @param position Where in the document the content is being inserted * @param cursorBias Current cursor bias * @param type Type of content being inserted * @param builder Nindo builder for the annotation ops to be sent to. * @return key->value map for all annotations whose inherited value isn't what is currently used. */ public StringMap<String> stripKeys(final ReadableAnnotationSet<String> annotationSet, final int position, final BiasDirection cursorBias, final ContentType type, final Builder builder) { // NOTE(patcoleman): currently all that is used is type = RICH_TEXT // when PLAIN_TEXT is hooked up, please make sure it's well tested. // Likewise, with cursorBias = RIGHT, as all that's used & tested so far is cursorBias = LEFT // calculate annotation maps before and after final StringMap<Object> before = CollectionUtils.createStringMap(); final StringMap<Object> after = CollectionUtils.createStringMap(); annotationSet.knownKeys().each(new Proc() { @Override public void apply(String key) { if (position > 0) { Object beforeV = annotationSet.getAnnotation(position - 1, key); if (beforeV != null) { before.put(key, beforeV); } } if (position< annotationSet.size()) { Object afterV = annotationSet.getAnnotation(position, key); if (afterV != null) { after.put(key, afterV); } } } }); // assign them to inside/outside the cursor based on bias: final StringMap<Object> inside = (cursorBias == BiasDirection.RIGHT ? after : before); final StringMap<Object> outside = (cursorBias == BiasDirection.RIGHT ? before : after); final StringMap<String> changedAnnotations = CollectionUtils.createStringMap(); annotationSet.knownKeys().each(new Proc() { @Override public void apply(String key) { interpretReplace(key, type, builder, inside, outside, before, changedAnnotations); } }); return changedAnnotations; } /** Mirror of stripKeys, called at the end of mutation to close annotations. */ public void unstripKeys(final Builder builder, final ReadableStringSet stripKeys, final ReadableStringSet ignoreSet) { // NOTE(patcoleman) - maybe worth adding a set minus here. stripKeys.subtract(ignoreSet).each( stripKeys.each(new Proc() { @Override public void apply(String element) { if (!ignoreSet.contains(element)) { builder.endAnnotation(element); } }}); } /** Interpret a replacement based on the annotation behaviour, return true if it was set. */ private boolean interpretReplace(String key, ContentType type, Builder builder, StringMap<Object> inside, StringMap<Object> outside, StringMap<Object> current, StringMap<String> changeCollector) { AnnotationBehaviour logic = annotationLogic.getClosestBehaviour(key); if (logic != null) { switch (logic.replace(inside, outside, type)) { case INSIDE : return safeSet(builder, key, inside.get(key), current.get(key), changeCollector); case OUTSIDE : return safeSet(builder, key, outside.get(key), current.get(key), changeCollector); case NEITHER : return safeSet(builder, key, null, current.get(key), changeCollector); } } return false; } /** Utility to set an annotation key if not already set, returns true if it is set. */ private boolean safeSet(Builder builder, String key, Object newValue, Object oldValue, StringMap<String> changeCollector) { String newString = newValue == null ? null : newValue.toString(); String oldString = oldValue == null ? null : oldValue.toString(); if (ValueUtils.notEqual(newString, oldString)) { builder.startAnnotation(key, newString); changeCollector.put(key, newString); return true; } else { return false; } } // Logic for normalizations of annotations in content about to be pasted /** * Extract and normalizes annotations inside a given range for the associated * document. * * By normalize, the position starting at the given range, will be normalized * to position 0, and the annotations are bounded by the size of the range. * * @param normalizedStart * @param normalizedEnd */ public List<RangedAnnotation<String>> extractNormalizedAnnotation(Point<N> normalizedStart, Point<N> normalizedEnd) { int start = doc.getLocation(normalizedStart); int end = doc.getLocation(normalizedEnd); ReadableStringSet interested = filterContentAnnotations(doc.knownKeys()); return trimAnnotations(doc.rangedAnnotations(start, end, interested), start, end - start); } /** * Normalizes the annotations in the range (offset, offset + length) to the * range (0, length) * * @param rangedAnnotations * @param offset * @param length */ private static List<RangedAnnotation<String>> trimAnnotations( Iterable<RangedAnnotation<String>> rangedAnnotations, int offset, int length) { List<RangedAnnotation<String>> ret = new ArrayList<RangedAnnotation<String>>(); for (RangedAnnotation<String> ann : rangedAnnotations) { int nStart = Math.max(0, ann.start() - offset); int nEnd = Math.min(ann.end() - offset, length); ret.add(new RangedAnnotationImpl<String>(ann.key(), ann.value(), nStart, nEnd)); } return ret; } /** * Given a set of keys, return a subset that starts with prefixes in the * whitelist. * * @param known */ private ReadableStringSet filterContentAnnotations(ReadableStringSet known) { final StringSet interested = CollectionUtils.createStringSet(); known.each(new Proc() { @Override public void apply(final String key) { AnnotationBehaviour behaviour = annotationLogic.getClosestBehaviour(key); if (behaviour != null && behaviour.getAnnotationFamily() == AnnotationFamily.CONTENT) { interested.add(key); } } }); return interested; } }