/** * 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.ReadableDocument; import org.waveprotocol.wave.model.document.ReadableWDocument; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.Preconditions; import java.util.Collections; import java.util.Map; import java.util.TreeMap; /** * Utilities for comparing documents. * * Mainly useful for testing - implementations are not necessarily efficient. * * @author danilatos@google.com (Daniel Danilatos) */ public final class DocCompare { /** * Document shape - presence of elements, size of text runs (still ignores * text node boundaries) */ public static final int SHAPE = 1 << 0; /** * Element types (tag names) */ public static final int TYPES = 1 << 1; /** * Element attribute names */ public static final int ATTR_NAMES = 1 << 2; /** * Subtract this to avoid caring about attribute values, only care about names */ public static final int ATTR_VALUES = 1 << 3; /** * Text data */ public static final int TEXT = 1 << 4; /** * Annotation keys, caring only whether the corresponding values are null or not */ public static final int ANNOTATION_KEYS = 1 << 5; /** * Subtract this to avoid caring about annotation values, only care about keys */ private static final int ANNOTATION_VALUES = 1 << 6; /** * Element attribute names and values. */ public static final int ATTRS = ATTR_NAMES | ATTR_VALUES; /** * All structure (still ignoring text node boundaries) */ public static final int STRUCTURE = SHAPE | TYPES | ATTRS | TEXT; /** * Annotation keys and values */ public static final int ANNOTATIONS = ANNOTATION_KEYS | ANNOTATION_VALUES; /** * Both structure and annotations */ public static final int ALL = STRUCTURE | ANNOTATIONS; /** * Normalises a document view based on what structural information we do and * do not care about */ private static class NormalisingView<N, E extends N, T extends N> extends IdentityView<N, E, T> { private final int flags; NormalisingView(ReadableDocument<N, E, T> inner, int flags) { super(inner); this.flags = flags; } @Override public String getTagName(E element) { return flag(TYPES) ? super.getTagName(element) : "x"; } @Override public String getAttribute(E element, String name) { if (flag(ATTR_VALUES)) { return super.getAttribute(element, name); } else if (flag(ATTR_NAMES)) { return super.getAttribute(element, name) != null ? "x" : null; } else { return null; } } @Override public Map<String,String> getAttributes(E element) { if (flag(ATTR_VALUES)) { // Ensure consistent order of keys return new TreeMap<String, String>(super.getAttributes(element)); } else if (flag(ATTR_NAMES)) { Map<String, String> copy = new TreeMap<String, String>(); for (String key : super.getAttributes(element).keySet()) { copy.put(key, "x"); } return copy; } else { return Collections.emptyMap(); } } @Override public String getData(T textNode) { String data = super.getData(textNode); return flag(TEXT) ? data : CollectionUtils.repeat('x', data.length()); } private boolean flag(int mask) { return (flags & mask) != 0; } } /** * Checks whether two documents are equivalent. * * @param flags specifies the aspects to compare for equivalence * @return true if the two documents are equivalent */ public static <N1, N2> boolean equivalent(final int flags, ReadableWDocument<N1, ?, ?> doc1, ReadableWDocument<N2, ?, ?> doc2) { return fungeEquivalent(flags, doc1, doc2); } private static <N1, E1 extends N1, T1 extends N1, N2, E2 extends N2, T2 extends N2> boolean fungeEquivalent( final int flags, ReadableWDocument<N1, E1, T1> doc1, ReadableWDocument<N2, E2, T2> doc2) { if ((flags & ANNOTATIONS) != 0) { throw new AssertionError("Annotations comparison not yet implemented"); } return structureEquivalent(flags & ~ANNOTATIONS, doc1, doc2) ;// && annotationsEquivalent(flags & ~STRUCTURE, doc1, doc2); } /** * Checks whether the structures of two documents are equivalent. * * Note: the documents' annotations are currently ignored, but this behaviour * should not be relied upon. * * @param flags specifies the aspects to compare for equivalence */ public static <N1, E1 extends N1, T1 extends N1, N2, E2 extends N2, T2 extends N2> boolean structureEquivalent( final int flags, ReadableDocument<N1, E1, T1> doc1, ReadableDocument<N2, E2, T2> doc2) { checkValidFlags(flags); ReadableDocument<N1, E1, T1> view1 = new NormalisingView<N1, E1, T1>(doc1, flags); ReadableDocument<N2, E2, T2> view2 = new NormalisingView<N2, E2, T2>(doc2, flags); return XmlStringBuilder.innerXml(view1).toString().equals( XmlStringBuilder.innerXml(view2).toString()); } /** * Checks whether a string is a representation of an equivalent document. The * string need not be normalized. * * Note: the documents' annotations are currently ignored, but this behaviour * should not be relied upon. * * @param str string for comparison * @param doc document for comparison * @param flags specifies the aspects to compare for equivalence * @return true if the string is a representation of an equivalent document */ public static <N, E extends N, T extends N> boolean equivalent( int flags, String str, ReadableDocument<N, E, T> doc) { return structureEquivalent(flags, DocProviders.POJO.parse(str), doc); } /** * Checks whether two strings are representations of equivalent documents. The * strings need not be normalized. * * @param flags specifies the aspects to compare for equivalence * @return true if the strings represent equivalent documents, based on the * types of things that are important as given by the flags */ public static <N, E extends N, T extends N> boolean equivalent(int flags, String a, String b) { return structureEquivalent(flags, DocProviders.POJO.parse(a), DocProviders.POJO.parse(b)); } /* TODO(danilatos) public static <V> boolean annotationsEquivalent( final int flags, ReadableAnnotationSet<V> doc1, ReadableAnnotationSet<V> doc2) { checkValidFlags(flags); Preconditions.checkArgument((flags & STRUCTURE) == 0, "ReadableAnnotationSet provides no access to structure"); throw new AssertionError("annotationsEquivalent not implemented"); } */ private static void checkValidFlags(int flags) { Preconditions.checkArgument(flags != 0, "flags must be non-empty"); Preconditions.checkArgument(implies(flags, TYPES, SHAPE), "flag TYPES must imply flag SHAPE"); Preconditions.checkArgument(implies(flags, ATTR_NAMES, SHAPE), "flag ATTR_NAMES must imply flag SHAPE"); Preconditions.checkArgument(implies(flags, TEXT, SHAPE), "flag TEXT must imply flag SHAPE"); Preconditions.checkArgument(implies(flags, ATTR_VALUES, ATTR_NAMES), "flag ATTR_VALUES must imply flag ATTR_NAMES"); Preconditions.checkArgument(implies(flags, ANNOTATION_VALUES, ANNOTATION_KEYS), "flag ANNOTATION_VALUES must imply flag ANNOTATION_KEYS"); } /** * The presence of the antecedent implies the presence of the consequent in the * flags parameter * @return true if the implication holds */ private static boolean implies(int flags, int antecedent, int consequent) { return (flags & antecedent) == 0 || (flags & consequent) != 0; } private DocCompare() { } }