/**
* 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.box.server.rpc.render.renderer;
import com.google.common.base.Preconditions;
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.Reduce;
import org.waveprotocol.wave.model.util.StringMap;
import org.waveprotocol.wave.model.wave.ParticipantId;
import java.util.EnumSet;
import java.util.Stack;
/**
* A renderer helper that uses {@link RenderingRules production rules} to
* perform reductions, resulting in a rendering.
*
*/
public final class ReducingRendererHelper<R> implements org.waveprotocol.box.server.rpc.render.renderer.ResultProducingRenderHelper<R> {
enum Type {
BLIP, THREAD, CONVERSATION, WAVE
}
/**
* A rendering scope. It contains a map of the objects that have been rendered
* in this scope.
*/
private final static class Scope<R> {
private final EnumSet<Type> expected;
private IdentityMap<ConversationBlip, R> blips;
private IdentityMap<ConversationThread, R> threads;
private IdentityMap<Conversation, R> conversations;
private IdentityMap<ConversationView, R> waves;
public Scope(EnumSet<Type> expected) {
this.expected = expected;
}
private void checkScopeExpects(Type t) {
if (!expected.contains(t)) {
throw new IllegalStateException("Encountered a " + t + " in a scope that only expected: "
+ expected);
}
}
/** Adds a rendering of blip to this scope. */
void add(ConversationBlip blip, R rendering) {
checkScopeExpects(Type.BLIP);
if (blips == null) {
blips = CollectionUtils.createIdentityMap();
}
blips.put(blip, rendering);
}
/** Adds a rendering of blip to this scope. */
void add(ConversationThread thread, R rendering) {
checkScopeExpects(Type.THREAD);
if (threads == null) {
threads = CollectionUtils.createIdentityMap();
}
threads.put(thread, rendering);
}
/** Adds a rendering of conversation to this scope. */
void add(Conversation conversation, R rendering) {
checkScopeExpects(Type.CONVERSATION);
if (conversations == null) {
conversations = CollectionUtils.createIdentityMap();
}
conversations.put(conversation, rendering);
}
/** Adds a rendering of wave to this scope. */
void add(ConversationView wave, R rendering) {
checkScopeExpects(Type.WAVE);
if (waves == null) {
waves = CollectionUtils.createIdentityMap();
}
waves.put(wave, rendering);
}
IdentityMap<ConversationBlip, R> getBlips() {
return blips != null ? blips : CollectionUtils.<ConversationBlip, R> emptyIdentityMap();
}
IdentityMap<ConversationThread, R> getThreads() {
return threads != null ? threads : CollectionUtils.<ConversationThread, R> emptyIdentityMap();
}
IdentityMap<Conversation, R> getConversations() {
return conversations != null ? conversations // \u2620
: CollectionUtils.<Conversation, R> emptyIdentityMap();
}
IdentityMap<ConversationView, R> getWaves() {
return waves != null ? waves : CollectionUtils.<ConversationView, R> emptyIdentityMap();
}
}
/** Scope stack. */
private final Stack<Scope<R>> scopes = new Stack<Scope<R>>();
/** Production rules. */
private final RenderingRules<R> builders;
private Scope<R> result;
private ReducingRendererHelper(RenderingRules<R> builders) {
this.builders = builders;
}
/**
* Creates a rendering builder, driven by a {@link ConversationRenderer}, that
* drives a set of production rules.
*
* @param builders implementation of production rules
*/
public static <R> ReducingRendererHelper<R> of(RenderingRules<R> builders) {
return new ReducingRendererHelper<R>(builders);
}
/**
* Creates a rendering builder, driven by a {@link ConversationRenderer}, that
* drives a set of production rules.
*
* @param builders implementation of production rules
*/
public static <R> ReducingRendererHelper<R> ofStarted(RenderingRules<R> builders) {
ReducingRendererHelper<R> helper = of(builders);
return helper;
}
/**
* Starts a rendering.
*/
@Override
public void begin() {
// Accept anything.
enter(EnumSet.allOf(Type.class));
result = null;
}
@Override
public void end() {
result = leave();
Preconditions.checkState(scopes.isEmpty());
}
@Override
public R getResult() {
Reduce<Object, R, R> aggregator = new Reduce<Object, R, R>() {
@Override
public R apply(R soFar, Object key, R item) {
if (soFar != null) {
throw new IllegalStateException("scope contains multiple renderings");
} else {
return item;
}
}
};
// Thread aggregator through the rendering maps, ensuring there is at most
// one rendering in any of them.
R rendering = null;
rendering = result.getBlips().reduce(rendering, aggregator);
rendering = result.getThreads().reduce(rendering, aggregator);
rendering = result.getConversations().reduce(rendering, aggregator);
rendering = result.getWaves().reduce(rendering, aggregator);
return rendering;
}
/**
* Enters a new rendering scope.
*/
private void enter(EnumSet<Type> expected) {
scopes.push(new Scope<R>(expected));
}
/**
* Leaves a rendering scope.
*
* @return the renderings of objects that were rendered in this scope.
*/
private Scope<R> leave() {
return scopes.pop();
}
/**
* Queries for the current scope.
*/
private Scope<R> currentScope() {
return scopes.peek();
}
//
// The pattern of these methods is as follows:
// * start() methods just enter() a new scope.
// * end() methods leave() a scope, then (optionally) add a rendering of the
// object whose scope has just been left.
//
@Override
public void startView(ConversationView wave) {
enter(EnumSet.of(Type.CONVERSATION));
}
@Override
public void endView(ConversationView wave) {
IdentityMap<Conversation, R> convs = leave().getConversations();
currentScope().add(wave, builders.render(wave, convs));
}
@Override
public void startConversation(Conversation conv) {
enter(EnumSet.of(Type.THREAD));
}
@Override
public void endConversation(Conversation conv) {
StringMap<R> allParticipants = CollectionUtils.createStringMap();
for (ParticipantId participant : conv.getParticipantIds()) {
allParticipants.put(participant.getAddress(), builders.render(conv, participant));
}
R participants = builders.render(conv, allParticipants);
R rootThread = leave().getThreads().get(conv.getRootThread());
currentScope().add(conv, builders.render(conv, participants, rootThread));
}
@Override
public void startBlip(final ConversationBlip blip) {
enter(EnumSet.of(Type.THREAD, Type.CONVERSATION));
}
@Override
public void endBlip(ConversationBlip blip) {
Scope<R> scope = leave();
IdentityMap<ConversationThread, R> threads = scope.getThreads();
IdentityMap<Conversation, R> nestedConversations = scope.getConversations();
R document = builders.render(blip, threads);
// Replace thread renderings with anchor renderings.
IdentityMap<ConversationThread, R> defaultAnchors = CollectionUtils.createIdentityMap();
for (ConversationThread reply : blip.getReplyThreads()) {
defaultAnchors.put(reply, builders.render(reply, threads.get(reply)));
}
currentScope().add(blip, builders.render(blip, document, defaultAnchors, nestedConversations));
}
@Override
public void startThread(ConversationThread thread) {
enter(EnumSet.of(Type.BLIP));
}
@Override
public void endThread(ConversationThread thread) {
IdentityMap<ConversationBlip, R> blips = leave().getBlips();
// Only include thread rendering if there was some component rendering.
currentScope().add(thread, builders.render(thread, blips));
}
@Override
public void startInlineThread(ConversationThread thread) {
// Pretend it's a regular thread.
startThread(thread);
}
@Override
public void endInlineThread(ConversationThread thread) {
// Pretend it's a regular thread.
endThread(thread);
}
}