/** * 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.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.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; 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.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.events.NetworkStatusEvent; import org.waveprotocol.box.webclient.client.events.NetworkStatusEventHandler; import org.waveprotocol.box.webclient.client.events.WaveCreationEvent; import org.waveprotocol.box.webclient.client.events.WaveCreationEventHandler; import org.waveprotocol.box.webclient.client.events.WaveSelectionEvent; import org.waveprotocol.box.webclient.client.events.WaveSelectionEventHandler; 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.util.Log; 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.common.util.UserAgent; import org.waveprotocol.wave.client.debug.logger.LogLevel; import org.waveprotocol.wave.client.scheduler.ScheduleCommand; import org.waveprotocol.wave.client.scheduler.Scheduler; 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.RelativePopupPositioner; 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.WaveRef; import java.util.Date; import java.util.logging.Logger; /** * 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); 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 UniversalPopup popup = createTurbulencePopup(); private static UniversalPopup createTurbulencePopup() { PopupChrome chrome = PopupChromeFactory.createPopupChrome(); UniversalPopup popup; if (UserAgent.isFirefox()) { popup = PopupFactory.createPopup(null, new RelativePopupPositioner() { @Override public void setPopupPositionAndMakeVisible(Element relative, final Element p) { ScheduleCommand.addCommand(new Scheduler.Task() { @Override public void execute() { p.getStyle().setLeft((RootPanel.get().getOffsetWidth() - p.getOffsetWidth()) / 2, Unit.PX); p.getStyle().setTop(100, Unit.PX); p.getStyle().setVisibility(Visibility.VISIBLE); } }); } }, chrome, true); } else { popup = PopupFactory.createPopup(null, new CenterPopupPositioner(), chrome, true); } popup.add(new HTML("<div style='color: red; padding: 5px; text-align: center;'><b>A turbulence detected!<br></br>" + " Please save your last changes to somewhere and reload the wave.</b></div>")); return popup; } private final ProfileManager profiles = new RemoteProfileManagerImpl(); @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; /** * This is the entry point method. */ @Override public void onModuleLoad() { ErrorHandler.install(); ClientEvents.get().addWaveCreationEventHandler( new WaveCreationEventHandler() { @Override public void onCreateRequest(WaveCreationEvent event) { LOG.info("WaveCreationEvent received"); if (channel == null) { throw new RuntimeException("Spaghetti attack. Create occured before login"); } openWave(WaveRef.of(idGenerator.newWaveId()), true); } }); setupConnectionIndicator(); HistorySupport.init(); websocket = new WaveWebSocketClient(useSocketIO(), getWebSocketBaseUrl(GWT.getModuleBaseURL())); websocket.connect(); if (Session.get().isLoggedIn()) { loggedInUser = new ParticipantId(Session.get().getAddress()); idGenerator = ClientIdGenerator.create(); loginToServer(); } setupUi(); 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, 330); if (LogLevel.showDebug()) { logPanel.enable(); } else { logPanel.removeFromParent(); } setupSearchPanel(); setupWavePanel(); } private void setupSearchPanel() { // On wave selection, fire an event. SearchPresenter.WaveSelectionHandler selectHandler = new SearchPresenter.WaveSelectionHandler() { @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, selectHandler, profiles); } private void setupWavePanel() { // Hide the frame until waves start getting opened. UIObject.setVisible(waveFrame.getElement(), false); // Handles opening waves. ClientEvents.get().addWaveSelectionEventHandler(new WaveSelectionEventHandler() { @Override public void onSelection(WaveRef waveRef) { openWave(waveRef, false); } }); } 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("Online"); element.setClassName("online"); isTurbulenceDetected = false; break; case DISCONNECTED: element.setInnerText("Offline"); element.setClassName("offline"); if (!isTurbulenceDetected) { isTurbulenceDetected = true; popup.show(); } break; case RECONNECTING: element.setInnerText("Connecting..."); element.setClassName("connecting"); break; } } } }); } /** * Returns <code>ws://yourhost[:port]/</code>. */ // XXX check formatting wrt GPE private native String getWebSocketBaseUrl(String moduleBase) /*-{return "ws" + /:\/\/[^\/]+/.exec(moduleBase)[0] + "/";}-*/; private native boolean useSocketIO() /*-{ return !!$wnd.__useSocketIO }-*/; /** */ 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. */ private void openWave(WaveRef waveRef, boolean isNewWave) { 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()); StagesProvider wave = new StagesProvider( holder, waveHolder, waveRef, channel, idGenerator, profiles, waveStore, isNewWave, Session.get().getDomain()); this.wave = wave; wave.load(new Command() { @Override public void execute() { loading.removeFromParent(); } }); String encodedToken = History.getToken(); if (encodedToken != null && !encodedToken.isEmpty()) { WaveRef fromWaveRef = HistorySupport.waveRefFromHistoryToken(encodedToken); if (waveRef == null) { 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(HistorySupport.historyTokenFromWaveref(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; } } }