/*
* 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.editor.inspectors.auth;
import com.eviware.soapui.impl.rest.OAuth2Profile;
import com.eviware.soapui.impl.rest.actions.oauth.BrowserListenerAdapter;
import com.eviware.soapui.impl.rest.actions.oauth.JavaScriptValidationError;
import com.eviware.soapui.impl.rest.actions.oauth.JavaScriptValidator;
import com.eviware.soapui.impl.rest.actions.oauth.OAuth2Parameters;
import com.eviware.soapui.impl.rest.actions.oauth.OAuth2TokenExtractor;
import com.eviware.soapui.impl.support.actions.ShowOnlineHelpAction;
import com.eviware.soapui.impl.wsdl.support.HelpUrls;
import com.eviware.soapui.support.DocumentListenerAdapter;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.components.JXToolBar;
import com.eviware.soapui.support.xml.SyntaxEditorUtil;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
/**
* Component that allows a user to edit the JavaScript snippets associated with an OAuth 2 flow.
*/
public class OAuth2ScriptsEditor extends JPanel {
static final String TEST_SCRIPTS_BUTTON_NAME = "testScriptsButton";
static final String ADD_SCRIPT_BUTTON_NAME = "addScriptButton";
static final String REMOVE_SCRIPT_BUTTON_NAME = "removeScriptButton";
static final String[] DEFAULT_SCRIPT_NAMES = {"Page 1 (e.g. login screen)", "Page 2 (e.g. consent screen)"};
private static final String HELP_LINK_TEXT = "How to automate the process of getting an access token";
private List<InputPanel> inputPanels = new ArrayList<InputPanel>();
private InputPanel selectedInputField = null;
private List<RSyntaxTextArea> scriptFields = new ArrayList<RSyntaxTextArea>();
private JavaScriptValidator javaScriptValidator = new JavaScriptValidator();
private JPanel scriptsPanel;
private JButton removeScriptButton;
private OAuth2Profile profile;
private DocumentListener scriptUpdater;
public OAuth2ScriptsEditor(final OAuth2Profile profile) {
super(new BorderLayout());
this.profile = profile;
add(buildToolbar(profile), BorderLayout.NORTH);
makeScriptsPanel(profile);
add(new JScrollPane(scriptsPanel), BorderLayout.CENTER);
JPanel linkPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
linkPanel.add(UISupport.createLabelLink(HelpUrls.OAUTH_AUTOMATING_ACCESS_TOKEN_RETRIEVAL, HELP_LINK_TEXT));
add(linkPanel, BorderLayout.SOUTH);
}
private JXToolBar buildToolbar(final OAuth2Profile profile) {
JXToolBar toolbar = UISupport.createToolbar();
JButton testScriptsButton = UISupport.createToolbarButton(new TestScriptsAction(profile));
testScriptsButton.setName(TEST_SCRIPTS_BUTTON_NAME);
toolbar.addFixed(testScriptsButton);
JButton addScriptButton = UISupport.createToolbarButton(new AddScriptAction());
addScriptButton.setName(ADD_SCRIPT_BUTTON_NAME);
toolbar.addFixed(addScriptButton);
removeScriptButton = UISupport.createToolbarButton(new RemoveScriptAction());
removeScriptButton.setName(REMOVE_SCRIPT_BUTTON_NAME);
toolbar.addFixed(removeScriptButton);
toolbar.addGlue();
toolbar.add(UISupport.createToolbarButton(new ShowOnlineHelpAction(HelpUrls.OAUTH_AUTOMATED_TOKEN_PROFILE_EDITOR)));
return toolbar;
}
public List<String> getJavaScripts() {
List<String> scripts = new ArrayList<String>();
for (RSyntaxTextArea scriptField : scriptFields) {
scripts.add(scriptField.getText());
}
return scripts;
}
/*
Helper methods
*/
protected OAuth2TokenExtractor getExtractor() {
return new OAuth2TokenExtractor();
}
void selectField(InputPanel field) {
selectedInputField = field;
for (InputPanel inputPanel : inputPanels) {
if (inputPanel == field) {
inputPanel.highlight();
} else {
inputPanel.removeHighlight();
}
}
removeScriptButton.setEnabled(selectedInputField != null);
}
private JPanel makeScriptsPanel(final OAuth2Profile profile) {
scriptUpdater = new ScriptUpdater(profile);
List<String> currentScripts = profile.getAutomationJavaScripts();
scriptsPanel = new JPanel();
scriptsPanel.setLayout(new BoxLayout(scriptsPanel, BoxLayout.Y_AXIS));
scriptsPanel.setBackground(Color.WHITE);
int numberOfFields = Math.max(2, currentScripts.size());
for (int index = 0; index < numberOfFields; index++) {
RSyntaxTextArea scriptField = SyntaxEditorUtil.createDefaultJavaScriptSyntaxTextArea();
String scriptName = (index < DEFAULT_SCRIPT_NAMES.length ? DEFAULT_SCRIPT_NAMES[index] : "Page " + (index + 1));
scriptField.setName(scriptName);
if (currentScripts.size() > index) {
scriptField.setText(currentScripts.get(index));
}
scriptField.getDocument().addDocumentListener(scriptUpdater);
scriptFields.add(scriptField);
InputPanel inputPanel = new InputPanel(scriptName, scriptField);
inputPanel.setName("Input panel " + (index + 1));
inputPanels.add(inputPanel);
scriptsPanel.add(inputPanel);
}
JPanel parentPanel = new JPanel(new BorderLayout());
parentPanel.setBorder(new CompoundBorder(new LineBorder(Color.BLACK), new EmptyBorder(15, 15, 15, 15)));
parentPanel.add(scriptsPanel, BorderLayout.CENTER);
return parentPanel;
}
private void showErrorMessage(String message) {
if (message.length() > UISupport.EXTENDED_ERROR_MESSAGE_THRESHOLD) {
UISupport.showErrorMessage(message.replaceAll("\r\n", "<br/>"));
} else {
UISupport.showErrorMessage(message);
}
}
/*
Private helper classes
*/
private class AddScriptAction extends AbstractAction {
private AddScriptAction() {
putValue(SMALL_ICON, UISupport.createImageIcon("/add.png"));
putValue(SHORT_DESCRIPTION, "Add script field");
putValue(LONG_DESCRIPTION, "Adds a new script input field");
}
@Override
public void actionPerformed(ActionEvent e) {
RSyntaxTextArea scriptField = SyntaxEditorUtil.createDefaultJavaScriptSyntaxTextArea();
int index = scriptFields.size() + 1;
String fieldName = "Page " + index;
scriptField.setName(fieldName);
scriptField.getDocument().addDocumentListener(scriptUpdater);
scriptFields.add(scriptField);
InputPanel inputPanel = new InputPanel(fieldName, scriptField);
inputPanel.setName("Input panel " + index);
inputPanels.add(inputPanel);
scriptsPanel.add(inputPanel, -1);
scriptsPanel.revalidate();
scriptsPanel.repaint();
}
}
private class RemoveScriptAction extends AbstractAction {
private RemoveScriptAction() {
putValue(SMALL_ICON, UISupport.createImageIcon("/delete.png"));
putValue(SHORT_DESCRIPTION, "Remove script field");
putValue(LONG_DESCRIPTION, "Removes the last script input field");
}
@Override
public void actionPerformed(ActionEvent e) {
if (UISupport.confirm("Do you really want to remove the script '" + selectedInputField.scriptField.getName() + "'",
"Remove script", OAuth2ScriptsEditor.this)) {
scriptFields.remove(selectedInputField.scriptField);
inputPanels.remove(selectedInputField);
scriptsPanel.remove(selectedInputField);
selectedInputField = null;
scriptsPanel.revalidate();
scriptsPanel.repaint();
selectField(null);
profile.setAutomationJavaScripts(getJavaScripts());
}
}
@Override
public boolean isEnabled() {
return selectedInputField != null;
}
}
private class TestScriptsAction extends AbstractAction {
private OAuth2Profile profile;
private TestScriptsAction(OAuth2Profile profile) {
this.profile = profile;
putValue(SMALL_ICON, UISupport.createImageIcon("/submit_request.gif"));
putValue(SHORT_DESCRIPTION, "Test scripts");
putValue(LONG_DESCRIPTION, "Validates the scripts and tries to execute them in a browser");
}
public void actionPerformed(ActionEvent e) {
boolean errorsFound = false;
for (RSyntaxTextArea scriptField : scriptFields) {
String script = scriptField.getText();
JavaScriptValidationError validate = javaScriptValidator.validate(script);
if (validate != null) {
showErrorMessage("The following script is invalid:\r\n" + script +
"\r\n\r\nError:<br/>" + validate.getErrorMessage());
errorsFound = true;
}
}
if (!errorsFound) {
OAuth2TokenExtractor extractor = getExtractor();
extractor.addBrowserListener(new JavaScriptErrorReporter(profile.getAutomationJavaScripts()));
OAuth2Parameters parameters = new OAuth2Parameters(profile);
try {
extractor.extractAccessToken(parameters);
} catch (Exception ignore) {
}
}
}
}
class InputPanel extends JPanel {
private RSyntaxTextArea scriptField;
private final Color originalBackground;
public InputPanel(String scriptName, RSyntaxTextArea scriptField) {
super(new BorderLayout(20, 20));
this.scriptField = scriptField;
add(new JLabel(scriptName), BorderLayout.NORTH);
add(new JScrollPane(scriptField), BorderLayout.CENTER);
MouseAdapter selectionHandler = new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (selectedInputField == InputPanel.this) {
if (e.getSource() == InputPanel.this) {
selectField(null);
}
} else {
selectField(InputPanel.this);
}
}
};
addMouseListener(selectionHandler);
setBorder(BorderFactory.createLineBorder(Color.WHITE));
originalBackground = getBackground();
}
@Override
public Dimension getMaximumSize() {
Dimension size = super.getMaximumSize();
size.height = 300;
return size;
}
public void highlight() {
setBorder(BorderFactory.createLineBorder(Color.GRAY));
setBackground(aDarkerShadeThan(originalBackground));
}
private Color aDarkerShadeThan(Color color) {
return new Color((int) (color.getRed() * .9),
(int) (color.getBlue() * .9), (int) (color.getGreen() * .9));
}
public void removeHighlight() {
setBorder(BorderFactory.createLineBorder(Color.WHITE));
setBackground(originalBackground);
}
@Override
public void setBorder(Border border) {
super.setBorder(new CompoundBorder(border, new EmptyBorder(20, 20, 20, 20)));
}
}
private class JavaScriptErrorReporter extends BrowserListenerAdapter {
private final List<String> expectedScripts;
private boolean hasErrors = false;
private List<String> executedScripts = new ArrayList<String>();
public JavaScriptErrorReporter(List<String> automationJavaScripts) {
this.expectedScripts = nonEmptyScriptsIn(automationJavaScripts);
}
private List<String> nonEmptyScriptsIn(List<String> scriptList) {
List<String> filteredList = new ArrayList<String>();
for (String script : scriptList) {
if (StringUtils.hasContent(script)) {
filteredList.add(script);
}
}
return filteredList;
}
@Override
public void javaScriptExecuted(final String script, final String errorLocation, final Exception error) {
executedScripts.add(script);
if (error != null) {
hasErrors = true;
// invokeLater() is necessary, because the call comes from the JavaFX invoker thread
SwingUtilities.invokeLater(new Runnable() {
public void run() {
showErrorMessage("The following script failed:\r\n" + script + "\r\nPage URL: " + errorLocation + "\r\nError:\r\n" +
error.getMessage() + "]");
}
});
}
}
@Override
public void browserClosed() {
if (!hasErrors) {
// invokeLater() is necessary, because the call comes from the JavaFX invoker thread
if (executedScripts.containsAll(expectedScripts)) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
UISupport.showInfoMessage("All scripts executed correctly.");
}
});
} else {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
UISupport.showInfoMessage("The scripts could only be partially validated, because all scripts " +
"weren't executed in the OAuth 2 flow.\n" +
"Maybe you already have an active session in the authorization server?",
"Scripts not fully validated");
}
});
}
}
}
}
private class ScriptUpdater extends DocumentListenerAdapter {
private final OAuth2Profile profile;
public ScriptUpdater(OAuth2Profile profile) {
this.profile = profile;
}
@Override
public void update(Document document) {
profile.setAutomationJavaScripts(getJavaScripts());
}
}
}