void printInfo(Line badLine, String msg) { Line line = getFirstLine(); PrintStream stream = System.out; int i = 0; while (line != null) { int indent = line.getIndent(); stream.println( CollectionUtils.repeat('.', line.getIndent()) + line.toString() + " indent:" + indent + CollectionUtils.repeat(' ', 20) + line.getCachedNumberValue() + " (" + i + ")"); if (line == badLine) { stream.println("\n\n\n"); stream = System.err; stream.println(msg); stream.println(">>>>>>>>>>>>>>>>>>>>>>>>> DIED ON LINE ABOVE <<<<<<<<<<<<<<<<<<\n\n"); } line = line.next(); i++; } }
/** Miscellaneous tests that depend on the previous state for convenience */ public void testMiscellaneous() { PainterRegistryImpl r = new PainterRegistryImpl("a", "b", new AnnotationPainter(null)); assertTrue(r.getPaintFunctions().isEmpty()); // Test we get back the function we registered r.registerPaintFunction(CollectionUtils.newStringSet("a"), p1); assertSame(p1, r.getPaintFunctions().iterator().next()); PainterRegistryImpl r2 = (PainterRegistryImpl) r.createExtension(); // Test the extension contains the function assertSame(p1, r2.getPaintFunctions().iterator().next()); assertEquals(r.debugGetVersion(), r2.debugGetKnownParentVersion()); // Test propagation occurs r.registerPaintFunction(CollectionUtils.newStringSet("b"), p2); assertFalse(r.debugGetVersion() == r2.debugGetKnownParentVersion()); assertTrue(r2.getPaintFunctions().contains(p2)); assertEquals(r.debugGetVersion(), r2.debugGetKnownParentVersion()); PainterRegistryImpl r3 = (PainterRegistryImpl) r2.createExtension(); // ensure cache is filled r3.getPaintFunctions(); // Test propagation occurs two levels deep after caching r.registerPaintFunction(CollectionUtils.newStringSet("c"), p3); assertFalse(r.debugGetVersion() == r2.debugGetKnownParentVersion()); assertTrue(r2.getPaintFunctions().contains(p3)); assertEquals(r.debugGetVersion(), r2.debugGetKnownParentVersion()); assertFalse(r2.debugGetVersion() == r3.debugGetKnownParentVersion()); assertTrue(r3.getPaintFunctions().contains(p3)); assertEquals(r2.debugGetVersion(), r3.debugGetKnownParentVersion()); }
public void testPropagatesDeeply() { PainterRegistryImpl r1 = new PainterRegistryImpl("a", "b", new AnnotationPainter(null)); PainterRegistryImpl r2 = (PainterRegistryImpl) r1.createExtension(); PainterRegistryImpl r3 = (PainterRegistryImpl) r2.createExtension(); r1.registerBoundaryFunction(CollectionUtils.newStringSet("a"), b1); assertTrue(r3.getBoundaryFunctions().contains(b1)); r1.registerPaintFunction(CollectionUtils.newStringSet("b"), p1); assertTrue(r3.getPaintFunctions().contains(p1)); }
public void testInitialState() { PainterRegistryImpl r = new PainterRegistryImpl("a", "b", new AnnotationPainter(null)); PainterRegistryImpl r2 = (PainterRegistryImpl) r.createExtension(); r2.registerPaintFunction(CollectionUtils.newStringSet("b"), p2); assertTrue(r2.getPaintFunctions().contains(p2)); assertTrue(r2.getKeys().contains("b")); r2.registerPaintFunction(CollectionUtils.newStringSet("d"), p4); assertTrue(r2.getPaintFunctions().contains(p4)); assertTrue(r2.getKeys().contains("d")); r2.registerBoundaryFunction(CollectionUtils.newStringSet("e"), b1); assertTrue(r2.getBoundaryFunctions().contains(b1)); assertTrue(r2.getKeys().contains("e")); }
public static void initRootRegistries() { if (rootRegistriesInitialised) { return; } rootRegistriesInitialised = true; // TODO(danilatos/patcoleman): Fix up this kludge Editor.TAB_TARGETS.addAll( CollectionUtils.newStringSet(Caption.TAGNAME, "profile-field", "text-setting")); ImeExtractor.register(ROOT_HANDLER_REGISTRY); ContentDocElement.register(ROOT_HANDLER_REGISTRY, ContentDocElement.DEFAULT_TAGNAME); Paragraph.register(ROOT_HANDLER_REGISTRY); LineRendering.registerLines(ROOT_HANDLER_REGISTRY); Caption.register(ROOT_HANDLER_REGISTRY); ChunkyElementHandler.register("br", ROOT_HANDLER_REGISTRY); AnnotationPaint.register(ROOT_HANDLER_REGISTRY); ImgDoodad.register(ROOT_HANDLER_REGISTRY); FormDoodads.register(ROOT_HANDLER_REGISTRY); // after registries, set selection information: ValidSelectionStrategy.registerTagForSelections( LineContainers.PARAGRAPH_FULL_TAGNAME, false, Skip.NONE); ValidSelectionStrategy.registerTagForSelections( AnnotationPaint.SPREAD_FULL_TAGNAME, false, Skip.SHALLOW); ValidSelectionStrategy.registerTagForSelections(LineContainers.LINE_TAGNAME, true, Skip.DEEP); }
/** A mock flushing operation sink. */ private static final class MockFlushingOperationSink implements FlushingOperationSink<WaveletOperation> { private static enum Method { CONSUME, FLUSH } private final Queue<Object[]> expectations = CollectionUtils.newLinkedList(); private Runnable resumeCommand; /** * Expects a call to consume an op. Optionally performs an action when the consume() call is * made. */ void expectConsume(WaveletOperation op, Runnable action) { expectations.add(new Object[] {Method.CONSUME, op, action}); } /** * Expects a call to flush and operation with a resume command. If {@code} succeed is true then * the call will return true, else it will return false (and the caller will expect the resume * command to be later invoked). */ void expectFlush(WaveletOperation operation, boolean succeed) { expectations.add(new Object[] {Method.FLUSH, operation, succeed}); } void checkExpectationsSatisfied() { assertTrue(expectations.isEmpty()); } /** @return the last command passed to flush() */ Runnable getLastResumeCommand() { return resumeCommand; } @Override public void consume(WaveletOperation op) { Object[] expected = expectations.remove(); assertEquals(expected[0], Method.CONSUME); assertSame(expected[1], op); if (expected[2] != null) { ((Runnable) expected[2]).run(); } } @Override public boolean flush(WaveletOperation operation, Runnable resume) { Object[] expected = expectations.remove(); assertEquals(expected[0], Method.FLUSH); assertSame(expected[1], operation); resumeCommand = resume; return (Boolean) expected[2]; } }
/** * Parses an element. * * @param parser tokenizer * @param parentElement the parent element to attach the parsed node to * @return a new element. */ private E parseElement(SafeXmlPullParser parser, D doc, E parentElement) { E element = doc.createElement( parser.getTagName(), CollectionUtils.newJavaMap(parser.getAttributes()), parentElement, null); parseChildren(parser, doc, element); return element; }
/** Opens a mux, binding its operation channels with operation-supporting wavelets. */ public static void openAndBind( WaveletOperationalizer operationalizer, WaveViewImpl<OpBasedWavelet> wave, WaveDocuments<? extends CcDocument> docRegistry, OperationChannelMultiplexer mux, IdFilter filter, Command whenOpened) { StaticChannelBinder staticBinder = new StaticChannelBinder(operationalizer, docRegistry); LiveChannelBinder liveBinder = new LiveChannelBinder(staticBinder, operationalizer, wave, mux, whenOpened); final Collection<KnownWavelet> remoteWavelets = CollectionUtils.createQueue(); final Collection<ObservableWaveletData> localWavelets = CollectionUtils.createQueue(); for (ObservableWaveletData wavelet : operationalizer.getWavelets()) { // Version 0 wavelets must be wavelets that the client has created in this // session. They are not to be included in the known-wavelet collection, // because the server does not know about them. if (wavelet.getVersion() > 0) { remoteWavelets.add( new KnownWavelet(wavelet, wavelet.getHashedVersion(), Accessibility.READ_WRITE)); } else { localWavelets.add(wavelet); } } // Start listening to wave events and channel events. wave.addListener(liveBinder); // This binder only starts getting events once open() has been called, since // that is what sets this binder as a mux listener. Since wavelet-to-channel // binding occurs through event callbacks, this listener setting must occur // before trying to bind localWavelets. mux.open(liveBinder, filter, remoteWavelets); for (ObservableWaveletData local : localWavelets) { mux.createOperationChannel(local.getWaveletId(), local.getCreator()); } }
private static void addDocumentSnapshotToWavelet(DocumentSnapshot snapshot, WaveletData container) throws InvalidParticipantAddress { DocOp op = CoreWaveletOperationSerializer.deserialize(snapshot.getDocumentOperation()); DocInitialization docInit = DocOpUtil.asInitialization(op); Collection<ParticipantId> contributors = CollectionUtils.newArrayList(); for (String p : snapshot.getContributorList()) { contributors.add(getParticipantId(p)); } container.createDocument( snapshot.getDocumentId(), getParticipantId(snapshot.getAuthor()), contributors, docInit, snapshot.getLastModifiedTime(), snapshot.getLastModifiedVersion()); }
/** * @param xmlString * @return parsed string */ public D parse(String xmlString) { SafeXmlPullParser parser; try { parser = XmlParserFactory.buffered(xmlString); } catch (XmlParseException e) { throw new RuntimeException("Cannot parse xml: " + xmlString, e); } // TODO(ohler): This can be an infinite loop. Fix that. while (parser.getCurrentType() != ItemType.START_ELEMENT) { parser.next(); } D document = factory.create(parser.getTagName(), CollectionUtils.newJavaMap(parser.getAttributes())); parseChildren(parser, document, document.getDocumentElement()); return document; }
/** * Instances of this class encapsulate the event to signal mapping logic for a specific environment * (os/browser). * * <p>Contains as much of the signal event logic as possible in a POJO testable manner. * * @author [email protected] (Daniel Danilatos) */ public final class SignalKeyLogic { /** For webkit + IE I think also all browsers on windows? */ public static final int IME_CODE = 229; private static final String DELETE_KEY_IDENTIFIER = "U+007F"; // TODO(danilatos): Use int map private static final Set<Integer> NAVIGATION_KEYS = new HashSet<Integer>(); private static final StringMap<Integer> NAVIGATION_KEY_IDENTIFIERS = CollectionUtils.createStringMap(); static { NAVIGATION_KEY_IDENTIFIERS.put("Left", KeyCodes.KEY_LEFT); NAVIGATION_KEY_IDENTIFIERS.put("Right", KeyCodes.KEY_RIGHT); NAVIGATION_KEY_IDENTIFIERS.put("Up", KeyCodes.KEY_UP); NAVIGATION_KEY_IDENTIFIERS.put("Down", KeyCodes.KEY_DOWN); NAVIGATION_KEY_IDENTIFIERS.put("PageUp", KeyCodes.KEY_PAGEUP); NAVIGATION_KEY_IDENTIFIERS.put("PageDown", KeyCodes.KEY_PAGEDOWN); NAVIGATION_KEY_IDENTIFIERS.put("Home", KeyCodes.KEY_HOME); NAVIGATION_KEY_IDENTIFIERS.put("End", KeyCodes.KEY_END); NAVIGATION_KEY_IDENTIFIERS.each( new ProcV<Integer>() { public void apply(String key, Integer keyCode) { NAVIGATION_KEYS.add(keyCode); } }); } public enum UserAgentType { WEBKIT, GECKO, IE } public enum OperatingSystem { WINDOWS, MAC, LINUX } @VisibleForTesting public static class Result { @VisibleForTesting public int keyCode; // Sentinal by default for testing purposes @VisibleForTesting public KeySignalType type = KeySignalType.SENTINAL; } private final UserAgentType userAgent; private final boolean commandIsCtrl; // Hack, get rid of this final boolean commandComboDoesntGiveKeypress; /** * @param userAgent * @param os Operating system */ public SignalKeyLogic( UserAgentType userAgent, OperatingSystem os, boolean commandComboDoesntGiveKeypress) { this.userAgent = userAgent; this.commandComboDoesntGiveKeypress = commandComboDoesntGiveKeypress; commandIsCtrl = os != OperatingSystem.MAC; } public boolean commandIsCtrl() { return commandIsCtrl; } public void computeKeySignalType( Result result, String typeName, int keyCode, int which, String keyIdentifier, boolean metaKey, boolean ctrlKey, boolean altKey, boolean shiftKey) { boolean ret = true; int typeInt; if ("keydown".equals(typeName)) { typeInt = Event.ONKEYDOWN; } else if ("keypress".equals(typeName)) { typeInt = Event.ONKEYPRESS; } else if ("keyup".equals(typeName)) { result.type = null; return; } else { throw new AssertionError("Non-key-event passed to computeKeySignalType"); } KeySignalType type; int computedKeyCode = which != 0 ? which : keyCode; if (computedKeyCode == 10) { computedKeyCode = KeyCodes.KEY_ENTER; } // For non-firefox browsers, we only get keydown events for IME, no keypress boolean isIME = computedKeyCode == IME_CODE; boolean commandKey = commandIsCtrl ? ctrlKey : metaKey; // Some trace logging very useful to debug EditorStaticDeps.logger .trace() .log( "KEY SIGNAL IN PROCESS identifier = " + keyIdentifier + " code = " + computedKeyCode + " type = " + (typeInt == Event.ONKEYDOWN ? "KeyDown" : "KeyPress") + (ctrlKey ? " CTRL" : "") + (shiftKey ? " SHIFT" : "") + (altKey ? " ALT" : "")); switch (userAgent) { case WEBKIT: // This is a bit tricky because there are significant differences // between safari 3.0 and safari 3.1... // We could probably actually almost use the same code that we use for IE // for safari 3.1, because with 3.1 the webkit folks made a big shift to // get the events to be in line with IE for compatibility. 3.0 events // are a lot more similar to FF, but different enough to need special // handling. However, it seems that using more advanced features like // keyIdentifier for safaris is probably better and more future-proof, // as well as being compatible between the two, so for now we're not // using IE logic for safari 3.1 // Weird special large keycode numbers for safari 3.0, where it gives // us keypress events (though they happen after the dom is changed, // for some things like delete. So not too useful). The number // 63200 is known as the cutoff mark. if (typeInt == Event.ONKEYDOWN && computedKeyCode > 63200) { result.type = null; return; } else if (typeInt == Event.ONKEYPRESS) { // Skip keypress for tab and escape, because they are the only non-input keys // that don't have keycodes above 63200. This is to prevent them from being treated // as INPUT in the || = keypress below. See (X) below if (computedKeyCode == KeyCodes.KEY_ESCAPE || computedKeyCode == KeyCodes.KEY_TAB) { result.type = null; return; } } // boolean isPossiblyCtrlInput = typeInt == Event.ONKEYDOWN && ret.getCtrlKey(); boolean isActuallyCtrlInput = false; boolean startsWithUPlus = keyIdentifier != null && keyIdentifier.startsWith("U+"); // Need to use identifier for the delete key because the keycode conflicts // with the keycode for the full stop. if (isIME) { // If is IME, override the logic below - we get keyIdentifiers for IME events, // but those are basically useless as the event is basically still an IME input // event (e.g. keyIdentifier might say "Up", but it's certainly not navigation, // it's just the user selecting from the IME dialog). type = KeySignalType.INPUT; } else if ((DELETE_KEY_IDENTIFIER.equals(keyIdentifier) && typeInt == Event.ONKEYDOWN) || computedKeyCode == KeyCodes.KEY_BACKSPACE) { // WAVE-407 Avoid missing the '.' char (KEYPRESS + CODE 46) // ensuring it's a KEYDOWN event with a DELETE_KEY_IDENTIFIER type = KeySignalType.DELETE; } else if (NAVIGATION_KEY_IDENTIFIERS.containsKey(keyIdentifier) && typeInt == Event.ONKEYDOWN) { // WAVE-407 Avoid missing chars with NAVIGATION_KEY_IDENTIFIERS but // represeting a SHIFT + key char (! " · ...). Navigation events come // with KEYDOWN, not with KEYPRESS type = KeySignalType.NAVIGATION; // Escape, backspace and context-menu-key (U+0010) are, to my knowledge, // the only non-navigation keys that // have a "U+..." keyIdentifier, so we handle them explicitly. // (Backspace was handled earlier). } else if (computedKeyCode == KeyCodes.KEY_ESCAPE || "U+0010".equals(keyIdentifier)) { type = KeySignalType.NOEFFECT; } else if (computedKeyCode < 63200 && // if it's not a safari 3.0 non-input key (See (X) above) (typeInt == Event.ONKEYPRESS || // if it's a regular keypress startsWithUPlus || computedKeyCode == KeyCodes.KEY_ENTER)) { type = KeySignalType.INPUT; isActuallyCtrlInput = ctrlKey || (commandComboDoesntGiveKeypress && commandKey); } else { type = KeySignalType.NOEFFECT; } // Maybe nullify it with the same logic as IE, EXCEPT for the special // Ctrl Input webkit behaviour, and IME for windows if (isActuallyCtrlInput) { if (computedKeyCode == KeyCodes.KEY_ENTER) { ret = typeInt == Event.ONKEYDOWN; } // HACK(danilatos): Don't actually nullify isActuallyCtrlInput for key press. // We get that for AltGr combos on non-mac computers. } else if (isIME || keyCode == KeyCodes.KEY_TAB) { ret = typeInt == Event.ONKEYDOWN; } else { ret = maybeNullWebkitIE(ret, typeInt, type); } if (!ret) { result.type = null; return; } break; case GECKO: boolean hasKeyCodeButNotWhich = keyCode != 0 && which == 0; // Firefox is easy for deciding signal events, because it issues a keypress for // whenever we would want a signal. So we can basically ignore all keydown events. // It also, on all OSes, does any default action AFTER the keypress (even for // things like Ctrl/Meta+C, etc). So keypress is perfect for us. // Ctrl+Space is an exception, where we don't get a keypress // Firefox also gives us keypress events even for Windows IME input if (ctrlKey && !altKey && !shiftKey && computedKeyCode == ' ') { if (typeInt != Event.ONKEYDOWN) { result.type = null; return; } } else if (typeInt == Event.ONKEYDOWN) { result.type = null; return; } // Backspace fails the !hasKeyCodeButNotWhich test, so check it explicitly first if (computedKeyCode == KeyCodes.KEY_BACKSPACE) { type = KeySignalType.DELETE; // This 'keyCode' but not 'which' works very nicely for catching normal typing input keys, // the only 'exceptions' I've seen so far are bksp & enter which have both } else if (!hasKeyCodeButNotWhich || computedKeyCode == KeyCodes.KEY_ENTER || computedKeyCode == KeyCodes.KEY_TAB) { type = KeySignalType.INPUT; } else if (computedKeyCode == KeyCodes.KEY_DELETE) { type = KeySignalType.DELETE; } else if (NAVIGATION_KEYS.contains(computedKeyCode)) { type = KeySignalType.NAVIGATION; } else { type = KeySignalType.NOEFFECT; } break; case IE: // Unfortunately IE gives us the least information, so there are no nifty tricks. // So we pretty much need to use some educated guessing based on key codes. // Experimentation page to the rescue. boolean isKeydownForInputKey = isInputKeyCodeIE(computedKeyCode); // IE has some strange behaviour with modifiers and whether or not there will // be a keypress. Ctrl kills the keypress, unless shift is also held. // Meta doesn't kill it. Alt always kills the keypress, overriding other rules. boolean hasModifiersThatResultInNoKeyPress = altKey || (ctrlKey && !shiftKey); if (typeInt == Event.ONKEYDOWN) { if (isKeydownForInputKey) { type = KeySignalType.INPUT; } else if (computedKeyCode == KeyCodes.KEY_BACKSPACE || computedKeyCode == KeyCodes.KEY_DELETE) { type = KeySignalType.DELETE; } else if (NAVIGATION_KEYS.contains(computedKeyCode)) { type = KeySignalType.NAVIGATION; } else { type = KeySignalType.NOEFFECT; } } else { // Escape is the only non-input thing that has a keypress event if (computedKeyCode == KeyCodes.KEY_ESCAPE) { result.type = null; return; } assert typeInt == Event.ONKEYPRESS; // I think the guessCommandFromModifiers() check here isn't needed, // but i feel safer putting it in. type = KeySignalType.INPUT; } if (hasModifiersThatResultInNoKeyPress || isIME || computedKeyCode == KeyCodes.KEY_TAB) { ret = typeInt == Event.ONKEYDOWN ? ret : false; } else { ret = maybeNullWebkitIE(ret, typeInt, type); } if (!ret) { result.type = null; return; } break; default: throw new UnsupportedOperationException("Unhandled user agent"); } if (ret) { result.type = type; result.keyCode = computedKeyCode; } else { result.type = null; return; } } private static final boolean isInputKeyCodeIE(int keyCode) { /* DATA ---- For KEYDOWN: "Input" 48-57 (numbers) 65-90 (a-z) 96-111 (Numpad digits & other keys, with numlock off. with numlock on, they behave like their corresponding keys on the rest of the keyboard) 186-192 219-222 (random non-alphanumeric next to letters on RHS + backtick) 229 Code that the input has passed to an IME Non-"input" < 48 ('0') 91-93 (Left & Right Win keys, ContextMenu key) 112-123 (F1-F12) 144-5 (NUMLOCK,SCROLL LOCK) For KEYPRESS: only "input" things get this event! yay! not even backspace! Well, one exception: ESCAPE */ // boundaries in keycode ranges where the keycode for a keydown is for an input // key. at "ON" it is, starting from the number going up, and the opposite for "OFF". final int A_ON = 48; final int B_OFF = 91; final int C_ON = 96; final int D_OFF = 112; final int E_ON = 186; return (keyCode == 9 || keyCode == 32 || keyCode == 13) || // And tab, enter & spacebar, of course! (keyCode >= A_ON && keyCode < B_OFF) || (keyCode >= C_ON && keyCode < D_OFF) || (keyCode >= E_ON); } /** Common logic between Webkit and IE for deciding whether we want the keydown or the keypress */ private static boolean maybeNullWebkitIE(boolean ret, int typeInt, KeySignalType type) { // Use keydown as the signal for everything except input. // This is because the mutation always happens after the keypress for // input (this is especially important for chrome, // which interleaves deferred commands between keydown and keypress). // // For everything else, keypress is redundant with keydown, and also, the resulting default // dom mutation (if any) often happens after the keydown but before the keypress in webkit. // Also, if the 'Command' key is held for chrome/safari etc, we want to get the keydown // event, NOT the keypress event, for everything because of things like ctrl+c etc. // where sometimes it'll happen just after the keydown, or sometimes we just won't // get a keypress at all if (typeInt == (type == KeySignalType.INPUT ? Event.ONKEYDOWN : Event.ONKEYPRESS)) { return false; } return ret; } }
/** * Binds operation channels from a {@link OperationChannelMultiplexer mux} with the output sinks of * wavelets, and keeps binding matching channel/wavelet pairs while live. * * @author [email protected] (David Hearnden) */ public final class LiveChannelBinder implements WaveViewListener, OperationChannelMultiplexer.Listener { private final StaticChannelBinder binder; private final WaveletOperationalizer operationalizer; private final WaveViewImpl<OpBasedWavelet> wave; private final OperationChannelMultiplexer mux; private final Command whenOpened; /** * Operation channels waiting to be bound. This map is populated from {@link * #onOperationChannelCreated(OperationChannel, ObservableWaveletData, Accessibility)}, and * depleted by {@link #connect(String)}. */ private final StringMap<OperationChannel> channels = CollectionUtils.createStringMap(); // // The binding flow is not completely trivial, because it has to work with // two directions of control flow: // // Client-created wavelets: // 1. wavelet shows up in the model // 2. this binder tells mux to create op channel, // 3. its operation channel shows up, then // 4. wavelet and op channel are bound together. // // Server-created wavelets: // 1. op channel shows up in the mux, // 2. this binder builds wavelet and puts it in the model, // 3. wavelet shows up in the model, then // 4. wavelet and op channel are bound together. // // Also, the initial set of operation channels when opening a wave with known // wavelet states is just like the server-created wavelet flow, except without // step 2. // private LiveChannelBinder( StaticChannelBinder binder, WaveletOperationalizer operationalizer, WaveViewImpl<OpBasedWavelet> wave, OperationChannelMultiplexer mux, Command whenOpened) { this.binder = binder; this.operationalizer = operationalizer; this.wave = wave; this.mux = mux; this.whenOpened = whenOpened; } /** Opens a mux, binding its operation channels with operation-supporting wavelets. */ public static void openAndBind( WaveletOperationalizer operationalizer, WaveViewImpl<OpBasedWavelet> wave, WaveDocuments<? extends CcDocument> docRegistry, OperationChannelMultiplexer mux, IdFilter filter, Command whenOpened) { StaticChannelBinder staticBinder = new StaticChannelBinder(operationalizer, docRegistry); LiveChannelBinder liveBinder = new LiveChannelBinder(staticBinder, operationalizer, wave, mux, whenOpened); final Collection<KnownWavelet> remoteWavelets = CollectionUtils.createQueue(); final Collection<ObservableWaveletData> localWavelets = CollectionUtils.createQueue(); for (ObservableWaveletData wavelet : operationalizer.getWavelets()) { // Version 0 wavelets must be wavelets that the client has created in this // session. They are not to be included in the known-wavelet collection, // because the server does not know about them. if (wavelet.getVersion() > 0) { remoteWavelets.add( new KnownWavelet(wavelet, wavelet.getHashedVersion(), Accessibility.READ_WRITE)); } else { localWavelets.add(wavelet); } } // Start listening to wave events and channel events. wave.addListener(liveBinder); // This binder only starts getting events once open() has been called, since // that is what sets this binder as a mux listener. Since wavelet-to-channel // binding occurs through event callbacks, this listener setting must occur // before trying to bind localWavelets. mux.open(liveBinder, filter, remoteWavelets); for (ObservableWaveletData local : localWavelets) { mux.createOperationChannel(local.getWaveletId(), local.getCreator()); } } @Override public void onFailed(CorruptionDetail detail) { throw new RuntimeException(detail); } @Override public void onOpenFinished() { if (whenOpened != null) { whenOpened.execute(); } } // // Wavelet and Channel lifecycle events: // @Override public void onWaveletAdded(ObservableWavelet wavelet) { String id = ModernIdSerialiser.INSTANCE.serialiseWaveletId(wavelet.getId()); if (channels.containsKey(id)) { connect(id); } else { // This will trigger the onOperationChannelCreated callback below. mux.createOperationChannel(wavelet.getId(), wavelet.getCreatorId()); } } @Override public void onOperationChannelCreated( OperationChannel channel, ObservableWaveletData snapshot, Accessibility accessibility) { WaveletId wid = snapshot.getWaveletId(); String id = ModernIdSerialiser.INSTANCE.serialiseWaveletId(wid); Preconditions.checkState(!channels.containsKey(id)); channels.put(id, channel); if (wave.getWavelet(wid) != null) { connect(id); } else { // This will trigger the onWaveletAdded callback above. wave.addWavelet(operationalizer.operationalize(snapshot)); } } @Override public void onWaveletRemoved(ObservableWavelet wavelet) { // TODO } @Override public void onOperationChannelRemoved(OperationChannel channel, WaveletId waveletId) { // TODO } private void connect(String id) { binder.bind(id, removeAndReturn(channels, id)); } // Something that should have been on StringMap from the beginning. private static <V> V removeAndReturn(StringMap<V> map, String key) { V value = map.get(key); map.remove(key); return value; } }
/** * De-multiplexes object channels a client is listening to. * * <p>Packets arrive with two keys, 'id' to identify the object, and 'm' containing the message * payload. * * @author [email protected] (Daniel Danilatos) */ public class GaeChannelDemuxer { // There should only ever be one global instance, or strange things will happen. private GaeChannelDemuxer() {} private static final GaeChannelDemuxer INSTANCE = new GaeChannelDemuxer(); public static GaeChannelDemuxer get() { return INSTANCE; } /** Channel that listens for messages to a specific object */ public interface GaeChannel { void onMessage(JsoView data); } private final Log log = Logs.create("demuxer"); private final StringMap<GaeChannel> channels = CollectionUtils.createStringMap(); private String currentToken = null; @SuppressWarnings("unused") // used by native code private JavaScriptObject socket; public void registerChannel(String objectId, GaeChannel channel) { Preconditions.checkState( !channels.containsKey(objectId), "Channel handler already registered for " + objectId); channels.put(objectId, channel); } public void deregisterChannel(String objectId) { Preconditions.checkState( channels.containsKey(objectId), "Channel handler not registered for %s", objectId); channels.remove(objectId); } public void connect(String token) { if (!Preconditions.checkNotNull(token, "Null token").equals(currentToken)) { log.log(Level.INFO, "Connecting with token ", token); currentToken = token; connectNative(token); } else { log.log(Level.DEBUG, "Already using same token, ignoring ", token); } } private native void connectNative(String token) /*-{ var me = this; var socket = [email protected]::socket; if (socket != null) { socket.close(); } channel = new $wnd.goog.appengine.Channel(token); socket = channel.open(); [email protected]::socket = socket; socket.onopen = $entry(function() { me. @com.google.walkaround.wave.client.GaeChannelDemuxer::onOpened() (); }); socket.onmessage = $entry(function(msg) { me. @com.google.walkaround.wave.client.GaeChannelDemuxer::onMessage(Ljava/lang/String;) (msg.data); }); socket.onerror = $entry(function(err) { me. @com.google.walkaround.wave.client.GaeChannelDemuxer::onError(ILjava/lang/String;) (err.code, err.description); }); socket.onclose = $entry(function() { me. @com.google.walkaround.wave.client.GaeChannelDemuxer::onClose() (); }); }-*/; @SuppressWarnings("unused") // called by native code private void onOpened() { log.log(Level.DEBUG, "onOpened "); } @SuppressWarnings("unused") // called by native code private void onMessage(String data) { log.log(Level.DEBUG, "onMessage data=", data); if (data == null) { log.log(Level.WARNING, "Null data on channel"); return; } try { JsoView jso = JsUtil.eval(data); if (!jso.containsKey("id") || !jso.containsKey("m")) { throw new MessageException("Missing fields"); } String id = jso.getString("id"); JsoView m = jso.getJsoView("m"); GaeChannel channel = channels.get(id); if (channel == null) { log.log(Level.WARNING, "No channel registered for object with id ", id); return; } channel.onMessage(m); } catch (MessageException e) { log.log(Level.WARNING, "Bad data on channel ", data, " ", e); } } @SuppressWarnings("unused") // called by native code private void onError(int httpCode, String description) { log.log(Level.WARNING, "onError code=", httpCode, " description=", description); } @SuppressWarnings("unused") // called by native code private void onClose() { log.log(Level.DEBUG, "onClose "); } }
/** * Presents a search model into a search view. * * <p>This class invokes rendering, and controls the lifecycle of digest views. It also handles all * UI gesture events sourced from views in the search panel. * * @author [email protected] (David Hearnden) */ public final class SearchPresenter implements Search.Listener, SearchPanelView.Listener, SearchView.Listener, ProfileListener { /** Handles wave actions. */ public interface WaveActionHandler { /** Handles the wave creation action. */ void onCreateWave(); /** Handles a wave selection action. */ void onWaveSelected(WaveId id); } private static final SearchPresenterMessages messages = GWT.create(SearchPresenterMessages.class); /** How often to repeat the search query. */ private static final int POLLING_INTERVAL_MS = 15000; // 15s private static final String DEFAULT_SEARCH = "in:inbox"; private static final int DEFAULT_PAGE_SIZE = 20; // External references private final TimerService scheduler; private final Search search; private final SearchPanelView searchUi; private final WaveActionHandler actionHandler; // Internal state private final IdentityMap<DigestView, Digest> digestUis = CollectionUtils.createIdentityMap(); private final IncrementalTask searchUpdater = new IncrementalTask() { @Override public boolean execute() { doSearch(); return true; } }; private final Task renderer = new Task() { @Override public void execute() { if (search.getState() == State.READY) { render(); } else { // Try again later. scheduler.schedule(this); } } }; /** Current search query. */ private String queryText = DEFAULT_SEARCH; /** Number of results to query for. */ private int querySize = DEFAULT_PAGE_SIZE; /** Current selected digest. */ private DigestView selected; /** The dispatcher of profiles events. */ SourcesEvents<ProfileListener> profiles; private boolean isRenderingInProgress = false; private final SearchPresenterResources.Css css; SearchPresenter( SearchPresenterResources.Css css, TimerService scheduler, Search search, SearchPanelView searchUi, WaveActionHandler actionHandler, SourcesEvents<ProfileListener> profiles) { this.css = css; this.search = search; this.searchUi = searchUi; this.scheduler = scheduler; this.actionHandler = actionHandler; this.profiles = profiles; } /** * Creates a search presenter. * * @param model model to present * @param view view to render into * @param actionHandler handler for actions * @param profileEventsDispatcher the dispatcher of profile events. */ public static SearchPresenter create( Search model, SearchPanelView view, WaveActionHandler actionHandler, SourcesEvents<ProfileListener> profileEventsDispatcher) { SearchPresenterResources.Css css = SearchPresenterResources.Loader.res.css(); SearchPresenter presenter = new SearchPresenter( css, SchedulerInstance.getHighPriorityTimer(), model, view, actionHandler, profileEventsDispatcher); presenter.init(); return presenter; } /** Performs initial presentation, and attaches listeners to live objects. */ private void init() { initToolbarMenu(); initSearchBox(); render(); search.addListener(this); profiles.addListener(this); searchUi.init(this); searchUi.getSearch().init(this); // Fire a polling search. scheduler.scheduleRepeating(searchUpdater, 0, POLLING_INTERVAL_MS); StateManagerInstance.get() .onStateChanged( false, new StateChangedHandler() { @Override public void onStateChanged(StateChangedEvent event) { StateAbstractDTO state = event.getState(); if (state instanceof StateContentDTO) { StateContentDTO ctn = (StateContentDTO) state; if (ctn.isWave() && ctn.isParticipant()) { // This will work only if the wave is in the current search // GwtWaverefEncoder.decodeWaveRefFromPath(DEFAULT_SEARCH) // final String waveUri = // GwtWaverefEncoder.encodeToUriPathSegment(WaveRef.of(digest.getWaveId())); selectWaveUri(ctn.getWaveRef().replaceFirst("\\/~\\/conv\\+root", "")); } } } }); } /** Releases resources and detaches listeners. */ public void destroy() { scheduler.cancel(searchUpdater); scheduler.cancel(renderer); searchUi.getSearch().reset(); searchUi.reset(); search.removeListener(this); profiles.removeListener(this); } /** Adds custom buttons to the toolbar. */ private void initToolbarMenu() { GroupingToolbar.View toolbarUi = searchUi.getToolbar(); ToolbarView group = toolbarUi.addGroup(); new ToolbarButtonViewBuilder() .setText(messages.newWave()) .applyTo( group.addClickButton(), new ToolbarClickButton.Listener() { @Override public void onClicked() { actionHandler.onCreateWave(); // HACK(hearnden): To mimic live search, fire a search poll // reasonably soon (500ms) after creating a wave. This will be unnecessary // with a real live search implementation. The delay is to give // enough time for the wave state to propagate to the server. int delay = 500; scheduler.scheduleRepeating(searchUpdater, delay, POLLING_INTERVAL_MS); } }); // Fake group with empty button - to force the separator be displayed. group = toolbarUi.addGroup(); new ToolbarButtonViewBuilder().setText("").applyTo(group.addClickButton(), null); } /** Initializes the search box. */ private void initSearchBox() { searchUi.getSearch().setQuery(queryText); } /** Executes the current search. */ private void doSearch() { search.find(queryText, querySize); } /** Renders the current state of the search result into the panel. */ private void render() { renderTitle(); renderDigests(); renderShowMore(); } /** Renders the paging information into the title bar. */ private void renderTitle() { int resultEnd = querySize; String totalStr; if (search.getTotal() != Search.UNKNOWN_SIZE) { resultEnd = Math.min(resultEnd, search.getTotal()); totalStr = messages.of(search.getTotal()); } else { totalStr = messages.ofUnknown(); } searchUi.setTitleText(queryText + " (0-" + resultEnd + " " + totalStr + ")"); } private void renderDigests() { isRenderingInProgress = true; // Preserve selection on re-rendering. WaveId toSelect = selected != null ? digestUis.get(selected).getWaveId() : null; searchUi.clearDigests(); digestUis.clear(); setSelected(null); for (int i = 0, size = search.getMinimumTotal(); i < size; i++) { Digest digest = search.getDigest(i); if (digest == null) { continue; } DigestView digestUi = searchUi.insertBefore(null, digest); digestUis.put(digestUi, digest); if (digest.getWaveId().equals(toSelect)) { setSelected(digestUi); } } isRenderingInProgress = false; } private void renderShowMore() { searchUi.setShowMoreVisible( search.getTotal() == Search.UNKNOWN_SIZE || querySize < search.getTotal()); } // // UI gesture events. // private void setSelected(DigestView digestUi) { if (digestUi != selected) { if (selected != null) { selected.deselect(); } if (digestUi != null) { // Log.info("SearchPresenter: Set selected"); digestUi.select(); } selected = digestUi; } } /** Invokes the wave-select action on the currently selected digest. */ private void openSelected() { actionHandler.onWaveSelected(digestUis.get(selected).getWaveId()); } @Override public void onClicked(DigestView digestUi) { setSelected(digestUi); openSelected(); } @Override public void onQueryEntered() { queryText = searchUi.getSearch().getQuery(); querySize = DEFAULT_PAGE_SIZE; searchUi.setTitleText(messages.searching()); doSearch(); } @Override public void onShowMoreClicked() { querySize += DEFAULT_PAGE_SIZE; doSearch(); } // // Search events. For now, dumbly re-render the whole list. // @Override public void onStateChanged() { // // If the state switches to searching, then do nothing. A manual title-bar // update is performed in onQueryEntered(), and the title-bar should not be // updated when a polling search fires. // // If the state switches to ready, then just update the title. Do not // necessarily re-render, since that is only necessary if a change occurred, // which would have fired one of the other methods below. // if (search.getState() == State.READY) { renderTitle(); } } @Override public void onDigestAdded(int index, Digest digest) { renderLater(); } @Override public void onDigestRemoved(int index, Digest digest) { renderLater(); } /** * Find the DigestView that contains a certain digest * * @param digest the digest the DigestView should contain. * @return the DigestView containing the digest. {@null} if the digest is not found. */ private DigestView findDigestView(Digest digest) { DigestView digestUi = searchUi.getFirst(); while (digestUi != null) { if (digestUis.get(digestUi).equals(digest)) { return digestUi; } digestUi = searchUi.getNext(digestUi); } return null; } private void selectWaveUri(String waveId) { // Log.info("Trying to select waveId " + waveId); DigestView digestUi = searchUi.getFirst(); while (digestUi != null) { Digest digest = digestUis.get(digestUi); WaveId iteraWaveId = digest.getWaveId(); // Log.info("Comparing to waveId " + iteraWaveId); if (iteraWaveId.equals(WaveId.deserialise(waveId))) { setSelected(digestUi); return; } digestUi = searchUi.getNext(digestUi); } setSelected(null); } /** * Insert a digest before amongst the currently shown digests * * @param insertRef the DigestView to insert the new digest before. The new digest is inserted * last if insertRef is {@null}. * @param digest the digest to insert. * @return the newly inserted DigestView. */ private DigestView insertDigest(DigestView insertRef, Digest digest) { DigestView newDigestUi = null; if (insertRef != null) { newDigestUi = searchUi.insertBefore(insertRef, digest); digestUis.put(newDigestUi, digest); } else { insertRef = searchUi.getLast(); newDigestUi = searchUi.insertAfter(insertRef, digest); digestUis.put(newDigestUi, digest); } return newDigestUi; } @Override public void onDigestReady(int index, Digest digest) { if (isRenderingInProgress) { return; } setSelected(null); DigestView digestToRemove = findDigestView(digest); if (digestToRemove == null) { return; } DigestView insertRef = searchUi.getNext(digestToRemove); digestToRemove.remove(); DigestView newDigestUi = insertDigest(insertRef, digest); setSelected(newDigestUi); } @Override public void onTotalChanged(int total) { renderLater(); } private void renderLater() { if (!scheduler.isScheduled(renderer)) { scheduler.schedule(renderer); } } @Override public void onProfileUpdated(Profile profile) { // NOTE: Search panel will be re-rendered once for every profile that comes // back to the client. If this causes an efficiency problem then have the // SearchPanelRenderer to be the profile listener, rather than // SearchPresenter, and make it stateful. Have it remember which digests // have used which profiles in their renderings. renderLater(); } }
protected static ReadableStringSet strs(String... strings) { return CollectionUtils.newStringSet(strings); }