http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/OpBasedWaveletFactory.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/OpBasedWaveletFactory.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/OpBasedWaveletFactory.java
new file mode 100644
index 0000000..a2011bf
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/OpBasedWaveletFactory.java
@@ -0,0 +1,194 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.testing;
+
+import org.waveprotocol.wave.model.id.IdGenerator;
+import org.waveprotocol.wave.model.id.WaveId;
+import org.waveprotocol.wave.model.id.WaveletId;
+import org.waveprotocol.wave.model.operation.OperationException;
+import org.waveprotocol.wave.model.operation.OperationRuntimeException;
+import org.waveprotocol.wave.model.operation.SilentOperationSink;
+import org.waveprotocol.wave.model.operation.SilentOperationSink.Executor;
+import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
+import org.waveprotocol.wave.model.schema.SchemaProvider;
+import org.waveprotocol.wave.model.version.HashedVersion;
+import org.waveprotocol.wave.model.wave.ParticipantId;
+import org.waveprotocol.wave.model.wave.data.DocumentFactory;
+import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
+import org.waveprotocol.wave.model.wave.data.WaveletData;
+import org.waveprotocol.wave.model.wave.data.impl.EmptyWaveletSnapshot;
+import 
org.waveprotocol.wave.model.wave.data.impl.ObservablePluggableMutableDocument;
+import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl;
+import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet;
+import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl;
+
+/**
+ * Factory for creating {@link OpBasedWavelet} instances suitable for testing.
+ *
+ */
+public final class OpBasedWaveletFactory implements 
WaveViewImpl.WaveletFactory<OpBasedWavelet>,
+    Factory<OpBasedWavelet> {
+
+  /**
+   * An operation sink that, on every operation it consumes, fires a version
+   * update operation back to the wavelet, then passes the operation along to
+   * the next sink. Wavelet versioning is specifically designed to be
+   * server-controlled. In a test context, this sink is used to simulate the
+   * behaviour of a wavelet server firing back acknowledgements with version
+   * updates, in order that tests that mutate wavelets also see version number
+   * increase.
+   */
+  private final static class VersionIncrementingSink implements
+      SilentOperationSink<WaveletOperation> {
+    private final WaveletData data;
+    private final SilentOperationSink<? super WaveletOperation> output;
+
+    public VersionIncrementingSink(WaveletData data,
+        SilentOperationSink<? super WaveletOperation> output) {
+      this.data = data;
+      this.output = output;
+    }
+
+    @Override
+    public void consume(WaveletOperation op) {
+      // Update local version, simulating server response.
+      try {
+        op.createVersionUpdateOp(1, null).apply(data);
+      } catch (OperationException e) {
+        throw new OperationRuntimeException("test sink verison update failed", 
e);
+      }
+
+      // Pass to output sink.
+      output.consume(op);
+    }
+  }
+
+  /**
+   * Builder, through which a factory can be conveniently configured.
+   */
+  public final static class Builder {
+    private final SchemaProvider schemas;
+    private ObservableWaveletData.Factory<?> holderFactory;
+    private SilentOperationSink<? super WaveletOperation> sink;
+    private ParticipantId author;
+
+    public Builder(SchemaProvider schemas) {
+      this.schemas = schemas;
+    }
+
+    public Builder with(SilentOperationSink<? super WaveletOperation> sink) {
+      this.sink = sink;
+      return this;
+    }
+
+    public Builder with(ObservableWaveletData.Factory<?> holderFactory) {
+      this.holderFactory = holderFactory;
+      return this;
+    }
+
+    public Builder with(ParticipantId author) {
+      this.author = author;
+      return this;
+    }
+
+    public OpBasedWaveletFactory build() {
+      if (holderFactory == null) {
+        DocumentFactory<?> docFactory = 
ObservablePluggableMutableDocument.createFactory(schemas);
+        holderFactory = WaveletDataImpl.Factory.create(docFactory);
+      }
+      if (sink == null) {
+        sink = SilentOperationSink.VOID;
+      }
+      if (author == null) {
+        // Old tests expect this.
+        author = FAKE_PARTICIPANT;
+      }
+      return new OpBasedWaveletFactory(holderFactory, sink, author);
+    }
+  }
+
+  private static final ParticipantId FAKE_PARTICIPANT = new 
ParticipantId("f...@example.com");
+
+  // Parameters with which to create the OpBasedWavelets.
+  private final ObservableWaveletData.Factory<?> holderFactory;
+  private final SilentOperationSink<? super WaveletOperation> sink;
+  private final ParticipantId author;
+
+  // Testing hacks.
+  private MockWaveletOperationContextFactory lastContextFactory;
+  private MockParticipationHelper lastAuthoriser;
+
+  /**
+   * Creates a factory, which creates op-based waves that adapt wave data
+   * holders provided by another factory, sending produced operations to a 
given
+   * sink.
+   *
+   * @param holderFactory factory for providing wave data holders
+   * @param sink sink to which produced operations are sent
+   * @param author id to which edits are to be attributed
+   */
+  private OpBasedWaveletFactory(ObservableWaveletData.Factory<?> holderFactory,
+      SilentOperationSink<? super WaveletOperation> sink,
+      ParticipantId author) {
+    this.holderFactory = holderFactory;
+    this.sink = sink;
+    this.author = author;
+  }
+
+  public static Builder builder(SchemaProvider schemas) {
+    return new Builder(schemas);
+  }
+
+  @Override
+  public OpBasedWavelet create() {
+    IdGenerator gen = FakeIdGenerator.create();
+    return create(gen.newWaveId(), gen.newConversationWaveletId(), 
FAKE_PARTICIPANT);
+  }
+
+  @Override
+  public OpBasedWavelet create(WaveId waveId, WaveletId waveletId, 
ParticipantId creator) {
+    long now = System.currentTimeMillis();
+    HashedVersion v0 = HashedVersion.unsigned(0);
+    ObservableWaveletData waveData = holderFactory
+        .create(new EmptyWaveletSnapshot(waveId, waveletId, creator, v0, now));
+    lastContextFactory = new 
MockWaveletOperationContextFactory().setParticipantId(author);
+    lastAuthoriser = new MockParticipationHelper();
+    SilentOperationSink<WaveletOperation> executor =
+        Executor.<WaveletOperation, WaveletData>build(waveData);
+    SilentOperationSink<WaveletOperation> out = new 
VersionIncrementingSink(waveData, sink);
+    return new OpBasedWavelet(waveId, waveData, lastContextFactory, 
lastAuthoriser, executor, out);
+  }
+
+  /**
+   * Gets the authoriser provided to help the last {@link OpBasedWavelet} that
+   * was created. The result is undefined if no wavelets have been created.
+   */
+  public MockParticipationHelper getLastAuthoriser() {
+    return lastAuthoriser;
+  }
+
+  /**
+   * Gets the helper provided to the last {@link OpBasedWavelet} that was
+   * created. The result is undefined if no wavelets have been created.
+   */
+  public MockWaveletOperationContextFactory getLastContextFactory() {
+    return lastContextFactory;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/OpMatchers.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/OpMatchers.java 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/OpMatchers.java
new file mode 100644
index 0000000..8c17f5c
--- /dev/null
+++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/OpMatchers.java
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.testing;
+
+import org.waveprotocol.wave.model.operation.wave.AddParticipant;
+import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation;
+import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Hamcrest matchers for CWM operations. Many of these are for use in JMock
+ * tests as replacements for the deprecated and broken
+ * {@link org.jmock.Expectations#a(Class)} and non-typesafe alternative
+ * {@link org.hamcrest.Matchers#instanceOf(Class)}.
+ *
+ */
+public class OpMatchers {
+  /**
+   * Alternative to Matchers.a(AddParticipant.class) since JMock/Hamcrest's
+   * implementation is deprecated due to being broken in Java 5 and 6.
+   */
+  public static Matcher<WaveletOperation> addParticipantOperation() {
+    return new BaseMatcher<WaveletOperation>() {
+      @Override
+      public boolean matches(Object obj) {
+        return obj instanceof AddParticipant;
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(" instanceof AddParticipant");
+      }
+    };
+  }
+
+  /** Creates a matcher for operations created by the given author. */
+  public static Matcher<WaveletOperation> opBy(final String author) {
+    return new TypeSafeMatcher<WaveletOperation>() {
+      @Override
+      public boolean matchesSafely(WaveletOperation op) {
+        return author.equals(op.getContext().getCreator().getAddress());
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(" op created by " + author);
+      }
+    };
+  }
+
+  /**
+   * Alternative to Matchers.a(WaveletBlipOperation.class) since
+   * JMock/Hamcrest's implementation is deprecated due to being broken in Java 
5
+   * and 6.
+   */
+  public static Matcher<WaveletOperation> waveletBlipOperation() {
+    return new BaseMatcher<WaveletOperation>() {
+      @Override
+      public boolean matches(Object obj) {
+        return obj instanceof WaveletBlipOperation;
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(" instanceof WaveletBlipOperation");
+      }
+    };
+  }
+
+  /** Uninstantiable. */
+  private OpMatchers() {
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomDocOpGenerator.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomDocOpGenerator.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomDocOpGenerator.java
new file mode 100644
index 0000000..3db3715
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomDocOpGenerator.java
@@ -0,0 +1,1421 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.testing;
+
+import org.waveprotocol.wave.model.document.bootstrap.BootstrapDocument;
+import org.waveprotocol.wave.model.document.operation.Attributes;
+import org.waveprotocol.wave.model.document.operation.AttributesUpdate;
+import org.waveprotocol.wave.model.document.operation.DocOp;
+import org.waveprotocol.wave.model.document.operation.DocOpCursor;
+import 
org.waveprotocol.wave.model.document.operation.automaton.AutomatonDocument;
+import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton;
+import 
org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ValidationResult;
+import 
org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ViolationCollector;
+import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
+import 
org.waveprotocol.wave.model.document.operation.impl.AnnotationBoundaryMapImpl;
+import org.waveprotocol.wave.model.document.operation.impl.AnnotationMap;
+import 
org.waveprotocol.wave.model.document.operation.impl.AttributesUpdateImpl;
+import 
org.waveprotocol.wave.model.document.operation.impl.DocInitializationBuilder;
+import org.waveprotocol.wave.model.document.operation.impl.DocOpBuffer;
+import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil;
+import org.waveprotocol.wave.model.document.operation.impl.DocOpValidator;
+import org.waveprotocol.wave.model.operation.OperationException;
+import 
org.waveprotocol.wave.model.testing.RandomDocOpGenerator.Parameters.AnnotationOption;
+import org.waveprotocol.wave.model.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Generates random document operations based on a document.  They can be
+ * valid or invalid, depending on parameters.
+ */
+public final class RandomDocOpGenerator {
+
+  /**
+   * Random number generator interface, to avoid the dependency on 
java.util.Random,
+   * which would prevent the use of this class with GWT.
+   */
+  public interface RandomProvider {
+    /** @returns a pseudorandom non-negative integer smaller than upperBound */
+    int nextInt(int upperBound);
+
+    /** @returns a pseudorandom boolean */
+    boolean nextBoolean();
+  }
+
+  private RandomDocOpGenerator() {}
+
+  /** Parameters for random DocOp generation. */
+  public static final class Parameters {
+
+    /**
+     * An annotation key with the corresponding list of value alternatives.
+     */
+    public static final class AnnotationOption {
+      final String key;
+      final List<String> valueAlternatives;
+
+      public AnnotationOption(String key, List<String> valueAlternatives) {
+        Preconditions.checkNotNull(key, "key must not be null");
+        Preconditions.checkNotNull(valueAlternatives, "valueAlternatives must 
not be null");
+        this.key = key;
+        this.valueAlternatives = valueAlternatives;
+      }
+
+      public String getKey() {
+        return key;
+      }
+
+      public String randomValue(RandomProvider r) {
+        return randomElement(r, valueAlternatives);
+      }
+    }
+
+    int maxOpeningComponents = 16;
+    int maxInsertLength = 10;
+    int maxDeleteLength = 5;
+    boolean valid = true;
+    // only relevant when producing invalid ops.
+    int maxSkipAfterEnd = 5;
+
+    // We use lists here instead of sets to have an explicit fixed ordering,
+    // which helps reproducibility when generating pseudo-random operations.
+    // SortedSets would also work for this, but then we'd have to make
+    // AnnotationOptions comparable, which is more work.
+
+    List<String> elementTypes = Collections.unmodifiableList(Arrays.asList(
+        "body", "line", "input",
+        "image", "caption", "br"// "gadget",
+        ));
+    List<String> attributeNames = Collections.unmodifiableList(Arrays.asList(
+        "_t", "t", "i", "attachment",
+        "style", "blipId", "state", "url", "fontWeight", "fontStyle", 
"invalid_dummy"));
+    // TODO: We should make attributeValues dependent on attributeNames (and 
perhaps on element
+    // types) so that we can randomly insert chess gadgets with a valid state 
and inline images
+    // with a proper attachment spec.
+    //
+    // updateAttributes will only generate attribute removals if null is in 
this list.
+    List<String> attributeValues = Collections.unmodifiableList(Arrays.asList(
+        null, "title", "li",
+        "h1", "h2", "h3", "h4", "",
+        "0", "1", "2", "3", "4", "5", "114", "9817"));
+
+    List<AnnotationOption> annotationOptions = Collections.unmodifiableList(
+        Arrays.asList(
+            new AnnotationOption("a", Arrays.asList(null, "1", "2")),
+            new AnnotationOption("b", Arrays.asList(null, "1")),
+            new AnnotationOption("c", Arrays.asList(null, "1"))
+        ));
+
+    public static final List<AnnotationOption> RENDERABLE_ANNOTATION_OPTIONS =
+        Collections.unmodifiableList(Arrays.asList(
+            new AnnotationOption("link/auto",
+                Arrays.asList(null,
+                    "http://www.youtube.com/watch?v=NBplLTBBmiA&feature=hd";,
+                    "http://code.google.com/p/wave-protocols/issues/entry";)),
+            new AnnotationOption("style/fontWeight", Arrays.asList(null, 
"bold")),
+            new AnnotationOption("style/textDecoration", Arrays.asList(null, 
"underline"))
+        ));
+
+
+    public List<String> attributeValues() {
+      return Collections.unmodifiableList(Arrays.asList("title", "li", "h1", 
"h2", "h3", "h4", "",
+          "0", "1", "2", "3", "4", "5", "114", "9817"));
+    }
+
+
+    public Parameters() {
+    }
+
+    public int getMaxOpeningComponents() {
+      return maxOpeningComponents;
+    }
+
+    /**
+     * @return the maxInsertLength
+     */
+    public int getMaxInsertLength() {
+      return maxInsertLength;
+    }
+
+    /**
+     * @return the maxDeleteLength
+     */
+    public int getMaxDeleteLength() {
+      return maxDeleteLength;
+    }
+
+    /**
+     * @return the annotationOptions
+     */
+    public List<AnnotationOption> getAnnotationOptions() {
+      return Collections.unmodifiableList(annotationOptions);
+    }
+
+    public Parameters setMaxOpeningComponents(int maxOpeningComponents) {
+      this.maxOpeningComponents = maxOpeningComponents;
+      return this;
+    }
+
+    /**
+     * @param maxInsertLength the maxInsertLength to set
+     */
+    public Parameters setMaxInsertLength(int maxInsertLength) {
+      this.maxInsertLength = maxInsertLength;
+      return this;
+    }
+
+    /**
+     * @param maxDeleteLength the maxDeleteLength to set
+     */
+    public Parameters setMaxDeleteLength(int maxDeleteLength) {
+      this.maxDeleteLength = maxDeleteLength;
+      return this;
+    }
+
+    /**
+     * @param annotationOptions the annotationOptions to set
+     */
+    public Parameters setAnnotationOptions(List<AnnotationOption> 
annotationOptions) {
+      this.annotationOptions = annotationOptions;
+      return this;
+    }
+
+    // Gotta love auto-generated javadoc.
+    /**
+     * @return the valid
+     */
+    public boolean getValidity() {
+      return valid;
+    }
+
+    /**
+     * @param valid the valid to set
+     */
+    public Parameters setValidity(boolean valid) {
+      this.valid = valid;
+      return this;
+    }
+
+    public int getMaxSkipAfterEnd() {
+      return maxSkipAfterEnd;
+    }
+
+    public Parameters setMaxSkipBeyondEnd(int maxSkipAfterEnd) {
+      this.maxSkipAfterEnd = maxSkipAfterEnd;
+      return this;
+    }
+
+    /**
+     * Returns the list of keys from annotationOptions.
+     */
+    public List<String> getAnnotationKeys() {
+      List<String> keys = new ArrayList<String>(annotationOptions.size());
+      for (AnnotationOption o : annotationOptions) {
+        keys.add(o.key);
+      }
+      return Collections.unmodifiableList(keys);
+    }
+
+    public List<String> getElementTypes() {
+      return elementTypes;
+    }
+
+    public Parameters setElementTypes(List<String> elementTypes) {
+      this.elementTypes = elementTypes;
+      return this;
+    }
+
+    public List<String> getAttributeNames() {
+      return attributeNames;
+    }
+
+    public Parameters setAttributeNames(List<String> attributeNames) {
+      Preconditions.checkArgument(
+          new HashSet<String>(attributeNames).size() == attributeNames.size(),
+          "duplicate attribute name");
+      this.attributeNames = attributeNames;
+      return this;
+    }
+
+    public List<String> getAttributeValues() {
+      return attributeValues;
+    }
+
+    public Parameters setAttributeValues(List<String> attributeValues) {
+      this.attributeValues = attributeValues;
+      return this;
+    }
+
+  }
+
+  private static <T> T randomElement(RandomProvider r, List<T> l) {
+    return l.get(r.nextInt(l.size()));
+  }
+
+  private static int randomIntFromRange(RandomProvider r, int min, int limit) {
+    assert 0 <= min; // not really a precondition, but true in our case
+    assert min < limit;
+
+    int x = r.nextInt(limit - min) + min;
+    assert min <= x;
+    assert x < limit;
+    return x;
+  }
+
+  private static <T> void swap(ArrayList<T> a, int i, int j) {
+    T temp = a.get(i);
+    a.set(i, a.get(j));
+    a.set(j, temp);
+  }
+
+  private static void shuffle(RandomProvider r, ArrayList<?> a) {
+    int N = a.size();
+    for (int i = 0; i < N; i++) {
+      int j = randomIntFromRange(r, i, N);
+      swap(a, i, j);
+    }
+  }
+
+
+  private interface Mapper<I, O> {
+    O map(I in);
+  }
+
+  private static <I, O> O pickRandomNonNullMappedElement(RandomProvider r, 
List<I> in,
+      Mapper<I, O> mapper) {
+    List<I> list = new ArrayList<I>(in);
+    while (!list.isEmpty()) {
+      int index = randomIntFromRange(r, 0, list.size());
+      O value = mapper.map(list.get(index));
+      if (value != null) {
+        return value;
+      }
+      // Remove element efficiently by swapping in an element from the end.
+      list.set(index, list.get(list.size() - 1));
+      list.remove(list.size() - 1);
+    }
+    return null;
+  }
+
+
+  private static class Generator {
+
+    abstract class RandomizerOperationComponent {
+      abstract ValidationResult check(DocOpAutomaton a, ViolationCollector v);
+      abstract void apply(DocOpAutomaton a);
+      abstract void output(DocOpCursor c);
+      boolean isAnnotationBoundary() { return false; }
+    }
+
+    enum Stage {
+      // all components are permitted
+      S1_UNRESTRICTED,
+      // if deletion stack and insertion stack are empty, permit nothing (go 
to next stage).
+      // while deletion stack is nonempty, permit annotation boundaries, 
deleteCharacters,
+      // deleteElementStarts and deleteElementEnds.  Must move on to next 
stage as soon as
+      // deletion stack becomes empty.
+      // while insertion stack is nonempty, permit elementEnds.
+      S2_CLOSE_STRUCTURE,
+      // if annotations are open, close them
+      S3_CLOSE_ANNOTATIONS,
+      // if not at end of document, assert invalidity and skip to end of 
document.
+      S4_SKIP_TO_END;
+    }
+
+    abstract class RandomOperationComponentGenerator {
+      // returns null if it couldn't generate a matching component
+      abstract RandomizerOperationComponent generate(DocOpAutomaton a, boolean 
valid, Stage stage);
+    }
+
+    class SkipGenerator extends RandomOperationComponentGenerator {
+      @SuppressWarnings("fallthrough")
+      @Override
+      RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, 
Stage stage) {
+        final int distance;
+        switch (stage) {
+          case S1_UNRESTRICTED:
+            int maxDistance = a.maxRetainItemCount();
+            if (maxDistance == 0) {
+              return null;
+            }
+            if (a.checkRetain(1, null).isIllFormed()) {
+              return null;
+            }
+            if (valid) {
+              if (!a.checkRetain(1, null).isValid()) {
+                return null;
+              }
+              int d = randomIntFromRange(r, 1, maxDistance + 1);
+              while (!a.checkRetain(d, null).isValid()) {
+                d--;
+                assert d > 0;
+              }
+              distance = d;
+              assert a.checkRetain(distance, null).isValid();
+            } else {
+              distance = randomIntFromRange(r, maxDistance + 1, maxDistance + 
p.getMaxSkipAfterEnd());
+              assert a.checkRetain(distance, null) == 
ValidationResult.INVALID_DOCUMENT;
+            }
+            break;
+          case S2_CLOSE_STRUCTURE:
+          case S3_CLOSE_ANNOTATIONS:
+            return null;
+          case S4_SKIP_TO_END:
+            if (!valid) {
+              throw new RuntimeException("Not implemented");
+            }
+            switch (a.checkRetain(1, null)) {
+              case INVALID_DOCUMENT:
+                assert a.checkFinish(null).isValid();
+                return null;
+              case VALID:
+                distance = a.maxRetainItemCount();
+                assert distance > 0;
+                assert !a.checkFinish(null).isValid();
+                break;
+              case INVALID_SCHEMA:
+              case ILL_FORMED: assert false;
+              default:
+                throw new RuntimeException("Unexpected validation result");
+            }
+            break;
+          default:
+            throw new RuntimeException("Unexpected stage: " + stage);
+        }
+        return new RandomizerOperationComponent() {
+          @Override
+          public ValidationResult check(DocOpAutomaton a, ViolationCollector 
v) {
+            return a.checkRetain(distance, v);
+          }
+
+          @Override
+          public void apply(DocOpAutomaton a) {
+            a.doRetain(distance);
+          }
+
+          @Override
+          public void output(DocOpCursor c) {
+            c.retain(distance);
+          }
+
+          @Override
+          public String toString() {
+            return "Skip(" + distance + ")";
+          }
+        };
+      }
+    }
+
+    class CharactersGenerator extends RandomOperationComponentGenerator {
+      @Override
+      RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, 
Stage stage) {
+        if (stage != Stage.S1_UNRESTRICTED) {
+          return null;
+        }
+        ValidationResult v = a.checkCharacters("a", null);
+        if (v.isIllFormed()) {
+          return null;
+        }
+        int count;
+        if (valid) {
+          if (!v.isValid()) {
+            return null;
+          }
+          // TODO: implement this once we have size limits.
+          int max = p.getMaxInsertLength();
+          if (max == 0) {
+            return null;
+          }
+          count = randomIntFromRange(r, 1, max + 1);
+        } else {
+          if (v.isValid()) {
+            // Exceed length of document (if p.maxInsertLength allows it).
+            int max = p.getMaxInsertLength();
+            // TODO: implement this once we have size limits.
+            //count = randomIntFromRange(r, min, max + 1);
+            return null;
+          } else {
+            count = randomIntFromRange(r, 1, p.getMaxInsertLength());
+          }
+        }
+        StringBuilder sb = new StringBuilder();
+        assert count > 0;
+        char startChar = r.nextBoolean() ? 'a' : 'A';
+        for (int i = 0; i < count; i++) {
+          if (i <= 26) {
+            sb.append((char) (startChar + i));
+          } else {
+            sb.append('.');
+          }
+        }
+        final String s = sb.toString();
+        return new RandomizerOperationComponent() {
+          @Override
+          public ValidationResult check(DocOpAutomaton a, ViolationCollector 
v) {
+            return a.checkCharacters(s, v);
+          }
+
+          @Override
+          public void apply(DocOpAutomaton a) {
+            a.doCharacters(s);
+          }
+
+          @Override
+          public void output(DocOpCursor c) {
+            c.characters(s);
+          }
+
+          @Override
+          public String toString() {
+            return "Characters(" + s + ")";
+          }
+        };
+      }
+    }
+
+    class DeleteCharactersGenerator extends RandomOperationComponentGenerator {
+      @Override
+      RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, 
Stage stage) {
+        if (stage != Stage.S1_UNRESTRICTED && (stage != 
Stage.S2_CLOSE_STRUCTURE || a.deletionStackComplexityMeasure() == 0)) {
+          return null;
+        }
+        // TODO: In stage 2, this should perhaps be less random about how many 
characters
+        // it deletes.  Alternatively, skip in stage 4 could be more random.
+        int nextChar = a.nextChar(0);
+        if (nextChar == -1 ||
+            a.checkDeleteCharacters("" + ((char) nextChar), 
null).isIllFormed()) {
+          return null;
+        }
+        final int count;
+        if (valid) {
+          int max = Math.min(a.maxCharactersToDelete(), 
p.getMaxDeleteLength());
+          if (max == 0) {
+            return null;
+          }
+          count = randomIntFromRange(r, 1, max + 1);
+        } else {
+          int max = p.getMaxDeleteLength();
+          int min = a.maxCharactersToDelete() + 1;
+          if (min > max) {
+            return null;
+          }
+          count = randomIntFromRange(r, min, max + 1);
+        }
+        // TODO: implement invalid case, both by right char but wrong
+        // annotations (if possible) and wrong char.
+        StringBuilder b = new StringBuilder();
+        for (int i = 0; i < count; i++) {
+          int c = a.nextChar(i);
+          assert c != -1;
+          b.append((char) c);
+          if (valid && !a.checkDeleteCharacters(b.toString(), null).isValid()) 
{
+            b.deleteCharAt(b.length() - 1);
+            break;
+          }
+        }
+        if (b.length() == 0) {
+          // TODO: simplify this method
+          return null;
+        }
+        final String s = b.toString();
+        RandomizerOperationComponent c = new RandomizerOperationComponent() {
+          @Override
+          public ValidationResult check(DocOpAutomaton a, ViolationCollector 
v) {
+            return a.checkDeleteCharacters(s, v);
+          }
+
+          @Override
+          public void apply(DocOpAutomaton a) {
+            a.doDeleteCharacters(s);
+          }
+
+          @Override
+          public void output(DocOpCursor c) {
+            c.deleteCharacters(s);
+          }
+
+          @Override
+          public String toString() {
+            return "DeleteCharacters(" + s + ")";
+          }
+        };
+        if (c.check(a, null).isValid() != valid) {
+          return null;
+        } else {
+          return c;
+        }
+      }
+    }
+
+    interface AttributesUpdateChecker {
+      ValidationResult check(AttributesUpdate u);
+    }
+
+    // returns null on failure
+    AttributesUpdate generateRandomAttributesUpdate(final boolean valid,
+        final Attributes oldAttributes,
+        final AttributesUpdateChecker checker) {
+      AttributesUpdate accu = new AttributesUpdateImpl();
+      if (valid && !checker.check(accu).isValid()
+          || !valid && checker.check(accu).isIllFormed()) {
+        return null;
+      }
+      if (!valid) {
+        // If we want an invalid component, and it's not already invalid 
without
+        // any attributes, make it invalid by adding an invalid attribute 
first.
+        if (checker.check(accu).isValid()) {
+          assert accu.changeSize() == 0;
+          accu = pickRandomNonNullMappedElement(r,
+              p.getAttributeNames(), new Mapper<String, AttributesUpdate>() {
+            @Override
+            public AttributesUpdate map(final String name) {
+              return pickRandomNonNullMappedElement(r, p.getAttributeValues(),
+                  new Mapper<String, AttributesUpdate> () {
+                @Override
+                public AttributesUpdate map(String value) {
+                  AttributesUpdate b = new AttributesUpdateImpl(name,
+                      oldAttributes.get(name), value);
+                  switch (checker.check(b)) {
+                    case ILL_FORMED:
+                      return null;
+                    case INVALID_DOCUMENT:
+                    case INVALID_SCHEMA:
+                      return b;
+                    case VALID:
+                      return null;
+                    default:
+                      throw new RuntimeException("Unexpected validation 
result");
+                  }
+                }
+              });
+            }
+          });
+          if (accu == null) {
+            return null;
+          }
+        }
+        assert !checker.check(accu).isValid();
+        // Flip a coin and terminate if the number of attributes was really
+        // supposed to be zero.
+        if (r.nextBoolean()) {
+          return accu;
+        }
+      }
+      while (r.nextBoolean()) {
+        final AttributesUpdate finalAccu = accu;
+        AttributesUpdate newAccu = pickRandomNonNullMappedElement(r,
+            p.getAttributeNames(), new Mapper<String, AttributesUpdate>() {
+          @Override
+          public AttributesUpdate map(final String name) {
+            for (int i = 0; i < finalAccu.changeSize(); i++) {
+              if (finalAccu.getChangeKey(i).equals(name)) {
+                return null;
+              }
+            }
+            return pickRandomNonNullMappedElement(r, p.getAttributeValues(),
+                new Mapper<String, AttributesUpdate>() {
+              @Override
+              public AttributesUpdate map(String value) {
+                AttributesUpdate b = finalAccu.composeWith(new 
AttributesUpdateImpl(name,
+                    oldAttributes.get(name), value));
+                assert b != finalAccu; // assert non-destructiveness
+                ValidationResult v = checker.check(b);
+                if (valid && !v.isValid() || !valid && v.isIllFormed()) {
+                  return null;
+                } else {
+                  return b;
+                }
+              }
+            });
+          }
+        });
+        if (newAccu == null) {
+          return accu;
+        }
+        accu = newAccu;
+      }
+      return accu;
+    }
+
+    class ElementStartGenerator extends RandomOperationComponentGenerator {
+      @Override
+      RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, 
Stage stage) {
+        switch (stage) {
+          case S1_UNRESTRICTED:
+            return generate(a, valid);
+          case S2_CLOSE_STRUCTURE:
+          case S3_CLOSE_ANNOTATIONS:
+          case S4_SKIP_TO_END:
+            return null;
+          default:
+            throw new RuntimeException("Unexpected stage: " + stage);
+        }
+      }
+
+      RandomizerOperationComponent generateGivenTag(final DocOpAutomaton a, 
final boolean valid,
+          final String tag) {
+        {
+          ValidationResult v = a.checkElementStart(tag, Attributes.EMPTY_MAP, 
null);
+          if (valid && !v.isValid() || !valid && v.isIllFormed()) {
+            // Early exit if we can't build an element start with this tag.
+            return null;
+          }
+        }
+
+        AttributesUpdate u = generateRandomAttributesUpdate(valid, 
Attributes.EMPTY_MAP,
+            new AttributesUpdateChecker() {
+              @Override
+              public ValidationResult check(AttributesUpdate u) {
+                Attributes attrs = Attributes.EMPTY_MAP.updateWith(u);
+                return a.checkElementStart(tag, attrs, null);
+              }
+            });
+        if (u == null) {
+          return null;
+        } else {
+          final Attributes attributes = Attributes.EMPTY_MAP.updateWith(u);
+          return new RandomizerOperationComponent() {
+            @Override
+            public ValidationResult check(DocOpAutomaton a, ViolationCollector 
v) {
+              return a.checkElementStart(tag, attributes, v);
+            }
+
+            @Override
+            public void apply(DocOpAutomaton a) {
+              a.doElementStart(tag, attributes);
+            }
+
+            @Override
+            public void output(DocOpCursor c) {
+              c.elementStart(tag, attributes);
+            }
+
+            @Override
+            public String toString() {
+              return "ElementStart(" + tag + ", " + attributes + ")";
+            }
+          };
+        }
+      }
+
+      RandomizerOperationComponent generate(final DocOpAutomaton a, final 
boolean valid) {
+        return pickRandomNonNullMappedElement(r, p.getElementTypes(),
+            new Mapper<String, RandomizerOperationComponent>() {
+              @Override
+              public RandomizerOperationComponent map(final String tag) {
+                return generateGivenTag(a, valid, tag);
+              }
+            });
+      }
+    }
+
+    abstract class RandomConstantOperationComponentGenerator
+        extends RandomOperationComponentGenerator {
+      abstract ValidationResult check(DocOpAutomaton a, ViolationCollector v);
+      abstract void apply(DocOpAutomaton a);
+      abstract void output(DocOpCursor c);
+
+      RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid) {
+        switch (check(a, null)) {
+          case ILL_FORMED:
+            return null;
+          case VALID:
+            if (!valid) {
+              return null;
+            }
+            break;
+          case INVALID_DOCUMENT:
+            if (valid) {
+              return null;
+            }
+            break;
+          case INVALID_SCHEMA:
+            if (valid) {
+              return null;
+            }
+            break;
+          default:
+            throw new RuntimeException("Unexpected validation result");
+        }
+        return new RandomizerOperationComponent() {
+          @Override
+          public ValidationResult check(DocOpAutomaton a, ViolationCollector 
v) {
+            return RandomConstantOperationComponentGenerator.this.check(a, v);
+          }
+
+          @Override
+          public void apply(DocOpAutomaton a) {
+            RandomConstantOperationComponentGenerator.this.apply(a);
+          }
+
+          @Override
+          public void output(DocOpCursor c) {
+            RandomConstantOperationComponentGenerator.this.output(c);
+          }
+
+          @Override
+          public String toString() {
+            return "Constant component from "
+                + 
RandomConstantOperationComponentGenerator.this.getClass().getName();
+          }
+        };
+      }
+    }
+
+    class ElementEndGenerator extends 
RandomConstantOperationComponentGenerator {
+      @Override
+      RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, 
Stage stage) {
+        switch (stage) {
+          case S1_UNRESTRICTED:
+            return generate(a, valid);
+          case S2_CLOSE_STRUCTURE:
+            if (a.insertionStackComplexityMeasure() == 0) {
+              return null;
+            }
+            return generate(a, valid);
+          case S3_CLOSE_ANNOTATIONS:
+          case S4_SKIP_TO_END:
+            return null;
+          default:
+            throw new RuntimeException("Unexpected stage: " + stage);
+        }
+      }
+
+      @Override
+      ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
+        return a.checkElementEnd(v);
+      }
+
+      @Override
+      void apply(DocOpAutomaton a) {
+        a.doElementEnd();
+      }
+
+      @Override
+      void output(DocOpCursor c) {
+        c.elementEnd();
+      }
+    }
+
+    class DeleteElementStartGenerator extends 
RandomOperationComponentGenerator {
+      @Override
+      RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, 
Stage stage) {
+        switch (stage) {
+          case S1_UNRESTRICTED:
+            return generate(a, valid);
+          case S2_CLOSE_STRUCTURE:
+            if (a.deletionStackComplexityMeasure() == 0) {
+              return null;
+            }
+            return generate(a, valid);
+          case S3_CLOSE_ANNOTATIONS:
+          case S4_SKIP_TO_END:
+            return null;
+          default:
+            throw new RuntimeException("Unexpected stage: " + stage);
+        }
+      }
+
+      RandomizerOperationComponent generate(final DocOpAutomaton a, final 
boolean valid) {
+        final String tag = a.currentElementStartTag();
+        final Attributes oldAttrs = a.currentElementStartAttributes();
+        if (tag == null) {
+          assert oldAttrs == null;
+          return null;
+        }
+        assert oldAttrs != null;
+        switch (a.checkDeleteElementStart(tag, oldAttrs, null)) {
+          case ILL_FORMED:
+          case INVALID_DOCUMENT: // TODO: bring back generating invalid ops
+          case INVALID_SCHEMA:
+            return null;
+          case VALID:
+            return new RandomizerOperationComponent() {
+              @Override
+              public ValidationResult check(DocOpAutomaton a, 
ViolationCollector v) {
+                return a.checkDeleteElementStart(tag, oldAttrs, v);
+              }
+
+              @Override
+              public void apply(DocOpAutomaton a) {
+                a.doDeleteElementStart(tag, oldAttrs);
+              }
+
+              @Override
+              public void output(DocOpCursor c) {
+                c.deleteElementStart(tag, oldAttrs);
+              }
+            };
+          default:
+            throw new RuntimeException("Unexpected validation result");
+        }
+      }
+    }
+
+    class DeleteElementEndGenerator extends 
RandomConstantOperationComponentGenerator {
+      @Override
+      void apply(DocOpAutomaton a) {
+        a.doDeleteElementEnd();
+      }
+
+      @Override
+      ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
+        return a.checkDeleteElementEnd(v);
+      }
+
+      @Override
+      void output(DocOpCursor c) {
+        c.deleteElementEnd();
+      }
+
+      @Override
+      RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, 
Stage stage) {
+        switch (stage) {
+          case S1_UNRESTRICTED:
+            return generate(a, valid);
+          case S2_CLOSE_STRUCTURE:
+            if (a.deletionStackComplexityMeasure() == 0) {
+              return null;
+            }
+            return generate(a, valid);
+          case S3_CLOSE_ANNOTATIONS:
+          case S4_SKIP_TO_END:
+            return null;
+          default:
+            throw new RuntimeException("Unexpected stage: " + stage);
+        }
+      }
+    }
+
+
+    class ReplaceAttributesGenerator extends RandomOperationComponentGenerator 
{
+      @Override
+      RandomizerOperationComponent generate(final DocOpAutomaton a, boolean 
valid, Stage stage) {
+        if (stage != Stage.S1_UNRESTRICTED) {
+          return null;
+        }
+        final Attributes oldAttrs = a.currentElementStartAttributes();
+        if (oldAttrs == null) {
+          if (valid) {
+            return null;
+          }
+        }
+        if (!valid) {
+          // TODO: bring this back.
+          // several cases: invalid because of wrong old attributes, or invalid
+          // because of schema violation of new attributes, or because no
+          // element start here
+          throw new RuntimeException("Not implemented");
+        }
+        AttributesUpdate u = generateRandomAttributesUpdate(valid,
+            oldAttrs, new AttributesUpdateChecker() {
+          @Override
+          public ValidationResult check(AttributesUpdate u) {
+            return a.checkReplaceAttributes(oldAttrs, oldAttrs.updateWith(u), 
null);
+          }
+        });
+
+        if (u == null) {
+          return null;
+        }
+
+        final Attributes newAttrs = oldAttrs.updateWith(u);
+        return new RandomizerOperationComponent() {
+          @Override
+          public void apply(DocOpAutomaton a) {
+            a.doReplaceAttributes(oldAttrs, newAttrs);
+          }
+
+          @Override
+          public ValidationResult check(DocOpAutomaton a, ViolationCollector 
v) {
+            return a.checkReplaceAttributes(oldAttrs, newAttrs, v);
+          }
+
+          @Override
+          public void output(DocOpCursor c) {
+            c.replaceAttributes(oldAttrs, newAttrs);
+          }
+
+          @Override
+          public String toString() {
+            return "ReplaceAttributes(" + oldAttrs + ", " + newAttrs + ")";
+          }
+        };
+      }
+    }
+
+    class UpdateAttributesGenerator extends RandomOperationComponentGenerator {
+      @Override
+      RandomizerOperationComponent generate(final DocOpAutomaton a, boolean 
valid, Stage stage) {
+        if (stage != Stage.S1_UNRESTRICTED) {
+          return null;
+        }
+        final Attributes oldAttrs = a.currentElementStartAttributes();
+        if (oldAttrs == null) {
+          if (valid) {
+            return null;
+          }
+        }
+        if (!valid) {
+          // TODO: bring this back.
+          // several cases: invalid because of wrong old attributes, or invalid
+          // because of schema violation of new attributes, or because no
+          // element start here
+          throw new RuntimeException("Not implemented");
+        }
+        final AttributesUpdate update = generateRandomAttributesUpdate(valid,
+            oldAttrs, new AttributesUpdateChecker() {
+          @Override
+          public ValidationResult check(AttributesUpdate u) {
+            return a.checkUpdateAttributes(u, null);
+          }
+        });
+
+        if (update == null) {
+          return null;
+        }
+
+        return new RandomizerOperationComponent() {
+          @Override
+          public void apply(DocOpAutomaton a) {
+            a.doUpdateAttributes(update);
+          }
+
+          @Override
+          public ValidationResult check(DocOpAutomaton a, ViolationCollector 
v) {
+            return a.checkUpdateAttributes(update, v);
+          }
+
+          @Override
+          public void output(DocOpCursor c) {
+            c.updateAttributes(update);
+          }
+
+          @Override
+          public String toString() {
+            return "UpdateAttributes(" + update + ")";
+          }
+        };
+      }
+    }
+
+    interface RunnableWithException<E extends Throwable> {
+      void run() throws E;
+    }
+
+    class AnnotationBoundaryGenerator extends 
RandomOperationComponentGenerator {
+      @Override
+      RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, 
Stage stage) {
+        switch (stage) {
+          case S1_UNRESTRICTED:
+          case S2_CLOSE_STRUCTURE:
+            return generateWithLookahead(a, valid, stage);
+          case S3_CLOSE_ANNOTATIONS:
+            assert valid;
+            return generateClosing(a);
+          case S4_SKIP_TO_END:
+            return null;
+          default:
+            throw new RuntimeException("Unexpected stage: " + stage);
+        }
+      }
+
+      RandomizerOperationComponent generate(final AnnotationBoundaryMapImpl 
map) {
+        return new RandomizerOperationComponent() {
+          @Override
+          void apply(DocOpAutomaton a) {
+            a.doAnnotationBoundary(map);
+          }
+
+          @Override
+          ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
+            return a.checkAnnotationBoundary(map, v);
+          }
+
+          @Override
+          void output(DocOpCursor c) {
+            c.annotationBoundary(map);
+          }
+
+          @Override
+          boolean isAnnotationBoundary() { return true; }
+
+          @Override
+          public String toString() {
+            return "AnnotationBoundary(" + map + ")";
+          }
+        };
+      }
+
+      String[] toArray(ArrayList<String> a) {
+        return a.toArray(new String[0]);
+      }
+
+      RandomizerOperationComponent generateClosing(DocOpAutomaton a) {
+        if (a.openAnnotations().isEmpty()) {
+          return null;
+        }
+        ArrayList<String> l = new ArrayList<String>(a.openAnnotations());
+        Collections.sort(l);
+        AnnotationBoundaryMapImpl map =
+          AnnotationBoundaryMapImpl.builder().initializationEnd(
+              toArray(l)).build();
+        assert !a.checkAnnotationBoundary(map, null).isIllFormed();
+        return generate(map);
+      }
+
+      class Result extends Exception {
+        final RandomizerOperationComponent component;
+        Result(RandomizerOperationComponent component) {
+          this.component = component;
+        }
+      }
+
+      class StringNullComparator implements Comparator<String> {
+        @Override
+        public int compare(String a, String b) {
+          if (a == b) {
+            return 0;
+          }
+          if (a == null) {
+            return -1;
+          }
+          if (b == null) {
+            return 1;
+          }
+          return a.compareTo(b);
+        }
+      }
+
+      RandomizerOperationComponent generateWithLookahead(final DocOpAutomaton 
a, boolean valid,
+          final Stage stage) {
+        {
+          ValidationResult r = a.checkAnnotationBoundary(
+              AnnotationBoundaryMapImpl.builder().updateValues("a", null, 
"1").build(), null);
+          assert r.isIllFormed() || r.isValid();
+          if (r.isIllFormed()) {
+            return null;
+          }
+        }
+        Set<String> keySet = new TreeSet<String>(new StringNullComparator());
+        for (AnnotationOption o : p.getAnnotationOptions()) {
+          keySet.add(o.key);
+        }
+        keySet.addAll(a.currentAnnotations().keySet());
+        keySet.addAll(a.inheritedAnnotations().keySet());
+        final ArrayList<String> keys = new ArrayList<String>(keySet);
+
+        Collections.sort(keys);
+
+        // For every key, either pick it, or don't (choice point, recursively
+        // explore both options).
+
+        // For each key, one option is to end that key if it currently is in
+        // openAnnotations().
+        // Another option is not to end that key: In that case, given the key,
+        // the valid old values are those from annotationOptions and
+        // those from currentAnnotations() (for deletions) and
+        // those from inheritedAnnotations() (for insertions);
+        // the valid new values are those from annotationOptions and
+        // those from inheritedAnnotations() (for deletion).
+        //
+        // Given the full map, we need to check if the component is valid, then
+        // temporarily apply it to find out if there is any valid component
+        // to follow up with.
+
+        final RunnableWithException<Result> chooseKeys = new 
RunnableWithException<Result>() {
+
+          ArrayList<String> keysToEnd = new ArrayList<String>();
+          ArrayList<String> changeKeys = new ArrayList<String>();
+          ArrayList<String> changeOldValues = new ArrayList<String>();
+          ArrayList<String> changeNewValues = new ArrayList<String>();
+
+          void tryThisOption() throws Result {
+            AnnotationBoundaryMapImpl map = AnnotationBoundaryMapImpl.builder()
+                .initializationEnd(toArray(keysToEnd))
+                .updateValues(toArray(changeKeys), toArray(changeOldValues),
+                    toArray(changeNewValues)).build();
+            final RandomizerOperationComponent component = generate(map);
+            DocOpAutomaton temp = new DocOpAutomaton(a);
+            ViolationCollector v = new ViolationCollector();
+            component.check(temp, v);
+            assert !component.check(temp, null).isIllFormed();
+            component.apply(temp);
+//            System.err.println("begin lookahead for " + map);
+            RandomizerOperationComponent followup = pickComponent(temp, stage);
+            if (followup != null) {
+//              System.err.println("end lookahead, success");
+              throw new Result(component);
+            }
+//            System.err.println("end lookahead, failed");
+          }
+
+          void removeLastMaybe(ArrayList<String> l, int lastItemIndex) {
+            assert lastItemIndex == l.size() || lastItemIndex == l.size() - 1;
+            if (lastItemIndex == l.size() - 1) {
+              l.remove(lastItemIndex);
+            }
+          }
+
+          void take(int nextKeyIndex, String key) throws Result {
+            assert key != null;
+            if (a.openAnnotations().contains(key)) {
+              int oldSize = keysToEnd.size();
+              try {
+                keysToEnd.add(key);
+                nextKey(nextKeyIndex);
+              } finally {
+                removeLastMaybe(keysToEnd, oldSize);
+              }
+            }
+
+            Set<String> valueSet = new TreeSet<String>(new 
StringNullComparator());
+            for (AnnotationOption o : p.getAnnotationOptions()) {
+              if (key.equals(o.key)) {
+                valueSet.addAll(o.valueAlternatives);
+              }
+            }
+            AnnotationMap inheritedAnnotations = a.inheritedAnnotations();
+            if (inheritedAnnotations.containsKey(key)) {
+              valueSet.add(inheritedAnnotations.get(key));
+            } else {
+              valueSet.add(null);
+            }
+            ArrayList<String> newValues = new ArrayList<String>(valueSet);
+            AnnotationMap currentAnnotations = a.currentAnnotations();
+            if (currentAnnotations.containsKey(key)) {
+              valueSet.add(currentAnnotations.get(key));
+            } else {
+              valueSet.add(null);
+            }
+            ArrayList<String> oldValues = new ArrayList<String>(valueSet);
+
+            shuffle(r, oldValues);
+            shuffle(r, newValues);
+
+            for (String oldValue : oldValues) {
+              for (String newValue : newValues) {
+                assert changeKeys.size() == changeOldValues.size();
+                assert changeKeys.size() == changeNewValues.size();
+                int oldSize = changeKeys.size();
+                try {
+                  changeKeys.add(key);
+                  changeOldValues.add(oldValue);
+                  changeNewValues.add(newValue);
+                  nextKey(nextKeyIndex);
+                } finally {
+                  removeLastMaybe(changeNewValues, oldSize);
+                  removeLastMaybe(changeOldValues, oldSize);
+                  removeLastMaybe(changeKeys, oldSize);
+                  assert changeKeys.size() == changeOldValues.size();
+                  assert changeKeys.size() == changeNewValues.size();
+                }
+              }
+            }
+          }
+
+          void nextKey(int nextKeyIndex) throws Result {
+            if (nextKeyIndex >= keys.size()) {
+              tryThisOption();
+              return;
+            }
+            String key = keys.get(nextKeyIndex);
+            boolean take = r.nextBoolean();
+            if (take) {
+              take(nextKeyIndex + 1, key);
+              nextKey(nextKeyIndex + 1);
+            } else {
+              nextKey(nextKeyIndex + 1);
+              take(nextKeyIndex + 1, key);
+            }
+          }
+
+          @Override
+          public void run() throws Result {
+            nextKey(0);
+          }
+        };
+
+        try {
+          chooseKeys.run();
+        } catch (Result e) {
+          return e.component;
+        }
+        return null;
+      }
+    }
+
+    private static boolean equal(Object a, Object b) {
+      return a == null ? b == null : a.equals(b);
+    }
+
+    final RandomProvider r;
+    final Parameters p;
+    final AutomatonDocument doc;
+
+    Generator(RandomProvider r, Parameters p, AutomatonDocument doc) {
+      this.r = r;
+      this.p = p;
+      this.doc = doc;
+    }
+
+    final List<RandomOperationComponentGenerator> componentGenerators =
+      Arrays.asList(
+          new AnnotationBoundaryGenerator(),
+          new CharactersGenerator(),
+          new ElementStartGenerator(),
+          new ElementEndGenerator(),
+          new SkipGenerator(),
+          new DeleteCharactersGenerator(),
+          new DeleteElementStartGenerator(),
+          new DeleteElementEndGenerator(),
+          new ReplaceAttributesGenerator(),
+          new UpdateAttributesGenerator()
+          );
+
+    DocOp generate() {
+      DocOpAutomaton a = new DocOpAutomaton(doc, 
DocumentSchema.NO_SCHEMA_CONSTRAINTS);
+      DocOpBuffer b = new DocOpBuffer();
+      generate1(a, b);
+      return b.finish();
+    }
+
+    RandomizerOperationComponent pickComponent(final DocOpAutomaton a, final 
Stage stage) {
+//      System.err.println("stage: " + stage);
+      RandomizerOperationComponent component = 
pickRandomNonNullMappedElement(r,
+          componentGenerators,
+          new Mapper<RandomOperationComponentGenerator, 
RandomizerOperationComponent>() {
+        @Override
+        public RandomizerOperationComponent 
map(RandomOperationComponentGenerator g) {
+//          System.err.println("trying generator " + g);
+          RandomizerOperationComponent c = g.generate(a, true, stage);
+          if (c != null) {
+            assert c.check(a, null).isValid();
+          }
+          return c;
+        }
+      });
+//      System.err.println("picked " + component);
+      return component;
+    }
+
+    RandomizerOperationComponent generate2(DocOpAutomaton a, DocOpCursor 
output, Stage stage) {
+      RandomizerOperationComponent component = pickComponent(a, stage);
+      assert component != null;
+      component.apply(a);
+      component.output(output);
+      return component;
+    }
+
+    void generate1(DocOpAutomaton a, DocOpCursor output) {
+      if (!p.getValidity()) {
+        throw new RuntimeException("generation of invalid operations not 
supported yet");
+      }
+      int desiredNumComponents = randomIntFromRange(r, 0, 
p.getMaxOpeningComponents());
+      int numComponentsPicked = 0;
+      while (numComponentsPicked < desiredNumComponents) {
+        RandomizerOperationComponent component = generate2(a, output, 
Stage.S1_UNRESTRICTED);
+        if (!component.isAnnotationBoundary()) {
+          numComponentsPicked++;
+        }
+      }
+
+      while (a.deletionStackComplexityMeasure() > 0) {
+        generate2(a, output, Stage.S2_CLOSE_STRUCTURE);
+      }
+
+      while (a.insertionStackComplexityMeasure() > 0) {
+        int before = a.insertionStackComplexityMeasure();
+        generate2(a, output, Stage.S2_CLOSE_STRUCTURE);
+        assert a.insertionStackComplexityMeasure() <= before;
+      }
+
+      if (!a.openAnnotations().isEmpty()) {
+        generate2(a, output, Stage.S3_CLOSE_ANNOTATIONS);
+        assert a.openAnnotations().isEmpty();
+      }
+
+      if (a.maxRetainItemCount() > 0) {
+        generate2(a, output, Stage.S4_SKIP_TO_END);
+        assert a.maxRetainItemCount() == 0;
+      }
+    }
+  }
+
+  /**
+   * Returns a randomly-generated document operation based on the given 
document,
+   * parameters, and schema.
+   */
+  public static DocOp generate(RandomProvider r, Parameters p, 
AutomatonDocument doc) {
+    DocOp op = new Generator(r, p, doc).generate();
+    ViolationCollector v = new ViolationCollector();
+    DocOpValidator.validate(v, null, doc, op);
+    assert !v.isIllFormed();
+    assert p.getValidity() == v.isValid();
+    return op;
+  }
+
+
+  /**
+   * Stand-alone main() for quick experimentation.
+   */
+  public static void main(String[] args) throws OperationException {
+    BootstrapDocument initialDoc = new BootstrapDocument();
+    initialDoc.consume(new DocInitializationBuilder()
+        .elementStart("blip", Attributes.EMPTY_MAP)
+        .elementStart("p", Attributes.EMPTY_MAP)
+        .characters("abc")
+        .elementEnd()
+        .elementEnd().build());
+
+    Parameters p = new Parameters();
+
+    p.setMaxOpeningComponents(10);
+
+    RandomProvider r = RandomProviderImpl.ofSeed(2538);
+    for (int i = 0; i < 200; i++) {
+      BootstrapDocument doc = new BootstrapDocument();
+      doc.consume(initialDoc.asOperation());
+      for (int j = 0; j < 20; j++) {
+        System.err.println("i=" + i + ", j=" + j);
+        System.err.println("old: " + DocOpUtil.toXmlString(doc.asOperation()));
+        System.err.println("old: " + 
DocOpUtil.toConciseString(doc.asOperation()));
+        DocOp op = generate(r, p, doc);
+        System.err.println("op:  " + DocOpUtil.toConciseString(op));
+        doc.consume(op);
+        System.err.println("new: " + 
DocOpUtil.toConciseString(doc.asOperation()));
+        System.err.println("new: " + DocOpUtil.toXmlString(doc.asOperation()));
+        if (!DocOpValidator.validate(null, 
DocumentSchema.NO_SCHEMA_CONSTRAINTS,
+            doc.asOperation()).isValid()) {
+          throw new RuntimeException("doc not valid");
+        }
+      }
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomNindoGenerator.java
----------------------------------------------------------------------
diff --git 
a/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomNindoGenerator.java
 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomNindoGenerator.java
new file mode 100644
index 0000000..65d97b7
--- /dev/null
+++ 
b/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomNindoGenerator.java
@@ -0,0 +1,776 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.testing;
+
+import org.waveprotocol.wave.model.document.indexed.IndexedDocument;
+import org.waveprotocol.wave.model.document.operation.Attributes;
+import org.waveprotocol.wave.model.document.operation.Nindo;
+import org.waveprotocol.wave.model.document.operation.Nindo.NindoCursor;
+import org.waveprotocol.wave.model.document.operation.NindoAutomaton;
+import org.waveprotocol.wave.model.document.operation.NindoValidator;
+import 
org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ValidationResult;
+import 
org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ViolationCollector;
+import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
+import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl;
+import 
org.waveprotocol.wave.model.document.operation.impl.AttributesUpdateImpl;
+import org.waveprotocol.wave.model.document.raw.impl.Element;
+import org.waveprotocol.wave.model.document.raw.impl.Node;
+import org.waveprotocol.wave.model.document.raw.impl.Text;
+import org.waveprotocol.wave.model.testing.RandomDocOpGenerator.Parameters;
+import org.waveprotocol.wave.model.testing.RandomDocOpGenerator.RandomProvider;
+import org.waveprotocol.wave.model.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Generates random document operations based on a document.  They can be
+ * valid or invalid, depending on parameters.
+ *
+ * @author oh...@google.com (Christian Ohler)
+ */
+@SuppressWarnings("unchecked") // TODO(ohler, danilatos): declare generics 
properly
+public final class RandomNindoGenerator {
+
+  private RandomNindoGenerator() {}
+
+  private static <T> T randomElement(RandomProvider r, List<T> l) {
+    return l.get(r.nextInt(l.size()));
+  }
+
+  private static <T> T randomElement(RandomProvider r, Set<T> s) {
+    int n = randomIntFromRange(r, 0, s.size());
+    for (T e : s) {
+      if (n == 0) {
+        return e;
+      }
+      n--;
+    }
+    assert false;
+    throw new RuntimeException("fell off end of loop");
+  }
+
+  private static int randomIntFromRange(RandomProvider r, int min, int limit) {
+    assert 0 <= min; // not really a precondition, but true in our case
+    assert min < limit;
+
+    int x = r.nextInt(limit - min) + min;
+    assert min <= x;
+    assert x < limit;
+    return x;
+  }
+
+  private interface Mapper<I, O> {
+    O map(I in);
+  }
+
+  private static <I, O> O pickRandomNonNullMappedElement(RandomProvider r, 
List<I> in,
+      Mapper<I, O> mapper) {
+    List<I> list = new ArrayList<I>(in);
+    while (!list.isEmpty()) {
+      int index = randomIntFromRange(r, 0, list.size());
+      O value = mapper.map(list.get(index));
+      if (value != null) {
+        return value;
+      }
+      // Remove element efficiently by swapping in an element from the end.
+      list.set(index, list.get(list.size() - 1));
+      list.remove(list.size() - 1);
+    }
+    return null;
+  }
+
+
+  private static class Generator {
+
+    interface RandomizerMutationComponent {
+      ValidationResult check(ViolationCollector v);
+      void apply();
+    }
+
+    abstract class RandomMutationComponentGenerator {
+      abstract RandomizerMutationComponent generate(boolean valid);
+      // 0 means this transition will never be needed to complete an operation
+      // (e.g., skip or setAttributes)
+      // -1 means this transition may be needed to complete an operation but
+      // increases the size of the structural stack (e.g. deleteElementStart)
+      // -2 means this transition may be needed to complete an operation but
+      // does not change the size of the structural stack (e.g. 
deleteCharacters)
+      // -3 means this transition may be needed to complete an operation and
+      // decreases the size of the structural stack (e.g. deleteElementEnd)
+      abstract int potential();
+    }
+
+    class SkipGenerator extends RandomMutationComponentGenerator {
+      @Override
+      public int potential() {
+        return 0;
+      }
+
+      @Override
+      RandomizerMutationComponent generate(boolean valid) {
+        int maxDistance = a.maxSkipDistance();
+        if (maxDistance == 0) {
+          return null;
+        }
+        if (a.checkSkip(1, null).isIllFormed()) {
+          return null;
+        }
+        final int distance;
+        if (valid) {
+          distance = randomIntFromRange(r, 1, maxDistance + 1);
+          assert a.checkSkip(distance, null).isValid();
+        } else {
+          distance = randomIntFromRange(r, maxDistance + 1, maxDistance + 
p.getMaxSkipAfterEnd());
+          assert a.checkSkip(distance, null) == 
ValidationResult.INVALID_DOCUMENT;
+        }
+        return new RandomizerMutationComponent() {
+          @Override
+          public ValidationResult check(ViolationCollector v) {
+            return a.checkSkip(distance, v);
+          }
+
+          @Override
+          public void apply() {
+            a.doSkip(distance);
+            targetDoc.skip(distance);
+          }
+        };
+      }
+    }
+
+    class CharactersGenerator extends RandomMutationComponentGenerator {
+      @Override
+      public int potential() {
+        return 0;
+      }
+
+      @Override
+      RandomizerMutationComponent generate(boolean valid) {
+        ValidationResult v = a.checkCharacters("a", null);
+        if (v.isIllFormed()) {
+          return null;
+        }
+        int count;
+        if (valid) {
+          if (!v.isValid()) {
+            return null;
+          }
+          int max = Math.min(a.maxLengthIncrease(), p.getMaxInsertLength());
+          if (max == 0) {
+            return null;
+          }
+          count = randomIntFromRange(r, 1, max + 1);
+        } else {
+          if (v.isValid()) {
+            // Exceed length of document (if p.maxInsertLength allows it).
+            int max = p.getMaxInsertLength();
+            int min = a.maxLengthIncrease() + 1;
+            if (min > max) {
+              return null;
+            }
+            count = randomIntFromRange(r, min, max + 1);
+          } else {
+            count = randomIntFromRange(r, 1, p.getMaxInsertLength());
+          }
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < count; i++) {
+          if (i <= 26) {
+            sb.append((char) ('a' + i));
+          } else {
+            sb.append('.');
+          }
+        }
+        final String s = sb.toString();
+        return new RandomizerMutationComponent() {
+          @Override
+          public ValidationResult check(ViolationCollector v) {
+            return a.checkCharacters(s, v);
+          }
+
+          @Override
+          public void apply() {
+            a.doCharacters(s);
+            targetDoc.characters(s);
+          }
+        };
+      }
+    }
+
+    class DeleteCharactersGenerator extends RandomMutationComponentGenerator {
+      @Override
+      RandomizerMutationComponent generate(boolean valid) {
+        if (a.checkDeleteCharacters(1, null).isIllFormed()) {
+          return null;
+        }
+        final int count;
+        if (valid) {
+          int max = Math.min(a.maxCharactersToDelete(), 
p.getMaxDeleteLength());
+          if (max == 0) {
+            return null;
+          }
+          count = randomIntFromRange(r, 1, max + 1);
+        } else {
+          int max = p.getMaxDeleteLength();
+          int min = a.maxCharactersToDelete() + 1;
+          if (min > max) {
+            return null;
+          }
+          count = randomIntFromRange(r, min, max + 1);
+        }
+        return new RandomizerMutationComponent() {
+          @Override
+          public ValidationResult check(ViolationCollector v) {
+            return a.checkDeleteCharacters(count, v);
+          }
+
+          @Override
+          public void apply() {
+            a.doDeleteCharacters(count);
+            targetDoc.deleteCharacters(count);
+          }
+        };
+      }
+
+      @Override
+      public int potential() {
+        return -2;
+      }
+    }
+
+    interface AttributesChecker {
+      ValidationResult check(Attributes attrs);
+    }
+
+    Attributes generateRandomAttributes(final boolean valid, final 
AttributesChecker checker) {
+      Attributes attrAccu = Attributes.EMPTY_MAP;
+      if (valid && !checker.check(Attributes.EMPTY_MAP).isValid()
+          || !valid && checker.check(Attributes.EMPTY_MAP).isIllFormed()) {
+        return null;
+      }
+      if (!valid) {
+        // If we want an invalid component, and it's not already invalid 
without
+        // any attributes, make it invalid by adding an invalid attribute 
first.
+        if (checker.check(attrAccu).isValid()) {
+          assert attrAccu.isEmpty();
+          attrAccu = pickRandomNonNullMappedElement(r,
+              p.getAttributeNames(), new Mapper<String, Attributes>() {
+            @Override
+            public Attributes map(final String name) {
+              return pickRandomNonNullMappedElement(r, p.getAttributeValues(),
+                  new Mapper<String, Attributes> () {
+                @Override
+                public Attributes map(String value) {
+                  Attributes b = new AttributesImpl(name, value);
+                  switch (checker.check(b)) {
+                    case ILL_FORMED:
+                      return null;
+                    case INVALID_DOCUMENT:
+                    case INVALID_SCHEMA:
+                      return b;
+                    case VALID:
+                      return null;
+                    default:
+                      throw new RuntimeException("unexpected validation 
result");
+                  }
+                }
+              });
+            }
+          });
+          if (attrAccu == null) {
+            return null;
+          }
+        }
+        assert !checker.check(attrAccu).isValid();
+        // Flip a coin and terminate if the number of attributes was really
+        // supposed to be zero.
+        if (r.nextBoolean()) {
+          return attrAccu;
+        }
+      }
+      while (r.nextBoolean()) {
+        final Attributes finalAttrAccu = attrAccu;
+        Pair<String, String> newAttr = pickRandomNonNullMappedElement(r,
+            p.getAttributeNames(), new Mapper<String, Pair<String, String>>() {
+          @Override
+          public Pair<String, String> map(final String name) {
+            if (finalAttrAccu.containsKey(name)) {
+              return null;
+            }
+            return pickRandomNonNullMappedElement(r, p.getAttributeValues(),
+                new Mapper<String, Pair<String, String>>() {
+              @Override
+              public Pair<String, String> map(String value) {
+                Attributes b = finalAttrAccu.updateWith(
+                    new AttributesUpdateImpl(name, null, value));
+                assert b != finalAttrAccu; // assert non-destructiveness
+                ValidationResult v = checker.check(b);
+                if (valid && !v.isValid() || !valid && v.isIllFormed()) {
+                  return null;
+                } else {
+                  return Pair.of(name, value);
+                }
+              }
+            });
+          }
+        });
+        if (newAttr == null) {
+          return attrAccu;
+        }
+        attrAccu = attrAccu.updateWith(
+            new AttributesUpdateImpl(newAttr.getFirst(), null, 
newAttr.getSecond()));
+      }
+      return attrAccu;
+    }
+
+    class ElementStartGenerator extends RandomMutationComponentGenerator {
+      @Override
+      public int potential() {
+        return 0;
+      }
+
+      @Override
+      RandomizerMutationComponent generate(final boolean valid) {
+        Pair<String, Attributes> args = pickRandomNonNullMappedElement(r, 
p.getElementTypes(),
+            new Mapper<String, Pair<String, Attributes>>() {
+              @Override
+              public Pair<String, Attributes> map(final String tag) {
+                {
+                  ValidationResult v = a.checkElementStart(tag, 
Attributes.EMPTY_MAP, null);
+                  if (valid && !v.isValid() || !valid && v.isIllFormed()) {
+                    // Early exit if we can't build an element start with this 
tag.
+                    return null;
+                  }
+                }
+
+                Attributes attrs = generateRandomAttributes(valid,
+                    new AttributesChecker() {
+                      @Override
+                      public ValidationResult check(Attributes attrs) {
+                        return a.checkElementStart(tag, attrs, null);
+                      }
+                    });
+                if (attrs == null) {
+                  return null;
+                } else {
+                  return Pair.of(tag, attrs);
+                }
+              }
+            });
+        if (args == null) {
+          return null;
+        }
+        final String tag = args.getFirst();
+        final Attributes attributes = args.getSecond();
+        return new RandomizerMutationComponent() {
+          @Override
+          public ValidationResult check(ViolationCollector v) {
+            return a.checkElementStart(tag, attributes, v);
+          }
+
+          @Override
+          public void apply() {
+            a.doElementStart(tag, attributes);
+            targetDoc.elementStart(tag, attributes);
+          }
+        };
+      }
+    }
+
+    abstract class RandomConstantMutationComponentGenerator
+        extends RandomMutationComponentGenerator {
+      abstract ValidationResult check(ViolationCollector v);
+      abstract void apply();
+
+      @Override
+      RandomizerMutationComponent generate(boolean valid) {
+        switch (check(null)) {
+          case ILL_FORMED:
+            return null;
+          case VALID:
+            if (!valid) {
+              return null;
+            }
+            break;
+          case INVALID_DOCUMENT:
+            if (valid) {
+              return null;
+            }
+            break;
+          case INVALID_SCHEMA:
+            if (valid) {
+              return null;
+            }
+            break;
+          default:
+            throw new RuntimeException("unexpected validation result");
+        }
+        return new RandomizerMutationComponent() {
+          @Override
+          public ValidationResult check(ViolationCollector v) {
+            return RandomConstantMutationComponentGenerator.this.check(v);
+          }
+
+          @Override
+          public void apply() {
+            RandomConstantMutationComponentGenerator.this.apply();
+          }
+
+          @Override
+          public String toString() {
+            return this.getClass().getName() + " from "
+                + 
RandomConstantMutationComponentGenerator.this.getClass().getName();
+          }
+        };
+      }
+    }
+
+    class ElementEndGenerator extends RandomConstantMutationComponentGenerator 
{
+      @Override
+      int potential() {
+        return -3;
+      }
+
+      @Override
+      ValidationResult check(ViolationCollector v) {
+        return a.checkElementEnd(v);
+      }
+
+      @Override
+      void apply() {
+        a.doElementEnd();
+        targetDoc.elementEnd();
+      }
+    }
+
+    class DeleteElementStartGenerator extends 
RandomConstantMutationComponentGenerator {
+      @Override
+      int potential() {
+        return -1;
+      }
+
+      @Override
+      ValidationResult check(ViolationCollector v) {
+        return a.checkDeleteElementStart(v);
+      }
+
+      @Override
+      void apply() {
+        a.doDeleteElementStart();
+        targetDoc.deleteElementStart();
+      }
+    }
+
+    class DeleteElementEndGenerator extends 
RandomConstantMutationComponentGenerator {
+      @Override
+      int potential() {
+        return -3;
+      }
+
+      @Override
+      ValidationResult check(ViolationCollector v) {
+        return a.checkDeleteElementEnd(v);
+      }
+
+      @Override
+      void apply() {
+        a.doDeleteElementEnd();
+        targetDoc.deleteElementEnd();
+      }
+    }
+
+    abstract class AttributesOnlyRandomMutationComponentGenerator
+        extends RandomMutationComponentGenerator {
+      abstract ValidationResult check(Attributes attrs, ViolationCollector v);
+      abstract void apply(Attributes attrs);
+      @Override
+      RandomizerMutationComponent generate(boolean valid) {
+        final Attributes attrs = generateRandomAttributes(valid, new 
AttributesChecker() {
+          @Override
+          public ValidationResult check(Attributes attrs) {
+            return 
AttributesOnlyRandomMutationComponentGenerator.this.check(attrs, null);
+          }
+        });
+
+        if (attrs == null) {
+          return null;
+        }
+
+        return new RandomizerMutationComponent() {
+          @Override
+          public ValidationResult check(ViolationCollector v) {
+            return 
AttributesOnlyRandomMutationComponentGenerator.this.check(attrs, v);
+          }
+
+          @Override
+          public void apply() {
+            AttributesOnlyRandomMutationComponentGenerator.this.apply(attrs);
+          }
+
+          @Override
+          public String toString() {
+            return this.getClass().getName() + " from "
+                + 
AttributesOnlyRandomMutationComponentGenerator.this.getClass().getName()
+                + " " + attrs;
+          }
+        };
+      }
+    }
+
+    class SetAttributesGenerator extends 
AttributesOnlyRandomMutationComponentGenerator {
+      @Override
+      int potential() {
+        return 0;
+      }
+
+      @Override
+      ValidationResult check(Attributes attrs, ViolationCollector v) {
+        return a.checkSetAttributes(attrs, v);
+      }
+
+      @Override
+      void apply(Attributes attrs) {
+        a.doSetAttributes(attrs);
+        targetDoc.replaceAttributes(attrs);
+      }
+    }
+
+    class UpdateAttributesGenerator extends 
AttributesOnlyRandomMutationComponentGenerator {
+      @Override
+      int potential() {
+        return 0;
+      }
+
+      @Override
+      ValidationResult check(Attributes attrs, ViolationCollector v) {
+        return a.checkUpdateAttributes(attrs, v);
+      }
+
+      @Override
+      void apply(Attributes attrs) {
+        a.doUpdateAttributes(attrs);
+        targetDoc.updateAttributes(attrs);
+      }
+    }
+
+    class StartAnnotationGenerator extends RandomMutationComponentGenerator {
+      @Override
+      int potential() {
+        return 0;
+      }
+
+      @Override
+      RandomizerMutationComponent generate(boolean valid) {
+        if (!valid) {
+          return null;
+        }
+        if (p.getAnnotationOptions().isEmpty()) {
+          return null;
+        }
+
+        Parameters.AnnotationOption option = randomElement(r, 
p.getAnnotationOptions());
+        final String key = option.getKey();
+        final String value = option.randomValue(r);
+        return new RandomizerMutationComponent() {
+          @Override
+          public ValidationResult check(ViolationCollector v) {
+            return a.checkStartAnnotation(key, value, v);
+          }
+
+          @Override
+          public void apply() {
+            a.doStartAnnotation(key, value);
+            targetDoc.startAnnotation(key, value);
+          }
+        };
+      }
+    }
+
+    class EndAnnotationGenerator extends RandomMutationComponentGenerator {
+      @Override
+      int potential() {
+        return -3;
+      }
+
+      @Override
+      RandomizerMutationComponent generate(boolean valid) {
+        if (!valid) {
+          return null;
+        }
+        return pickRandomNonNullMappedElement(r, p.getAnnotationKeys(),
+            new Mapper<String, RandomizerMutationComponent>() {
+              @Override
+              public RandomizerMutationComponent map(final String key) {
+                switch (a.checkEndAnnotation(key, null)) {
+                  case ILL_FORMED:
+                    return null;
+                  case VALID:
+                    return new RandomizerMutationComponent() {
+                      @Override
+                      public ValidationResult check(ViolationCollector v) {
+                        return a.checkEndAnnotation(key, v);
+                      }
+
+                      @Override
+                      public void apply() {
+                        a.doEndAnnotation(key);
+                        targetDoc.endAnnotation(key);
+                      }
+                    };
+                  case INVALID_DOCUMENT:
+                  case INVALID_SCHEMA:
+                  default:
+                    throw new RuntimeException("unexpected validation result");
+                }
+              }
+            }
+          );
+      }
+    }
+
+    final RandomProvider r;
+    final Parameters p;
+    final DocumentSchema schemaConstraints;
+    @SuppressWarnings("rawtypes")
+    NindoAutomaton a;
+    NindoCursor targetDoc;
+    final IndexedDocument<Node, Element, Text> doc;
+
+    Generator(RandomProvider r, Parameters p, DocumentSchema s,
+        IndexedDocument<Node, Element, Text> doc) {
+      this.r = r;
+      this.p = p;
+      this.doc = doc;
+      this.schemaConstraints = s;
+    }
+
+    final List<RandomMutationComponentGenerator> componentGenerators =
+      Arrays.asList(new SkipGenerator(),
+          new CharactersGenerator(),
+          new DeleteCharactersGenerator(),
+          new ElementStartGenerator(),
+          new ElementEndGenerator(),
+          new DeleteElementStartGenerator(),
+          new DeleteElementEndGenerator(),
+          new SetAttributesGenerator(),
+          new UpdateAttributesGenerator(),
+          new StartAnnotationGenerator(),
+          new EndAnnotationGenerator()
+          );
+
+    @SuppressWarnings("rawtypes")
+    Nindo generate() {
+      while (true) {
+        this.a = new NindoAutomaton(schemaConstraints, doc);
+        Nindo.Builder b = new Nindo.Builder();
+        targetDoc = b;
+        boolean ok = generate1();
+        if (ok) {
+          return b.build();
+        }
+      }
+    }
+
+    boolean generate1() {
+      int desiredNumComponents = randomIntFromRange(r, 0, 
p.getMaxOpeningComponents());
+      for (int i = 0; i < desiredNumComponents; i++) {
+        RandomizerMutationComponent component = 
pickRandomNonNullMappedElement(r,
+            componentGenerators,
+            new Mapper<RandomMutationComponentGenerator, 
RandomizerMutationComponent>() {
+              @Override
+              public RandomizerMutationComponent 
map(RandomMutationComponentGenerator g) {
+                return g.generate(p.getValidity());
+              }
+        });
+        if (component == null) {
+          // This can happen e.g. if we have skipped to the end of the 
document, and valid
+          // may be true, and there may not be any annotation options.
+          break;
+        }
+        component.apply();
+      }
+
+      // Close all open components.
+      while (a.checkFinish(null) == ValidationResult.ILL_FORMED) {
+        int potential = -3 - 1;
+        RandomizerMutationComponent component;
+        do {
+          potential++;
+          final int finalPotential = potential;
+          component = pickRandomNonNullMappedElement(r, componentGenerators,
+              new Mapper<RandomMutationComponentGenerator, 
RandomizerMutationComponent>() {
+            @Override
+            public RandomizerMutationComponent 
map(RandomMutationComponentGenerator g) {
+              if (g.potential() >= finalPotential) {
+                return null;
+              }
+              return g.generate(p.getValidity());
+            }
+          });
+        } while (potential < 0 && component == null);
+        if (component == null) {
+          // This can happen e.g. if we did an deleteAntiElementStart on the
+          // final </p> of the blip, where there is nothing to join with.
+          return false;
+        }
+        component.apply();
+      }
+      return true;
+    }
+  }
+
+  /**
+   * Returns a randomly-generated document mutation based on the given 
document,
+   * parameters, and schema.
+   */
+  public static Nindo generate(RandomProvider r, Parameters p,
+      DocumentSchema s, IndexedDocument<Node, Element, Text> doc) {
+    Nindo m = new Generator(r, p, s, doc).generate();
+    ViolationCollector v = NindoValidator.validate(doc, m, s);
+    assert !v.isIllFormed();
+    assert p.getValidity() == v.isValid();
+    return m;
+  }
+
+
+  /**
+   * Stand-alone main() for quick experimentation.
+   */
+  public static void main(String[] args) {
+//    IndexedDocument<Node, Element, Text> doc =
+//      DocProviders.POJO.parse("<body><line></line>a</body>");
+//
+//    Parameters p = new Parameters();
+//
+//    p.setMaxOpeningComponents(10);
+//
+//    for (int i = 0; i < 200; i++) {
+//      System.out.println("i=" + i);
+//      RandomProvider r = RandomProviderImpl.ofSeed(i);
+//      Nindo m = generate(r, p,
+//          NindoValidator.DEFAULT_BLIP_SCHEMA_CONSTRAINTS, doc);
+//      System.out.print(m);
+//    }
+  }
+
+}

Reply via email to