/**
* 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.box.server.rpc.render;
import com.google.wave.api.Blip;
import com.google.wave.api.BlipData;
import com.google.wave.api.BlipThread;
import com.google.wave.api.InvalidRequestException;
import com.google.wave.api.OperationRequest;
import com.google.wave.api.ParticipantProfile;
import com.google.wave.api.data.converter.ContextResolver;
import com.google.wave.api.data.converter.EventDataConverter;
import com.google.wave.api.impl.EventMessageBundle;
import com.google.wave.api.impl.WaveletData;
import org.waveprotocol.box.server.robots.OperationContext;
import org.waveprotocol.box.server.robots.operations.GravatarProfileFetcher;
import org.waveprotocol.box.server.robots.operations.OperationService;
import org.waveprotocol.box.server.rpc.render.FullHtmlWaveRenderer.DocRefRenderer;
import org.waveprotocol.box.server.rpc.render.account.impl.ProfileImpl;
import org.waveprotocol.box.server.rpc.render.account.impl.ProfileManagerImpl;
import org.waveprotocol.box.server.rpc.render.common.safehtml.EscapeUtils;
import org.waveprotocol.box.server.rpc.render.uibuilder.UiBuilder;
import org.waveprotocol.box.server.rpc.render.view.ModelIdMapperImpl;
import org.waveprotocol.box.server.rpc.render.view.ViewFactory;
import org.waveprotocol.box.server.rpc.render.view.ViewIdMapper;
import org.waveprotocol.box.server.rpc.render.view.builder.BlipViewBuilder;
import org.waveprotocol.box.server.rpc.render.view.builder.CollapsibleBuilder;
import org.waveprotocol.box.server.rpc.render.view.builder.ContinuationIndicatorViewBuilder;
import org.waveprotocol.box.server.rpc.render.view.builder.FlowConversationViewBuilder;
import org.waveprotocol.box.server.rpc.render.view.builder.InlineConversationViewBuilder;
import org.waveprotocol.box.server.rpc.render.view.builder.ParticipantsViewBuilder;
import org.waveprotocol.box.server.rpc.render.view.builder.ReplyBoxViewBuilder;
import org.waveprotocol.box.server.rpc.render.view.builder.RootThreadViewBuilder;
import org.waveprotocol.box.server.rpc.render.view.builder.TopConversationViewBuilder;
import org.waveprotocol.box.server.rpc.render.view.builder.WavePanelResources;
import org.waveprotocol.box.server.rpc.render.web.text.ContentRenderer;
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.ObservableConversation;
import org.waveprotocol.wave.model.conversation.ObservableConversationView;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.supplement.ReadableSupplementedWave;
import org.waveprotocol.wave.model.supplement.SimpleWantedEvaluationSet;
import org.waveprotocol.wave.model.supplement.ThreadState;
import org.waveprotocol.wave.model.supplement.WantedEvaluation;
import org.waveprotocol.wave.model.supplement.WantedEvaluationSet;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.IdentityMap;
import org.waveprotocol.wave.model.util.ReadableStringMap;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.Wavelet;
import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet;
import org.waveprotocol.wave.model.waveref.WaveRef;
import org.waveprotocol.wave.util.escapers.jvm.JavaWaverefEncoder;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* {@link OperationService} for the "fetchWave" operation.
*
* @author yurize@apache.org (Yuri Zelikov)
*/
public class RenderWaveService implements OperationService {
private static final WavePanelResources RESOURCES = new WavePanelResources() {
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.RootThreadViewBuilder.Resources getRootThread() {
return new org.waveprotocol.box.server.rpc.render.view.builder.RootThreadViewBuilder.Resources() {
@Override
public RootThreadViewBuilder.Css css() {
return makeCssProxy(RootThreadViewBuilder.Css.class);
}
};
}
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.ReplyBoxViewBuilder.Resources getReplyBox() {
return new org.waveprotocol.box.server.rpc.render.view.builder.ReplyBoxViewBuilder.Resources() {
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.ReplyBoxViewBuilder.Css css() {
return makeCssProxy(ReplyBoxViewBuilder.Css.class);
}
};
}
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.ParticipantsViewBuilder.Resources getParticipants() {
return new org.waveprotocol.box.server.rpc.render.view.builder.ParticipantsViewBuilder.Resources() {
@Override
public WaveImageResource expandButton() {
return null;
}
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.ParticipantsViewBuilder.Css css() {
return makeCssProxy(ParticipantsViewBuilder.Css.class);
}
@Override
public WaveImageResource collapseButton() {
return null;
}
@Override
public WaveImageResource addButton() {
return null;
}
};
}
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.TopConversationViewBuilder.Resources getConversation() {
return new org.waveprotocol.box.server.rpc.render.view.builder.TopConversationViewBuilder.Resources() {
@Override
public WaveImageResource emptyToolbar() {
return null;
}
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.TopConversationViewBuilder.Css css() {
return makeCssProxy(TopConversationViewBuilder.Css.class);
}
};
}
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.ContinuationIndicatorViewBuilder.Resources getContinuationIndicator() {
return new org.waveprotocol.box.server.rpc.render.view.builder.ContinuationIndicatorViewBuilder.Resources() {
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.ContinuationIndicatorViewBuilder.Css css() {
return makeCssProxy(ContinuationIndicatorViewBuilder.Css.class);
}
@Override
public WaveImageResource continuationIcon() {
return null;
}
};
}
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.CollapsibleBuilder.Resources getCollapsible() {
return new org.waveprotocol.box.server.rpc.render.view.builder.CollapsibleBuilder.Resources() {
@Override
public WaveImageResource expandedUnread() {
return null;
}
@Override
public WaveImageResource expandedRead() {
return null;
}
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.CollapsibleBuilder.Css css() {
return makeCssProxy(CollapsibleBuilder.Css.class);
}
@Override
public WaveImageResource collapsedUnread() {
return null;
}
@Override
public WaveImageResource collapsedRead() {
return null;
}
@Override
public WaveImageResource callout() {
return null;
}
};
}
@Override
public BlipViewBuilder.Resources getBlip() {
return new org.waveprotocol.box.server.rpc.render.view.builder.BlipViewBuilder.Resources() {
@Override
public org.waveprotocol.box.server.rpc.render.view.builder.BlipViewBuilder.Css css() {
return makeCssProxy(BlipViewBuilder.Css.class);
}
};
}
};
public static ReadableSupplementedWave EMPTY_SUPPLEMENTED_WAVE = new ReadableSupplementedWave() {
@Override
public boolean isUnread(ConversationBlip blip) {
return false;
}
@Override
public boolean isTrashed() {
return false;
}
@Override
public boolean isTagsUnread(Wavelet wavelet) {
return false;
}
@Override
public boolean isParticipantsUnread(Wavelet wavelet) {
return false;
}
@Override
public boolean isMute() {
return false;
}
@Override
public boolean isInbox() {
return false;
}
@Override
public boolean isFollowed() {
return false;
}
@Override
public boolean isArchived() {
return false;
}
@Override
public boolean haveParticipantsEverBeenRead(Wavelet wavelet) {
return false;
}
@Override
public boolean hasPendingNotification() {
return false;
}
@Override
public boolean hasBeenSeen() {
return false;
}
@Override
public WantedEvaluationSet getWantedEvaluationSet(Wavelet wavelet) {
return new SimpleWantedEvaluationSet(wavelet.getId(), (Collection<WantedEvaluation>) null);
}
@Override
public ThreadState getThreadState(ConversationThread thread) {
return ThreadState.EXPANDED;
}
@Override
public HashedVersion getSeenVersion(WaveletId id) {
return HashedVersion.unsigned(0);
}
@Override
public String getGadgetStateValue(String gadgetId, String key) {
return "";
}
@Override
public ReadableStringMap<String> getGadgetState(String gadgetId) {
return CollectionUtils.emptyMap();
}
@Override
public Set<Integer> getFolders() {
return Collections.emptySet();
}
};
/**
* A ViewFactory that creates views suitable for embedding in a fixed-height
* context.
*/
public static final ViewFactory FIXED = new ViewFactory() {
@Override
public TopConversationViewBuilder createTopConversationView(String id, UiBuilder threadUi,
UiBuilder participantsUi) {
return FlowConversationViewBuilder.createRoot(RESOURCES, id, threadUi, participantsUi);
}
@Override
public final InlineConversationViewBuilder createInlineConversationView(String id,
UiBuilder threadUi, UiBuilder participantsUi) {
return InlineConversationViewBuilder.create(RESOURCES, id, participantsUi, threadUi);
}
};
public static final DocRefRenderer HTML_DOC_RENDERER = new DocRefRenderer() {
@Override
public UiBuilder render(ConversationBlip blip,
IdentityMap<ConversationThread, UiBuilder> replies) {
return UiBuilder.Constant.of(EscapeUtils.fromSafeConstant("[" + blip.getId() + "]"));
}
};
@SuppressWarnings("unchecked")
private static <T> T makeCssProxy(Class<T> clazz) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[] {clazz},
new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
return method.getName();
}
});
}
private RenderWaveService() {
}
public String exec(WaveId waveId, WaveletId waveletId, String blipId, ParticipantId participant,
OperationContext context) throws InvalidRequestException {
OpBasedWavelet opBasedWavelet = null;
opBasedWavelet = context.openWavelet(waveId, waveletId, participant);
ObservableConversationView conversationView =
context.getConversationUtil().buildConversation(opBasedWavelet);
ProfileManagerImpl profileManagerImpl = new ProfileManagerImpl() {
GravatarProfileFetcher profileFetcher = GravatarProfileFetcher.create();
@Override
public ProfileImpl getProfile(ParticipantId participantId) {
ParticipantProfile participantProfile =
profileFetcher.fetchProfile(participantId.getAddress());
ProfileImpl profile = new ProfileImpl(null, participantId);
profile.update(participantProfile.getName(), participantProfile.getName(),
participantProfile.getImageUrl());
return profile;
}
};
HtmlThreadReadStateMonitorImpl readStateMonitor =
HtmlThreadReadStateMonitorImpl.create(
ServiceUtil.buildSupplement(waveId, waveletId, context, participant), conversationView);
String path = JavaWaverefEncoder.encodeToUriPathSegment(WaveRef.of(waveId, waveletId));
HtmlRenderer renderer =
FullHtmlWaveRendererImpl.create(conversationView, profileManagerImpl,
new HtmlShallowBlipRenderer(profileManagerImpl, EMPTY_SUPPLEMENTED_WAVE),
new ViewIdMapper(ModelIdMapperImpl.create(conversationView, "UC")), readStateMonitor,
FIXED, HTML_DOC_RENDERER, RESOURCES, "/#" + path);
String html = null;
ObservableConversation conversation = conversationView.getRoot();
if (blipId == null) {
html = renderer.render(conversationView);
} else {
ConversationBlip blip = conversation.getBlip(blipId);
html = renderer.render(blip);
}
EventMessageBundle messages =
mapWaveletToMessageBundle(context.getConverter(), participant, opBasedWavelet, conversation);
Map<String, Blip> blips = new HashMap<String, Blip>();
Map<String, BlipThread> threads = new HashMap<String, BlipThread>();
WaveletData waveletData = context.getConverter().toWaveletData(opBasedWavelet, conversation, messages);
com.google.wave.api.Wavelet wavelet = com.google.wave.api.Wavelet.deserialize(null, blips, threads, waveletData);
threads.putAll(messages.getThreads());
for (Map.Entry<String, BlipData> entry : messages.getBlipData().entrySet()) {
BlipData blipData = context.getConverter().toBlipData(conversation.getBlip(entry.getKey()), opBasedWavelet, messages);
Blip tempBlip = Blip.deserialize(null, wavelet, blipData);
blips.put(tempBlip.getBlipId(), tempBlip);
}
ContentRenderer contentRenderer = new ContentRenderer();
for (Map.Entry<java.lang.String,com.google.wave.api.Blip> entry : wavelet.getBlips().entrySet()) {
com.google.wave.api.Blip blip = entry.getValue();
String blipHtml = contentRenderer.renderHtml(blip.getContent(), blip.getAnnotations(), blip.getElements(), blip.getContributors());
// TODO (Yuri Z.) Make it more efficient and safe.
String htmlId = "[" + blip.getBlipId() + "]";
html = html.replace(htmlId, blipHtml);
}
return html;
}
@Override
public void execute(OperationRequest operation, OperationContext context,
ParticipantId participant) throws InvalidRequestException {
// OpBasedWavelet wavelet = null;
// try {
// wavelet = context.openWavelet(operation, participant);
// } catch (InvalidRequestException e) {
// context.constructErrorResponse(operation, e.getMessage());
// return;
// }
// String blipId = OperationUtil.getOptionalParameter(operation, ParamsProperty.BLIP_ID);
// ObservableConversationView conversationView =
// context.getConversationUtil().buildConversation(wavelet);
// ProfileManagerImpl profileManagerImpl = new ProfileManagerImpl();
// HtmlThreadReadStateMonitorImpl readStateMonitor =
// HtmlThreadReadStateMonitorImpl.create(ServiceUtil.buildSupplement(operation, context, participant), conversationView);
// HtmlRenderer renderer =
// FullHtmlWaveRendererImpl.create(conversationView, profileManagerImpl,
// new HtmlShallowBlipRenderer(profileManagerImpl, EMPTY_SUPPLEMENTED_WAVE),
// new ViewIdMapper(ModelIdMapperImpl.create(conversationView, "UC")), null,
// readStateMonitor, FIXED, HTML_DOC_RENDERER, RESOURCES);
//
// String html = null;
// if (blipId == null) {
// html = renderer.render(conversationView);
// } else {
// ObservableConversation conversation =
// context.openConversation(operation, participant).getRoot();
// ConversationBlip blip = conversation.getBlip(blipId);
// html = renderer.render(blip);
// }
//
// Map<ParamsProperty, Object> data =
// ImmutableMap.<ParamsProperty, Object> of(ParamsProperty.RENDER_RESULT, html);
// context.constructResponse(operation, data);
}
/**
* Maps a wavelet and its conversation to a new {@link EventMessageBundle}.
*
* @param converter to convert to API objects.
* @param participant the participant who the bundle is for.
* @param wavelet the wavelet to put in the bundle.
* @param conversation the conversation to put in the bundle.
*/
private EventMessageBundle mapWaveletToMessageBundle(EventDataConverter converter,
ParticipantId participant, Wavelet wavelet, Conversation conversation) {
EventMessageBundle messages = new EventMessageBundle(participant.getAddress(), "");
WaveletData waveletData = converter.toWaveletData(wavelet, conversation, messages);
messages.setWaveletData(waveletData);
ContextResolver.addAllBlipsToEventMessages(messages, conversation, wavelet, converter);
return messages;
}
public static RenderWaveService create() {
return new RenderWaveService();
}
}