Java tutorial
/* * PlumberAPI.java * * Copyright (C) 2009-18 by RStudio, Inc. * * Unless you have received this program directly from RStudio pursuant * to the terms of a commercial license agreement with RStudio, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ package org.rstudio.studio.client.plumber; import org.rstudio.core.client.BrowseCap; import org.rstudio.core.client.Size; import org.rstudio.core.client.StringUtil; import org.rstudio.core.client.command.CommandBinder; import org.rstudio.core.client.command.Handler; import org.rstudio.core.client.dom.WindowCloseMonitor; import org.rstudio.core.client.dom.WindowEx; import org.rstudio.studio.client.application.ApplicationInterrupt; import org.rstudio.studio.client.application.Desktop; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.application.events.InterruptStatusEvent; import org.rstudio.studio.client.application.events.RestartStatusEvent; import org.rstudio.studio.client.common.GlobalDisplay; import org.rstudio.studio.client.common.dependencies.DependencyManager; import org.rstudio.studio.client.common.plumber.model.PlumberServerOperations; import org.rstudio.studio.client.common.satellite.SatelliteManager; import org.rstudio.studio.client.common.satellite.events.WindowClosedEvent; import org.rstudio.studio.client.plumber.events.LaunchPlumberAPIEvent; import org.rstudio.studio.client.plumber.events.PlumberAPIStatusEvent; import org.rstudio.studio.client.plumber.model.PlumberAPIParams; import org.rstudio.studio.client.plumber.model.PlumberRunCmd; import org.rstudio.studio.client.plumber.model.PlumberViewerType; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.server.ServerRequestCallback; import org.rstudio.studio.client.workbench.commands.Commands; import org.rstudio.studio.client.workbench.prefs.model.UIPrefs; import org.rstudio.studio.client.workbench.views.console.events.ConsoleBusyEvent; import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent; import org.rstudio.studio.client.workbench.views.environment.events.DebugModeChangedEvent; import org.rstudio.studio.client.workbench.views.viewer.events.ViewerClearedEvent; import com.google.gwt.core.client.JavaScriptObject; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @Singleton public class PlumberAPI implements PlumberAPIStatusEvent.Handler, ConsoleBusyEvent.Handler, DebugModeChangedEvent.Handler, RestartStatusEvent.Handler, WindowClosedEvent.Handler, LaunchPlumberAPIEvent.Handler, InterruptStatusEvent.Handler { public interface Binder extends CommandBinder<Commands, PlumberAPI> { } @Inject public PlumberAPI(EventBus eventBus, Commands commands, Binder binder, Provider<UIPrefs> pPrefs, final SatelliteManager satelliteManager, PlumberServerOperations server, GlobalDisplay display, DependencyManager dependencyManager, ApplicationInterrupt interrupt) { eventBus_ = eventBus; satelliteManager_ = satelliteManager; commands_ = commands; pPrefs_ = pPrefs; server_ = server; display_ = display; isBusy_ = false; currentViewType_ = PlumberViewerType.PLUMBER_VIEWER_NONE; dependencyManager_ = dependencyManager; interrupt_ = interrupt; eventBus_.addHandler(PlumberAPIStatusEvent.TYPE, this); eventBus_.addHandler(LaunchPlumberAPIEvent.TYPE, this); eventBus_.addHandler(ConsoleBusyEvent.TYPE, this); eventBus_.addHandler(DebugModeChangedEvent.TYPE, this); eventBus_.addHandler(RestartStatusEvent.TYPE, this); eventBus_.addHandler(WindowClosedEvent.TYPE, this); eventBus_.addHandler(InterruptStatusEvent.TYPE, this); binder.bind(commands, this); exportPlumberAPIClosedCallback(); } // Event handlers ---------------------------------------------------------- @Override public void onPlumberAPIStatus(PlumberAPIStatusEvent event) { if (StringUtil.equals(event.getParams().getState(), PlumberAPIParams.STATE_STARTED)) { currentViewType_ = event.getParams().getViewerType(); // open the window to view the API if needed if (currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_WINDOW) { activateWindow(event.getParams()); } else if (currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_BROWSER) { display_.openWindow(event.getParams().getUrl()); } params_ = event.getParams(); // if the API was started from the same path as a pending satellite // closure, don't shut down the API when the close finishes if (StringUtil.equals(event.getParams().getPath(), satelliteClosePath_)) { stopOnNextClose_ = false; } } else if (StringUtil.equals(event.getParams().getState(), PlumberAPIParams.STATE_STOPPED)) { params_ = null; } } @Override public void onLaunchPlumberAPI(LaunchPlumberAPIEvent event) { launchPlumberAPI(event.getPath()); } @Override public void onConsoleBusy(ConsoleBusyEvent event) { isBusy_ = event.isBusy(); // if the browser is up and R stops being busy, presume it's because the // API has stopped if (!isBusy_ && params_ != null && params_.getViewerType() == PlumberViewerType.PLUMBER_VIEWER_BROWSER) { params_.setState(PlumberAPIParams.STATE_STOPPED); eventBus_.fireEvent(new PlumberAPIStatusEvent(params_)); } } @Override public void onDebugModeChanged(DebugModeChangedEvent event) { // When leaving debug mode while the Plumber API is open in a // browser, automatically return to the API by activating the window. if (!event.debugging() && params_ != null && currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_WINDOW) { satelliteManager_.activateSatelliteWindow(PlumberAPISatellite.NAME); } } @Override public void onRestartStatus(RestartStatusEvent event) { // Close the satellite window when R restarts, since this leads to the // Plumber API being terminated. Closing the window triggers a // PlumberAPIStatusEvent that allows the rest of the UI a chance // to react to the API's termination. if (event.getStatus() == RestartStatusEvent.RESTART_INITIATED && currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_WINDOW) { satelliteManager_.closeSatelliteWindow(PlumberAPISatellite.NAME); } } @Override public void onWindowClosed(WindowClosedEvent event) { // we get this event on the desktop (currently only Cocoa); it lets us // know that the satellite has been shut down even in the case where the // script window that ordinarily would let us know has been disconnected. if (!StringUtil.equals(event.getName(), PlumberAPISatellite.NAME)) return; // stop the API if this event wasn't generated by a disconnect if (params_ != null && disconnectingUrl_ == null && stopOnNextClose_) { params_.setState(PlumberAPIParams.STATE_STOPPING); notifyPlumberAPIClosed(params_); } } @Handler public void onPlumberRunInPane() { setPlumberViewerType(PlumberViewerType.PLUMBER_VIEWER_PANE); } @Handler public void onPlumberRunInViewer() { setPlumberViewerType(PlumberViewerType.PLUMBER_VIEWER_WINDOW); } @Handler public void onPlumberRunInBrowser() { setPlumberViewerType(PlumberViewerType.PLUMBER_VIEWER_BROWSER); } @Override public void onInterruptStatus(InterruptStatusEvent event) { // If API is stopped via Console, ensure Satellite is closed if (currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_WINDOW) { satelliteManager_.closeSatelliteWindow(PlumberAPISatellite.NAME); } } private void launchPlumberAPI(final String filePath) { String fileDir = filePath.substring(0, filePath.lastIndexOf("/")); if (fileDir.equals(currentAppPath())) { // The API being launched is the one already running; open and reload the API. if (currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_WINDOW) { satelliteManager_.dispatchCommand(commands_.reloadPlumberAPI(), PlumberAPISatellite.NAME); activateWindow(); } else if (currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_PANE && commands_.viewerRefresh().isEnabled()) { commands_.viewerRefresh().execute(); } else if (currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_BROWSER) { eventBus_.fireEvent(new PlumberAPIStatusEvent(params_)); } return; } else if (params_ != null && isBusy_) { // There's another API running. Interrupt it and then start this one. interrupt_.interruptR(() -> launchPlumberFile(filePath)); } else { // Nothing else running, start this API. dependencyManager_.withRPlumber("Running Plumber API", () -> launchPlumberFile(filePath)); } } private String currentAppPath() { if (params_ != null) return params_.getPath(); return null; } private void notifyPlumberAPIDisconnected(JavaScriptObject params) { PlumberAPIParams apiState = params.cast(); if (params_ == null) return; // remember that this URL is disconnecting (so we don't interrupt R when // the window is torn down) disconnectingUrl_ = apiState.getUrl(); } private void notifyPlumberAPIClosed(final JavaScriptObject params) { // if we don't know that an API is running, ignore this event if (params_ == null) return; satelliteClosePath_ = params_.getPath(); // wait for confirmation of window closure (could be a reload) WindowCloseMonitor.monitorSatelliteClosure(PlumberAPISatellite.NAME, () -> { // satellite closed for real; shut down the API satelliteClosePath_ = null; onPlumberAPIClosed(params); }, () -> { // satellite didn't actually close (it was a reload) satelliteClosePath_ = null; }); } private void onPlumberAPIClosed(JavaScriptObject params) { PlumberAPIParams apiState = params.cast(); // this completes any pending disconnection disconnectingUrl_ = null; // if we were asked not to stop when the window closes (i.e. when // changing viewer types), bail out if (!stopOnNextClose_) { stopOnNextClose_ = true; return; } // If the API is stopping, then the user initiated the stop by // closing the API window. Interrupt R to stop the Plumber API. if (StringUtil.equals(apiState.getState(), PlumberAPIParams.STATE_STOPPING)) { if (commands_.interruptR().isEnabled()) commands_.interruptR().execute(); apiState.setState(PlumberAPIParams.STATE_STOPPED); } eventBus_.fireEvent(new PlumberAPIStatusEvent((PlumberAPIParams) params.cast())); } private final native void exportPlumberAPIClosedCallback()/*-{ var registry = this; $wnd.notifyPlumberAPIClosed = $entry( function(params) { registry.@org.rstudio.studio.client.plumber.PlumberAPI::notifyPlumberAPIClosed(Lcom/google/gwt/core/client/JavaScriptObject;)(params); } ); $wnd.notifyPlumberAPIDisconnected = $entry( function(params) { registry.@org.rstudio.studio.client.plumber.PlumberAPI::notifyPlumberAPIDisconnected(Lcom/google/gwt/core/client/JavaScriptObject;)(params); } ); }-*/; private void setPlumberViewerType(int viewerType) { UIPrefs prefs = pPrefs_.get(); prefs.plumberViewerType().setGlobalValue(viewerType); prefs.writeUIPrefs(); // if we have a running Plumber API and the viewer type has changed, // snap the API into the new location if (currentViewType_ != viewerType && params_ != null) { // if transitioning away from the pane or the window, close down // the old instance if (currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_PANE || currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_WINDOW) { if (currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_WINDOW) { stopOnNextClose_ = false; satelliteManager_.closeSatelliteWindow(PlumberAPISatellite.NAME); } else { eventBus_.fireEvent(new ViewerClearedEvent(false)); } } // assign new viewer type currentViewType_ = viewerType; params_.setViewerType(viewerType); if (currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_PANE || currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_WINDOW || currentViewType_ == PlumberViewerType.PLUMBER_VIEWER_BROWSER) { eventBus_.fireEvent(new PlumberAPIStatusEvent(params_)); } } } private void launchPlumberFile(String file) { server_.getPlumberRunCmd(file, new ServerRequestCallback<PlumberRunCmd>() { @Override public void onResponseReceived(PlumberRunCmd cmd) { eventBus_.fireEvent(new SendToConsoleEvent(cmd.getRunCmd(), true)); } @Override public void onError(ServerError error) { display_.showErrorMessage("Plumber API Launch Failed", error.getMessage()); } }); } private void activateWindow() { activateWindow(null); } private void activateWindow(PlumberAPIParams params) { WindowEx win = satelliteManager_.getSatelliteWindowObject(PlumberAPISatellite.NAME); boolean isRefresh = win != null && (params == null || (params_ != null && StringUtil.equals(params.getPath(), params_.getPath()))); boolean isChrome = !Desktop.isDesktop() && BrowseCap.isChrome(); if (params != null) params_ = params; if (win == null || (!isRefresh && !isChrome)) { int width = 910; // If there's no window yet, or we're switching APIs in a browser // other than Chrome, do a normal open satelliteManager_.openSatellite(PlumberAPISatellite.NAME, params_, new Size(width, 1100)); } else if (isChrome) { // we have a window and we're Chrome, so force a close and reopen satelliteManager_.forceReopenSatellite(PlumberAPISatellite.NAME, params_, true); } else { satelliteManager_.activateSatelliteWindow(PlumberAPISatellite.NAME); } } private final EventBus eventBus_; private final SatelliteManager satelliteManager_; private final DependencyManager dependencyManager_; private final Commands commands_; private final Provider<UIPrefs> pPrefs_; private final PlumberServerOperations server_; private final GlobalDisplay display_; private final ApplicationInterrupt interrupt_; private PlumberAPIParams params_; private String disconnectingUrl_; private boolean isBusy_; private boolean stopOnNextClose_ = true; private String satelliteClosePath_ = null; private int currentViewType_; }