/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.webclient.client;
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.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.OptionElement;
import com.google.gwt.dom.client.SelectElement;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.http.client.UrlBuilder;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.Location;
import com.google.gwt.user.client.ui.DockLayoutPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.SplitLayoutPanel;
import com.google.gwt.user.client.ui.UIObject;
import org.waveprotocol.box.webclient.client.i18n.WebClientMessages;
import org.waveprotocol.box.webclient.profile.RemoteProfileManagerImpl;
import org.waveprotocol.box.webclient.search.RemoteSearchService;
import org.waveprotocol.box.webclient.search.Search;
import org.waveprotocol.box.webclient.search.SearchPanelRenderer;
import org.waveprotocol.box.webclient.search.SearchPanelWidget;
import org.waveprotocol.box.webclient.search.SearchPresenter;
import org.waveprotocol.box.webclient.search.SimpleSearch;
import org.waveprotocol.box.webclient.search.WaveStore;
import org.waveprotocol.box.webclient.widget.error.ErrorIndicatorPresenter;
import org.waveprotocol.box.webclient.widget.frame.FramedPanel;
import org.waveprotocol.box.webclient.widget.loading.LoadingIndicator;
import org.waveprotocol.wave.client.account.ProfileManager;
import org.waveprotocol.wave.client.common.safehtml.SafeHtml;
import org.waveprotocol.wave.client.common.safehtml.SafeHtmlBuilder;
import org.waveprotocol.wave.client.common.util.AsyncHolder.Accessor;
import org.waveprotocol.wave.client.debug.logger.LogLevel;
import org.waveprotocol.wave.client.doodad.attachment.AttachmentManagerImpl;
import org.waveprotocol.wave.client.doodad.attachment.AttachmentManagerProvider;
import org.waveprotocol.wave.client.events.ClientEvents;
import org.waveprotocol.wave.client.events.Log;
import org.waveprotocol.wave.client.events.NetworkStatusEvent;
import org.waveprotocol.wave.client.events.NetworkStatusEventHandler;
import org.waveprotocol.wave.client.events.WaveCreationEvent;
import org.waveprotocol.wave.client.events.WaveCreationEventHandler;
import org.waveprotocol.wave.client.events.WaveSelectionEvent;
import org.waveprotocol.wave.client.events.WaveSelectionEventHandler;
import org.waveprotocol.wave.client.wavepanel.event.EventDispatcherPanel;
import org.waveprotocol.wave.client.wavepanel.event.WaveChangeHandler;
import org.waveprotocol.wave.client.wavepanel.event.FocusManager;
import org.waveprotocol.wave.client.widget.common.ImplPanel;
import org.waveprotocol.wave.client.widget.popup.CenterPopupPositioner;
import org.waveprotocol.wave.client.widget.popup.PopupChrome;
import org.waveprotocol.wave.client.widget.popup.PopupChromeFactory;
import org.waveprotocol.wave.client.widget.popup.PopupFactory;
import org.waveprotocol.wave.client.widget.popup.UniversalPopup;
import org.waveprotocol.wave.model.id.IdGenerator;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.waveref.InvalidWaveRefException;
import org.waveprotocol.wave.model.waveref.WaveRef;
import org.waveprotocol.wave.util.escapers.GwtWaverefEncoder;
import java.util.Date;
import java.util.Set;
import java.util.logging.Logger;
import org.waveprotocol.box.stat.Timing;
import org.waveprotocol.box.webclient.stat.SingleThreadedRequestScope;
import org.waveprotocol.box.webclient.stat.gwtevent.GwtStatisticsEventSystem;
import org.waveprotocol.box.webclient.stat.gwtevent.GwtStatisticsHandler;
/**
* Entry point classes define <code>onModuleLoad()</code>.
*/
public class WebClient implements EntryPoint {
interface Binder extends UiBinder<DockLayoutPanel, WebClient> {
}
interface Style extends CssResource {
}
private static final Binder BINDER = GWT.create(Binder.class);
private static final WebClientMessages messages = GWT.create(WebClientMessages.class);
static Log LOG = Log.get(WebClient.class);
// Use of GWT logging is only intended for sending exception reports to the
// server, nothing else in the client should use java.util.logging.
// Please also see WebClientDemo.gwt.xml.
private static final Logger REMOTE_LOG = Logger.getLogger("REMOTE_LOG");
private static final String DEFAULT_LOCALE = "default";
/** Creates a popup that warns about network disconnects. */
private static UniversalPopup createTurbulencePopup() {
PopupChrome chrome = PopupChromeFactory.createPopupChrome();
UniversalPopup popup =
PopupFactory.createPopup(null, new CenterPopupPositioner(), chrome, true);
popup.add(new HTML("<div style='color: red; padding: 5px; text-align: center;'>"
+ "<b>" + messages.turbulenceDetected() + "<br></br> "
+ messages.saveAndReloadWave() + "</b></div>"));
return popup;
}
private final ProfileManager profiles = new RemoteProfileManagerImpl();
private final UniversalPopup turbulencePopup = createTurbulencePopup();
@UiField
SplitLayoutPanel splitPanel;
@UiField
Style style;
@UiField
FramedPanel waveFrame;
@UiField
ImplPanel waveHolder;
private final Element loading = new LoadingIndicator().getElement();
@UiField(provided = true)
final SearchPanelWidget searchPanel = new SearchPanelWidget(new SearchPanelRenderer(profiles));
@UiField
DebugMessagePanel logPanel;
/** The wave panel, if a wave is open. */
private StagesProvider wave;
private final WaveStore waveStore = new SimpleWaveStore();
/**
* Create a remote websocket to talk to the server-side FedOne service.
*/
private WaveWebSocketClient websocket;
private ParticipantId loggedInUser;
private IdGenerator idGenerator;
private RemoteViewServiceMultiplexer channel;
private LocaleService localeService = new RemoteLocaleService();
/**
* This is the entry point method.
*/
@Override
public void onModuleLoad() {
ErrorHandler.install();
ClientEvents.get().addWaveCreationEventHandler(
new WaveCreationEventHandler() {
@Override
public void onCreateRequest(WaveCreationEvent event, Set<ParticipantId> participantSet) {
LOG.info("WaveCreationEvent received");
if (channel == null) {
throw new RuntimeException("Spaghetti attack. Create occured before login");
}
openWave(WaveRef.of(idGenerator.newWaveId()), true, participantSet);
}
});
setupLocaleSelect();
setupConnectionIndicator();
HistorySupport.init(new HistoryProviderDefault());
HistoryChangeListener.init();
websocket = new WaveWebSocketClient(websocketNotAvailable(), getWebSocketBaseUrl());
websocket.connect();
if (Session.get().isLoggedIn()) {
loggedInUser = new ParticipantId(Session.get().getAddress());
idGenerator = ClientIdGenerator.create();
loginToServer();
}
setupUi();
setupStatistics();
History.fireCurrentHistoryState();
LOG.info("SimpleWebClient.onModuleLoad() done");
}
private void setupUi() {
// Set up UI
DockLayoutPanel self = BINDER.createAndBindUi(this);
RootPanel.get("app").add(self);
// DockLayoutPanel forcibly conflicts with sensible layout control, and
// sticks inline styles on elements without permission. They must be
// cleared.
self.getElement().getStyle().clearPosition();
splitPanel.setWidgetMinSize(searchPanel, 300);
AttachmentManagerProvider.init(AttachmentManagerImpl.getInstance());
if (LogLevel.showDebug()) {
logPanel.enable();
} else {
logPanel.removeFromParent();
}
setupSearchPanel();
setupWavePanel();
FocusManager.init();
}
private void setupSearchPanel() {
// On wave action fire an event.
SearchPresenter.WaveActionHandler actionHandler =
new SearchPresenter.WaveActionHandler() {
@Override
public void onCreateWave() {
ClientEvents.get().fireEvent(new WaveCreationEvent());
}
@Override
public void onWaveSelected(WaveId id) {
ClientEvents.get().fireEvent(new WaveSelectionEvent(WaveRef.of(id)));
}
};
Search search = SimpleSearch.create(RemoteSearchService.create(), waveStore);
SearchPresenter.create(search, searchPanel, actionHandler, profiles);
}
private void setupWavePanel() {
// Hide the frame until waves start getting opened.
UIObject.setVisible(waveFrame.getElement(), false);
Document.get().getElementById("signout").setInnerText(messages.signout());
// Handles opening waves.
ClientEvents.get().addWaveSelectionEventHandler(new WaveSelectionEventHandler() {
@Override
public void onSelection(WaveRef waveRef) {
openWave(waveRef, false, null);
}
});
}
private void setupLocaleSelect() {
final SelectElement select = (SelectElement) Document.get().getElementById("lang");
String currentLocale = LocaleInfo.getCurrentLocale().getLocaleName();
String[] localeNames = LocaleInfo.getAvailableLocaleNames();
for (String locale : localeNames) {
if (!DEFAULT_LOCALE.equals(locale)) {
String displayName = LocaleInfo.getLocaleNativeDisplayName(locale);
OptionElement option = Document.get().createOptionElement();
option.setValue(locale);
option.setText(displayName);
select.add(option, null);
if (locale.equals(currentLocale)) {
select.setSelectedIndex(select.getLength() - 1);
}
}
}
EventDispatcherPanel.of(select).registerChangeHandler(null, new WaveChangeHandler() {
@Override
public boolean onChange(ChangeEvent event, Element context) {
UrlBuilder builder = Location.createUrlBuilder().setParameter(
"locale", select.getValue());
Window.Location.replace(builder.buildString());
localeService.storeLocale(select.getValue());
return true;
}
});
}
private void setupConnectionIndicator() {
ClientEvents.get().addNetworkStatusEventHandler(new NetworkStatusEventHandler() {
boolean isTurbulenceDetected = false;
@Override
public void onNetworkStatus(NetworkStatusEvent event) {
Element element = Document.get().getElementById("netstatus");
if (element != null) {
switch (event.getStatus()) {
case CONNECTED:
case RECONNECTED:
element.setInnerText(messages.online());
element.setClassName("online");
isTurbulenceDetected = false;
turbulencePopup.hide();
break;
case DISCONNECTED:
element.setInnerText(messages.offline());
element.setClassName("offline");
if (!isTurbulenceDetected) {
isTurbulenceDetected = true;
turbulencePopup.show();
}
break;
case RECONNECTING:
element.setInnerText(messages.connecting());
element.setClassName("connecting");
break;
}
}
}
});
}
private void setupStatistics() {
Timing.setScope(new SingleThreadedRequestScope());
Timing.setEnabled(true);
GwtStatisticsEventSystem eventSystem = new GwtStatisticsEventSystem();
eventSystem.addListener(new GwtStatisticsHandler(), true);
eventSystem.enable(true);
}
/**
* Returns <code>ws(s)://yourhost[:port]/</code>.
*/
// XXX check formatting wrt GPE
private native String getWebSocketBaseUrl() /*-{return ((window.location.protocol == "https:") ? "wss" : "ws") + "://" + $wnd.__websocket_address + "/";}-*/;
private native boolean websocketNotAvailable() /*-{ return !window.WebSocket }-*/;
/**
*/
private void loginToServer() {
assert loggedInUser != null;
channel = new RemoteViewServiceMultiplexer(websocket, loggedInUser.getAddress());
}
/**
* Shows a wave in a wave panel.
*
* @param waveRef wave id to open
* @param isNewWave whether the wave is being created by this client session.
* @param participants the participants to add to the newly created wave.
* {@code null} if only the creator should be added
*/
private void openWave(WaveRef waveRef, boolean isNewWave, Set<ParticipantId> participants) {
final org.waveprotocol.box.stat.Timer timer = Timing.startRequest("Open Wave");
LOG.info("WebClient.openWave()");
if (wave != null) {
wave.destroy();
wave = null;
}
// Release the display:none.
UIObject.setVisible(waveFrame.getElement(), true);
waveHolder.getElement().appendChild(loading);
Element holder = waveHolder.getElement().appendChild(Document.get().createDivElement());
Element unsavedIndicator = Document.get().getElementById("unsavedStateContainer");
StagesProvider wave =
new StagesProvider(holder, unsavedIndicator, waveHolder, waveFrame, waveRef, channel, idGenerator,
profiles, waveStore, isNewWave, Session.get().getDomain(), participants);
this.wave = wave;
wave.load(new Command() {
@Override
public void execute() {
loading.removeFromParent();
Timing.stop(timer);
}
});
String encodedToken = History.getToken();
if (encodedToken != null && !encodedToken.isEmpty()) {
WaveRef fromWaveRef;
try {
fromWaveRef = GwtWaverefEncoder.decodeWaveRefFromPath(encodedToken);
} catch (InvalidWaveRefException e) {
LOG.info("History token contains invalid path: " + encodedToken);
return;
}
if (fromWaveRef.getWaveId().equals(waveRef.getWaveId())) {
// History change was caused by clicking on a link, it's already
// updated by browser.
return;
}
}
History.newItem(GwtWaverefEncoder.encodeToUriPathSegment(waveRef), false);
}
/**
* An exception handler that reports exceptions using a <em>shiny banner</em>
* (an alert placed on the top of the screen). Once the stack trace is
* prepared, it is revealed in the banner via a link.
*/
static class ErrorHandler implements UncaughtExceptionHandler {
/** Next handler in the handler chain. */
private final UncaughtExceptionHandler next;
/**
* Indicates whether an error has already been reported (at most one error
* is ever reported by this handler).
*/
private boolean hasFired;
private ErrorHandler(UncaughtExceptionHandler next) {
this.next = next;
}
public static void install() {
GWT.setUncaughtExceptionHandler(new ErrorHandler(GWT.getUncaughtExceptionHandler()));
}
@Override
public void onUncaughtException(Throwable e) {
if (!hasFired) {
hasFired = true;
final ErrorIndicatorPresenter error =
ErrorIndicatorPresenter.create(RootPanel.get("banner"));
getStackTraceAsync(e, new Accessor<SafeHtml>() {
@Override
public void use(SafeHtml stack) {
error.addDetail(stack, null);
REMOTE_LOG.severe(stack.asString().replace("<br>", "\n"));
}
});
}
if (next != null) {
next.onUncaughtException(e);
}
}
private void getStackTraceAsync(final Throwable t, final Accessor<SafeHtml> whenReady) {
// TODO: Request stack-trace de-obfuscation. For now, just use the
// javascript stack trace.
//
// Use minimal services here, in order to avoid the chance that reporting
// the error produces more errors. In particular, do not use WIAB's
// scheduler to run this command.
// Also, this code could potentially be put behind a runAsync boundary, to
// save whatever dependencies it uses from the initial download.
new Timer() {
@Override
public void run() {
SafeHtmlBuilder stack = new SafeHtmlBuilder();
Throwable error = t;
while (error != null) {
String token = String.valueOf((new Date()).getTime());
stack.appendHtmlConstant("Token: " + token + "<br> ");
stack.appendEscaped(String.valueOf(error.getMessage())).appendHtmlConstant("<br>");
for (StackTraceElement elt : error.getStackTrace()) {
stack.appendHtmlConstant(" ")
.appendEscaped(maybe(elt.getClassName(), "??")).appendHtmlConstant(".") //
.appendEscaped(maybe(elt.getMethodName(), "??")).appendHtmlConstant(" (") //
.appendEscaped(maybe(elt.getFileName(), "??")).appendHtmlConstant(":") //
.appendEscaped(maybe(elt.getLineNumber(), "??")).appendHtmlConstant(")") //
.appendHtmlConstant("<br>");
}
error = error.getCause();
if (error != null) {
stack.appendHtmlConstant("Caused by: ");
}
}
whenReady.use(stack.toSafeHtml());
}
}.schedule(1);
}
private static String maybe(String value, String otherwise) {
return value != null ? value : otherwise;
}
private static String maybe(int value, String otherwise) {
return value != -1 ? String.valueOf(value) : otherwise;
}
}
}