/** * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.wave.client.testing; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.GWT.UncaughtExceptionHandler; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.RepeatingCommand; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.user.client.Command; import org.waveprotocol.wave.client.StageOne; import org.waveprotocol.wave.client.StageThree; import org.waveprotocol.wave.client.StageTwo; import org.waveprotocol.wave.client.StageZero; import org.waveprotocol.wave.client.Stages; import org.waveprotocol.wave.client.common.safehtml.SafeHtmlBuilder; import org.waveprotocol.wave.client.common.util.AsyncHolder; import org.waveprotocol.wave.client.concurrencycontrol.MuxConnector; import org.waveprotocol.wave.client.util.ClientFlags; import org.waveprotocol.wave.client.util.NullTypedSource; import org.waveprotocol.wave.client.util.OverridingTypedSource; import org.waveprotocol.wave.common.bootstrap.FlagConstants; import org.waveprotocol.wave.concurrencycontrol.channel.WaveViewService; import org.waveprotocol.wave.model.conversation.Conversation; import org.waveprotocol.wave.model.conversation.ConversationBlip; import org.waveprotocol.wave.model.conversation.ConversationThread; import org.waveprotocol.wave.model.conversation.ConversationView; import org.waveprotocol.wave.model.conversation.WaveBasedConversationView; import org.waveprotocol.wave.model.document.util.XmlStringBuilder; import org.waveprotocol.wave.model.id.IdGenerator; import org.waveprotocol.wave.model.schema.SchemaProvider; import org.waveprotocol.wave.model.schema.conversation.ConversationSchemas; import org.waveprotocol.wave.model.testing.BasicFactories; import org.waveprotocol.wave.model.testing.FakeIdGenerator; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV; import org.waveprotocol.wave.model.util.StringMap; 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.ReadableWaveletData; import org.waveprotocol.wave.model.wave.data.WaveViewData; import org.waveprotocol.wave.model.wave.data.impl.WaveViewDataImpl; 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; import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl.WaveletConfigurator; import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl.WaveletFactory; /** * Kicks off some initial actions for development purposes. * */ public class UndercurrentHarness implements EntryPoint { private UndercurrentHarness() { } private static boolean loaded; /** * Runs the harness script. */ @Override public void onModuleLoad() { if (loaded) { return; } loaded = true; final Timeline timeline = new Timeline(); new Stages() { @Override protected AsyncHolder<StageZero> createStageZeroLoader() { return new StageZero.DefaultProvider() { @Override protected void onStageInit() { timeline.add("stage0_start"); } @Override protected void onStageLoaded() { timeline.add("stage0_end"); } @Override protected UncaughtExceptionHandler createUncaughtExceptionHandler() { return GWT.getUncaughtExceptionHandler(); } }; } @Override protected AsyncHolder<StageOne> createStageOneLoader(StageZero zero) { return new StageOne.DefaultProvider(zero) { @Override protected void onStageInit() { timeline.add("stage1_start"); } @Override protected void onStageLoaded() { timeline.add("stage1_end"); } @Override protected Element createWaveHolder() { return Document.get().getElementById("initialHtml"); } }; } @Override protected AsyncHolder<StageTwo> createStageTwoLoader(StageOne one) { return new StageTwo.DefaultProvider(one) { @Override protected void onStageInit() { timeline.add("stage2_start"); } @Override protected void onStageLoaded() { timeline.add("stage2_end"); } @Override protected void fetchWave(Accessor<WaveViewData> whenReady) { timeline.add("fakewave_start"); WaveViewData fake = WaveFactory.create(getDocumentRegistry()); timeline.add("fakewave_end"); whenReady.use(fake); } @Override protected ParticipantId createSignedInUser() { return ParticipantId.ofUnsafe("nobody@example.com"); } @Override protected String createSessionId() { return "session"; } @Override protected MuxConnector createConnector() { return new MuxConnector() { @Override public void connect(Command whenOpened) { if (whenOpened != null) { whenOpened.execute(); } } @Override public void close() { // Ignore } }; } @Override protected WaveViewService createWaveViewService() { // The vacuous MuxConnector should avoid the need for a // communication layer. throw new UnsupportedOperationException(); } @Override protected SchemaProvider createSchemas() { return new ConversationSchemas(); } }; } @Override protected AsyncHolder<StageThree> createStageThreeLoader(StageTwo two) { ClientFlags.resetWithSourceForTesting(OverridingTypedSource.of(new NullTypedSource()) .withBoolean(FlagConstants.ENABLE_UNDERCURRENT_EDITING, true) .build()); return new StageThree.DefaultProvider(two) { @Override protected void onStageInit() { timeline.add("stage3_start"); } @Override protected void onStageLoaded() { timeline.add("stage3_end"); } }; } }.load(new Command() { @Override public void execute() { showInfo(timeline); } }); } /** * Populates the info box. Continuously reports which element has browser * focus, and reports timing information for the stage loading. * * @param timeline timeline to report */ private static void showInfo(Timeline timeline) { Element timeBox = Document.get().getElementById("timeline"); timeline.dump(timeBox); Scheduler.get().scheduleFixedDelay(new RepeatingCommand() { private final Element activeBox = Document.get().getElementById("active"); @Override public boolean execute() { Element e = getActiveElement(); String text = (e != null ? e.getTagName() + " id:" + e.getId() : "none"); activeBox.setInnerText(text); return true; } private native Element getActiveElement() /*-{ return $doc.activeElement; }-*/; }, 1000); } /** * Creates a sample wave with a conversation in it. */ private final static class WaveFactory { /** * Creates a sample wave. * * @param docFactory factory/registry for documents in the wave * @return the wave state of the sample wave. */ public static WaveViewDataImpl create(DocumentFactory<?> docFactory) { // Create a sample wave. WaveViewData sampleData = createSampleWave(); // Now build one that has the same setup state as that required by // undercurrent (complex issue with the per-document output sinks). WaveViewDataImpl newData = WaveViewDataImpl.create(sampleData.getWaveId()); WaveletDataImpl.Factory copier = WaveletDataImpl.Factory.create(docFactory); for (ReadableWaveletData src : sampleData.getWavelets()) { WaveletDataImpl copied = copier.create(src); for (ParticipantId p : src.getParticipants()) { copied.addParticipant(p); } copied.setVersion(copied.getVersion()); copied.setHashedVersion(src.getHashedVersion()); copied.setLastModifiedTime(src.getLastModifiedTime()); newData.addWavelet(copied); } return newData; } /** @return a sample wave with a conversation in it. */ private static WaveViewData createSampleWave() { final ParticipantId sampleAuthor = ParticipantId.ofUnsafe("nobody@example.com"); IdGenerator gen = FakeIdGenerator.create(); final WaveViewDataImpl waveData = WaveViewDataImpl.create(gen.newWaveId()); final DocumentFactory<?> docFactory = BasicFactories.fakeDocumentFactory(); final ObservableWaveletData.Factory<?> waveletDataFactory = new ObservableWaveletData.Factory<WaveletDataImpl>() { private final ObservableWaveletData.Factory<WaveletDataImpl> inner = WaveletDataImpl.Factory.create(docFactory); @Override public WaveletDataImpl create(ReadableWaveletData data) { WaveletDataImpl wavelet = inner.create(data); waveData.addWavelet(wavelet); return wavelet; } }; WaveletFactory<OpBasedWavelet> waveletFactory = BasicFactories .opBasedWaveletFactoryBuilder() .with(waveletDataFactory) .with(sampleAuthor) .build(); WaveViewImpl<?> wave = WaveViewImpl.create( waveletFactory, waveData.getWaveId(), gen, sampleAuthor, WaveletConfigurator.ADD_CREATOR); // Build a conversation in that wave. ConversationView v = WaveBasedConversationView.create(wave, gen); Conversation c = v.createRoot(); ConversationThread root = c.getRootThread(); sampleReply(root.appendBlip()); write(root.appendBlip()); write(root.appendBlip()); write(root.appendBlip()); return waveData; } private static void write(ConversationBlip blip) { org.waveprotocol.wave.model.document.Document d = blip.getContent(); d.emptyElement(d.getDocumentElement()); d.appendXml(XmlStringBuilder.createFromXmlString("<body><line></line>Hello World</body>")); } private static void sampleReply(ConversationBlip blip) { write(blip); ConversationThread thread = blip.addReplyThread(8); write(thread.appendBlip()); } private static void biggerSampleReply(ConversationBlip blip) { write(blip); ConversationThread thread = blip.addReplyThread(); sampleReply(thread.appendBlip()); sampleReply(thread.appendBlip()); write(thread.appendBlip()); } } private static class Timeline { private final StringMap<Integer> events = CollectionUtils.createStringMap(); private final Duration duration = new Duration(); void add(String name) { events.put(name, duration.elapsedMillis()); } void dump(Element timeBox) { final SafeHtmlBuilder timeHtml = new SafeHtmlBuilder(); timeHtml.appendHtmlConstant("<table cellpadding='0' cellspacing='0'>"); events.each(new ProcV<Integer>() { @Override public void apply(String key, Integer value) { timeHtml.appendHtmlConstant("<tr><td>"); timeHtml.appendEscaped(key); timeHtml.appendHtmlConstant(":</td><td>"); timeHtml.appendEscaped("" + value); timeHtml.appendHtmlConstant("</td></tr>"); } }); timeHtml.appendHtmlConstant("</table>"); timeBox.setInnerHTML(timeHtml.toSafeHtml().asString()); } } }