/** * Copyright 2011 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.wavepanel.view.fake; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import org.waveprotocol.wave.client.common.util.LinkedSequence; import org.waveprotocol.wave.client.render.ReductionBasedRenderer; import org.waveprotocol.wave.client.render.RenderingRules; import org.waveprotocol.wave.client.render.WaveRenderer; import org.waveprotocol.wave.client.wavepanel.view.AnchorView; import org.waveprotocol.wave.client.wavepanel.view.BlipMetaView; import org.waveprotocol.wave.client.wavepanel.view.BlipView; import org.waveprotocol.wave.client.wavepanel.view.InlineThreadView; import org.waveprotocol.wave.client.wavepanel.view.ParticipantView; import org.waveprotocol.wave.client.wavepanel.view.ParticipantsView; import org.waveprotocol.wave.client.wavepanel.view.RootThreadView; import org.waveprotocol.wave.client.wavepanel.view.ThreadView; import org.waveprotocol.wave.client.wavepanel.view.TopConversationView; import org.waveprotocol.wave.client.wavepanel.view.View; import org.waveprotocol.wave.client.wavepanel.view.dom.ModelAsViewProvider; 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.util.CollectionUtils; import org.waveprotocol.wave.model.util.IdentityMap; import org.waveprotocol.wave.model.util.IdentityMap.ProcV; import org.waveprotocol.wave.model.util.IdentityMap.Reduce; import org.waveprotocol.wave.model.util.Pair; import org.waveprotocol.wave.model.util.StringMap; import org.waveprotocol.wave.model.wave.ParticipantId; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; /** * A wave renderer that renders waves into fake view objects. */ public final class FakeRenderer implements WaveRenderer<View>, ModelAsViewProvider { /** Factory and registry of fake views. */ class ViewStore { final BiMap<ConversationBlip, FakeBlipView> blipUis = HashBiMap.create(); final BiMap<Conversation, FakeConversationView> convUis = HashBiMap.create(); final BiMap<ConversationThread, FakeRootThreadView> rootThreadUis = HashBiMap.create(); final BiMap<ConversationThread, FakeInlineThreadView> inlineThreadUis = HashBiMap.create(); final BiMap<ConversationThread, FakeAnchor> defaultAnchorUis = HashBiMap.create(); final BiMap<ConversationThread, FakeAnchor> inlineAnchorUis = HashBiMap.create(); /** Puts a value in a map and returns it. */ private <K, V> V put(Map<? super K, ? super V> map, K key, V value) { map.put(key, value); return value; } FakeBlipView createBlipView(ConversationBlip blip, LinkedSequence<FakeAnchor> anchors, LinkedSequence<FakeInlineConversationView> convos) { return put(blipUis, blip, new FakeBlipView(FakeRenderer.this, anchors, convos)); } FakeConversationView createTopConversationView(Conversation conv, FakeRootThreadView thread) { return put(convUis, conv, new FakeTopConversationView(thread)); } FakeConversationView createInlineConversationView( Conversation conv, FakeRootThreadView thread) { return put(convUis, conv, new FakeInlineConversationView(thread)); } FakeThreadView createRootThreadView( ConversationThread thread, LinkedSequence<FakeBlipView> blipUis) { return put(rootThreadUis, thread, new FakeRootThreadView(FakeRenderer.this, blipUis)); } FakeThreadView createInlineThreadView( ConversationThread thread, LinkedSequence<FakeBlipView> blipUis) { return put(inlineThreadUis, thread, new FakeInlineThreadView(FakeRenderer.this, blipUis)); } FakeAnchor createDefaultAnchorView(ConversationThread thread) { return put(defaultAnchorUis, thread, new FakeAnchor()); } FakeAnchor createInlineAnchorView(ConversationThread thread) { return put(inlineAnchorUis, thread, new FakeAnchor()); } } class Rules implements RenderingRules<View> { @Override public View render(ConversationBlip blip, IdentityMap<ConversationThread, View> replies) { return new FakeDocumentView(blip.getContent().toXmlString()); } @Override public FakeBlipView render(ConversationBlip blip, View document, IdentityMap<ConversationThread, View> defaultAnchors, IdentityMap<Conversation, View> nestedReplies) { LinkedSequence<FakeAnchor> anchorsUi = LinkedSequence.create(); for (ConversationThread reply : blip.getReplyThreads()) { anchorsUi.append((FakeAnchor) defaultAnchors.get(reply)); } LinkedSequence<FakeInlineConversationView> nestedUis = LinkedSequence.create(); // Order by conversation id. Ideally, the sort key would be creation // time, but that is not exposed in the conversation API. final List<Conversation> ordered = CollectionUtils.newArrayList(); nestedReplies.each(new ProcV<Conversation, View>() { @Override public void apply(Conversation conv, View ui) { ordered.add(conv); } }); Collections.sort(ordered, new Comparator<Conversation>() { @Override public int compare(Conversation o1, Conversation o2) { return o1.getId().compareTo(o2.getId()); } }); for (Conversation nested : ordered) { nestedUis.append((FakeInlineConversationView) nestedReplies.get(nested)); } FakeBlipView blipUi = views.createBlipView(blip, anchorsUi, nestedUis); blipUi.getMeta().setContent((FakeDocumentView) document); return blipUi; } @Override public FakeThreadView render( ConversationThread thread, IdentityMap<ConversationBlip, View> blips) { LinkedSequence<FakeBlipView> blipUis = LinkedSequence.create(); for (ConversationBlip blip : thread.getBlips()) { blipUis.append((FakeBlipView) blips.get(blip)); } if (thread.getConversation().getRootThread().equals(thread)) { return views.createRootThreadView(thread, blipUis); } else { return views.createInlineThreadView(thread, blipUis); } } @Override public FakeConversationView render(Conversation conversation, View participants, View thread) { if (!conversation.hasAnchor()) { return views.createTopConversationView(conversation, (FakeRootThreadView) thread); } else { return views.createInlineConversationView(conversation, (FakeRootThreadView) thread); } } @Override public View render(Conversation conversation, ParticipantId participant) { // Ignore participants; not yet exercised by tests. return null; } @Override public View render(Conversation conversation, StringMap<View> participants) { // Ignore participants; not yet exercised by tests. return null; } @Override public TopConversationView render( ConversationView wave, IdentityMap<Conversation, View> conversations) { // Pick the first one. return conversations.isEmpty() ? null : conversations.reduce(null, new Reduce<Conversation, View, TopConversationView>() { @Override public TopConversationView apply(TopConversationView soFar, Conversation key, View item) { return soFar != null ? soFar : (TopConversationView) item; } }); } @Override public FakeAnchor render(ConversationThread thread, View threadUi) { FakeAnchor anchor = views.createDefaultAnchorView(thread); anchor.attach((InlineThreadView) threadUi); return anchor; } } private final ViewStore views = new ViewStore(); private final WaveRenderer<View> renderer; private FakeRenderer(ConversationView wave) { this.renderer = ReductionBasedRenderer.of(new Rules(), wave); } /** * Creates a renderer of fake views. */ public static FakeRenderer create(ConversationView wave) { return new FakeRenderer(wave); } // TODO: Expose view store, so that fake views can remove themselves after // destruction, so that view lookups below do not report spurious results. // This code path is unique to this fake renderer, because in a DOM renderer, // cleanup occurs implicitly by virtue of lookups being based on // Document.getElementById(). public FakeAnchor createInlineAnchor(ConversationThread thread) { return views.createInlineAnchorView(thread); } // Delegate wave-rendering to the internal driver. @Override public View render(Conversation conversation, ParticipantId participant) { return renderer.render(conversation, participant); } @Override public View render(Conversation conversation) { return renderer.render(conversation); } @Override public View render(ConversationBlip blip) { return renderer.render(blip); } @Override public View render(ConversationThread thread) { return renderer.render(thread); } @Override public View render(ConversationView wave) { return renderer.render(wave); } // Delegate view lookup to view store. @Override public BlipView getBlipView(ConversationBlip blip) { return views.blipUis.get(blip); } @Override public InlineThreadView getInlineThreadView(ConversationThread thread) { return views.inlineThreadUis.get(thread); } @Override public RootThreadView getRootThreadView(ConversationThread thread) { return views.rootThreadUis.get(thread); } @Override public org.waveprotocol.wave.client.wavepanel.view.ConversationView getConversationView( Conversation conv) { return views.convUis.get(conv); } @Override public BlipMetaView getBlipMetaView(ConversationBlip blip) { BlipView blipUi = getBlipView(blip); return blipUi != null ? blipUi.getMeta() : null; } @Override public AnchorView getDefaultAnchor(ConversationThread thread) { return views.defaultAnchorUis.get(thread); } @Override public AnchorView getInlineAnchor(ConversationThread thread) { return views.inlineAnchorUis.get(thread); } @Override public ParticipantsView getParticipantsView(Conversation conv) { // Participant views not supported. return null; } @Override public ParticipantView getParticipantView(Conversation conv, ParticipantId source) { return null; } // Inverse lookup. @Override public ConversationBlip getBlip(BlipView blipUi) { return views.blipUis.inverse().get(blipUi); } @Override public ConversationThread getThread(ThreadView threadUi) { ConversationThread inline = views.inlineThreadUis.inverse().get(threadUi); return inline != null ? inline : views.rootThreadUis.inverse().get(threadUi); } @Override public Pair<Conversation, ParticipantId> getParticipant(ParticipantView participantUi) { return null; } @Override public Conversation getParticipants(ParticipantsView participantsUi) { return null; } }