/* * SoapUI, Copyright (C) 2004-2016 SmartBear Software * * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: * * http://ec.europa.eu/idabc/eupl * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the Licence for the specific language governing permissions and limitations * under the Licence. */ package com.eviware.soapui.support.components; import com.eviware.soapui.SoapUI; import com.eviware.soapui.impl.rest.actions.oauth.BrowserListener; import com.eviware.soapui.support.Tools; import com.eviware.soapui.support.xml.XmlUtils; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Worker; import javafx.embed.swing.JFXPanel; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.web.PopupFeatures; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.util.Callback; import netscape.javascript.JSObject; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.awt.BorderLayout; import java.awt.Component; import java.awt.DefaultKeyboardFocusManager; import java.awt.HeadlessException; import java.awt.KeyEventDispatcher; import java.awt.KeyboardFocusManager; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; class EnabledWebViewBasedBrowserComponent implements WebViewBasedBrowserComponent { public static final String CHARSET_PATTERN = "(.+)(;\\s*charset=)(.+)"; public static final String DEFAULT_ERROR_PAGE = "<html><body><h1>The page could not be loaded</h1></body></html>"; private Pattern charsetFinderPattern = Pattern.compile(CHARSET_PATTERN); private JPanel panel = new JPanel(new BorderLayout()); private String errorPage; private boolean showingErrorPage; public String url; private PropertyChangeSupport pcs = new PropertyChangeSupport(this); private java.util.List<BrowserListener> listeners = new ArrayList<BrowserListener>(); public WebView webView; private WebViewNavigationBar navigationBar; private String lastLocation; private Set<BrowserWindow> browserWindows = new HashSet<BrowserWindow>(); private JFXPanel browserPanel; private PopupStrategy popupStrategy; EnabledWebViewBasedBrowserComponent(boolean addNavigationBar, PopupStrategy popupStrategy) { this.popupStrategy = popupStrategy; initializeWebView(addNavigationBar); } @Override public Component getComponent() { return panel; } private void initializeWebView(boolean addNavigationBar) { if (addNavigationBar) { navigationBar = new WebViewNavigationBar(); panel.add(navigationBar.getComponent(), BorderLayout.NORTH); } final JFXPanel browserPanel = new JFXPanel(); panel.add(browserPanel, BorderLayout.CENTER); WebViewInitialization webViewInitialization = new WebViewInitialization(browserPanel); if (Platform.isFxApplicationThread()) { webViewInitialization.run(); } else { Platform.runLater(webViewInitialization); } Runnable runnable = new Runnable() { public void run() { if (navigationBar != null) { navigationBar.focusUrlField(); } } }; SwingUtilities.invokeLater(runnable); } private String readDocumentAsString() throws TransformerException { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); transformer.setOutputProperty(OutputKeys.METHOD, "xml"); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); StringWriter stringWriter = new StringWriter(); transformer.transform(new DOMSource(getWebEngine().getDocument()), new StreamResult(stringWriter)); return stringWriter.getBuffer().toString().replaceAll("\n|\r", ""); } private void addKeyboardFocusManager(final JFXPanel browserPanel) { KeyboardFocusManager kfm = DefaultKeyboardFocusManager.getCurrentKeyboardFocusManager(); kfm.addKeyEventDispatcher(new KeyEventDispatcher() { @Override public boolean dispatchKeyEvent(KeyEvent e) { if (DefaultKeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() == browserPanel) { if (e.getID() == KeyEvent.KEY_TYPED && e.getKeyChar() == 10) { e.setKeyChar((char) 13); } } return false; } } ); } @Override public void executeJavaScript(final String script) { Platform.runLater(new Runnable() { public void run() { try { webView.getEngine().executeScript(script); for (BrowserListener listener : listeners) { listener.javaScriptExecuted(script, null, null); } } catch (Exception e) { SoapUI.log.warn("Error executing JavaScript [" + script + "]", e); for (BrowserListener listener : listeners) { listener.javaScriptExecuted(script, lastLocation, e); } } } }); } @Override public void addJavaScriptEventHandler(final String memberName, final Object eventHandler) { Platform.runLater(new Runnable() { @Override public void run() { getWebEngine().getLoadWorker().stateProperty().addListener( new ChangeListener<Worker.State>() { public void changed(ObservableValue observableValue, Worker.State oldState, Worker.State newState) { if (newState == Worker.State.SUCCEEDED) { JSObject window = (JSObject) getWebEngine().executeScript("window"); window.setMember(memberName, eventHandler); } } } ); } }); } @Override public void close(boolean cascade) { if (cascade) { for (Iterator<BrowserWindow> iterator = browserWindows.iterator(); iterator.hasNext(); ) { iterator.next().close(); iterator.remove(); } } for (BrowserListener listener : listeners) { listener.browserClosed(); } release(); } private void release() { setContent(""); browserPanel.setScene(null); } @Override public void setContent(final String contentAsString, final String contentType) { if (SoapUI.isBrowserDisabled()) { return; } Platform.runLater(new Runnable() { public void run() { getWebEngine().loadContent(contentAsString, removeCharsetFrom(contentType)); } }); } private String removeCharsetFrom(String contentType) { Matcher matcher = charsetFinderPattern.matcher(contentType); return matcher.matches() ? matcher.group(1) : contentType; } @Override public void setContent(final String contentAsString) { if (SoapUI.isBrowserDisabled()) { return; } Platform.runLater(new Runnable() { public void run() { getWebEngine().loadContent(contentAsString); } }); pcs.firePropertyChange("content", null, contentAsString); } private WebEngine getWebEngine() { return webView.getEngine(); } public String getContent() { return webView == null ? null : XmlUtils.serialize(getWebEngine().getDocument()); } public String getUrl() { return url; } public void setErrorPage(String errorPage) { this.errorPage = errorPage; } public void addPropertyChangeListener(PropertyChangeListener pcl) { pcs.addPropertyChangeListener(pcl); } public void removePropertyChangeListener(PropertyChangeListener pcl) { pcs.removePropertyChangeListener(pcl); } @Override public void navigate(final String url) { navigate(url, DEFAULT_ERROR_PAGE); } public void navigate(final String url, String errorPage) { if (SoapUI.isBrowserDisabled()) { return; } setErrorPage(errorPage); this.url = url; Platform.runLater(new Runnable() { public void run() { getWebEngine().load(url); } }); } @Override public void addBrowserStateListener(BrowserListener listener) { listeners.add(listener); } @Override public void removeBrowserStateListener(BrowserListener listener) { listeners.remove(listener); } /* Class used to open a new browser window, used in the popup handler of the component. */ private class BrowserWindow extends JFrame { private final EnabledWebViewBasedBrowserComponent browser; private BrowserWindow(PopupFeatures popupFeatures) throws HeadlessException { super("Browser"); setIconImages(SoapUI.getFrameIcons()); browser = new EnabledWebViewBasedBrowserComponent(popupFeatures.hasToolbar(), popupStrategy); getContentPane().setLayout(new BorderLayout()); getContentPane().add(browser.getComponent()); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { browser.close(false); } }); } public void close() { setVisible(false); dispose(); browser.close(true); browser.release(); } } /* The task to initialize the Java FX WebView component. Will be run synchronously if we're already on the Java FX event thread, asynchronously if we aren't. */ private class WebViewInitialization implements Runnable { public WebViewInitialization(JFXPanel browserPanel) { EnabledWebViewBasedBrowserComponent.this.browserPanel = browserPanel; } public void run() { webView = new WebView(); createPopupHandler(); listenForLocationChanges(); listenForStateChanges(); if (navigationBar != null) { navigationBar.initialize(getWebEngine(), EnabledWebViewBasedBrowserComponent.this); } browserPanel.setScene(createJfxScene()); addKeyboardFocusManager(browserPanel); } private Scene createJfxScene() { Group jfxComponentGroup = new Group(); Scene scene = new Scene(jfxComponentGroup); webView.prefWidthProperty().bind(scene.widthProperty()); webView.prefHeightProperty().bind(scene.heightProperty()); jfxComponentGroup.getChildren().add(webView); return scene; } private void createPopupHandler() { switch (popupStrategy) { case INTERNAL_BROWSER_NEW_WINDOW: webView.getEngine().setCreatePopupHandler(new Callback<PopupFeatures, WebEngine>() { @Override public WebEngine call(PopupFeatures pf) { BrowserWindow popupWindow = new BrowserWindow(pf); browserWindows.add(popupWindow); popupWindow.setSize(800, 600); popupWindow.setVisible(true); return popupWindow.browser.getWebEngine(); } }); break; case EXTERNAL_BROWSER: webView.getEngine().setCreatePopupHandler(new Callback<PopupFeatures, WebEngine>() { @Override public WebEngine call(PopupFeatures pf) { final WebEngine webEngine = new WebEngine(); webEngine.locationProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observableValue, String oldValue, String newValue) { observableValue.removeListener(this); Platform.runLater(new Runnable() { @Override public void run() { webEngine.loadContent(""); } }); Tools.openURL(newValue); } }); return webEngine; } }); break; case DISABLED: webView.getEngine().setCreatePopupHandler(new Callback<PopupFeatures, WebEngine>() { @Override public WebEngine call(PopupFeatures pf) { return null; } }); break; case INTERNAL_BROWSER_REUSE_WINDOW: default: break; } } private void listenForLocationChanges() { webView.getEngine().locationProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observableValue, String oldLocation, String newLocation) { lastLocation = newLocation; for (BrowserListener listener : listeners) { listener.locationChanged(newLocation); } } }); } private void listenForStateChanges() { webView.getEngine().getLoadWorker().stateProperty().addListener( new ChangeListener<Worker.State>() { @Override public void changed(ObservableValue value, Worker.State oldState, Worker.State newState) { if (newState == Worker.State.SUCCEEDED) { try { if (getWebEngine().getDocument() != null) { String output = readDocumentAsString(); for (BrowserListener listener : listeners) { listener.contentChanged(output); } } } catch (Exception ex) { SoapUI.logError(ex, "Error processing state change to " + newState); } } else if (newState == Worker.State.FAILED && !showingErrorPage) { try { showingErrorPage = true; setContent(errorPage == null ? DEFAULT_ERROR_PAGE : errorPage); } finally { showingErrorPage = false; } } } } ); } } }