/*
* 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.SoapUI;
import com.eviware.soapui.impl.rest.OAuth2Profile;
import com.eviware.soapui.impl.rest.actions.oauth.RefreshOAuthAccessTokenAction;
import com.eviware.soapui.impl.wsdl.support.HelpUrls;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.components.SimpleBindingForm;
import com.eviware.soapui.support.components.SimpleForm;
import com.eviware.soapui.support.editor.inspectors.AbstractXmlInspector;
import com.jgoodies.binding.PresentationModel;
import com.jgoodies.binding.adapter.Bindings;
import javax.annotation.Nonnull;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Insets;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
public class OAuth2Form extends AbstractAuthenticationForm implements OAuth2AccessTokenStatusChangeListener {
public static final String ADVANCED_OPTIONS_BUTTON_NAME = "Advanced...";
public static final String REFRESH_ACCESS_TOKEN_BUTTON_NAME = "refreshAccessTokenButton";
private static final int ACCESS_TOKEN_DIALOG_HORIZONTAL_OFFSET = 120;
private static final Dimension HORIZONTAL_SPACING_IN_ACCESS_TOKEN_ROW = new Dimension(5, 0);
private static final String ACCESS_TOKEN_LABEL = "Access Token";
private static final Insets ACCESS_TOKEN_FIELD_INSETS = new Insets(5, 5, 5, 5);
private static final float ACCESS_TOKEN_STATUS_TEXT_FONT_SCALE = 0.95f;
private static final int ACCESS_TOKEN_STATUS_TEXT_WIDTH = 100;
private static final String GET_ACCESS_TOKEN_BUTTON_DEFAULT_LABEL = "Get Token";
private static final String GET_ACCESS_TOKEN_BUTTON_RESUME_LABEL = GET_ACCESS_TOKEN_BUTTON_DEFAULT_LABEL + " (Resume)";
private final Color DEFAULT_COLOR = Color.WHITE;
private final Color SUCCESS_COLOR = new Color(0xccffcb);
private final Color FAIL_COLOR = new Color(0xffcccc);
static final ImageIcon SUCCESS_ICON = UISupport.createImageIcon("/check.png");
static final ImageIcon WAIT_ICON = UISupport.createImageIcon("/waiting-spinner.gif");
static final ImageIcon FAIL_ICON = UISupport.createImageIcon("/alert.png");
private final AbstractXmlInspector inspector;
private final OAuth2AccessTokenStatusChangeManager statusChangeManager;
private OAuth2Profile profile;
private JPanel formPanel;
private boolean disclosureButtonDisabled;
private boolean isMouseOnDisclosureLabel;
private SimpleBindingForm oAuth2Form;
private JTextField accessTokenField;
private JLabel accessTokenStatusIcon;
private JLabel accessTokenStatusText;
private JLabel disclosureButton;
private OAuth2GetAccessTokenForm accessTokenForm;
private SoapUIMainWindowFocusListener mainWindowFocusListener;
public OAuth2Form(OAuth2Profile profile, AbstractXmlInspector inspector) {
super();
this.profile = profile;
this.inspector = inspector;
statusChangeManager = new OAuth2AccessTokenStatusChangeManager(this);
}
void release() {
SoapUI.getFrame().removeWindowFocusListener(mainWindowFocusListener);
accessTokenForm.release();
oAuth2Form.getPresentationModel().release();
statusChangeManager.unregister();
}
@Override
public void onAccessTokenStatusChanged(@Nonnull OAuth2Profile.AccessTokenStatus status) {
setAccessTokenStatusFeedback(status);
}
@Nonnull
@Override
public OAuth2Profile getProfile() {
return profile;
}
@Override
protected JPanel buildUI() {
oAuth2Form = new SimpleBindingForm(new PresentationModel<OAuth2Profile>(profile));
addOAuth2Panel(oAuth2Form);
statusChangeManager.register();
if (profile.getAccessTokenStatus() != OAuth2Profile.AccessTokenStatus.RETRIEVAL_CANCELED) {
profile.resetAccessTokenStatusToStartingStatus();
}
setAccessTokenStatusFeedback(profile.getAccessTokenStatus());
return formPanel;
}
// TODO Use the refactored SimpleBidningsForm instead
private void addOAuth2Panel(SimpleBindingForm oAuth2Form) {
populateOAuth2Form(oAuth2Form);
formPanel = new JPanel(new BorderLayout());
JPanel centerPanel = oAuth2Form.getPanel();
setBackgroundColorOnPanel(centerPanel);
JPanel southPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JLabel oAuthDocumentationLink = UISupport.createLabelLink(HelpUrls.OAUTH_OVERVIEW, "Learn about OAuth 2");
southPanel.add(oAuthDocumentationLink);
southPanel.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, CARD_BORDER_COLOR));
setBackgroundColorOnPanel(southPanel);
formPanel.add(centerPanel, BorderLayout.CENTER);
formPanel.add(southPanel, BorderLayout.SOUTH);
setBorderOnPanel(formPanel);
}
private void populateOAuth2Form(SimpleBindingForm oAuth2Form) {
initForm(oAuth2Form);
oAuth2Form.addSpace(TOP_SPACING);
JTextField accessTokenField = createAccessTokenField();
JLabel accessTokenStatusIcon = createAccessTokenStatusIcon();
JLabel accessTokenStatusText = createAccessTokenStatusText();
final JButton refreshAccessTokenButton = createRefreshButton();
JPanel accessTokenRowPanel = createAccessTokenRowPanel(accessTokenField, accessTokenStatusIcon, accessTokenStatusText, refreshAccessTokenButton);
oAuth2Form.append(ACCESS_TOKEN_LABEL, accessTokenRowPanel);
oAuth2Form.addInputFieldHintText("Enter existing access token, or use \"Get Token\" below.");
disclosureButton = new JLabel(GET_ACCESS_TOKEN_BUTTON_DEFAULT_LABEL);
disclosureButton.setIcon(UISupport.createImageIcon("/pop-down-open.png"));
disclosureButton.setName("oAuth2DisclosureButton");
oAuth2Form.addComponentWithoutLabel(disclosureButton);
accessTokenForm = new OAuth2GetAccessTokenForm(profile);
final JDialog accessTokenFormDialog = accessTokenForm.getComponent();
disclosureButton.addMouseListener(new DisclosureButtonMouseListener(accessTokenFormDialog, disclosureButton));
accessTokenFormDialog.addWindowFocusListener(new AccessTokenFormDialogWindowListener(accessTokenFormDialog));
JButton advancedOptionsButton = oAuth2Form.addButtonWithoutLabelToTheRight(ADVANCED_OPTIONS_BUTTON_NAME, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
new OAuth2AdvancedOptionsDialog(profile, refreshAccessTokenButton);
}
});
advancedOptionsButton.setName(ADVANCED_OPTIONS_BUTTON_NAME);
mainWindowFocusListener = new SoapUIMainWindowFocusListener(accessTokenFormDialog);
SoapUI.getFrame().addWindowFocusListener(mainWindowFocusListener);
}
private JTextField createAccessTokenField() {
accessTokenField = new JTextField();
accessTokenField.setName(OAuth2Profile.ACCESS_TOKEN_PROPERTY);
accessTokenField.setColumns(SimpleForm.MEDIUM_TEXT_FIELD_COLUMNS);
accessTokenField.setMargin(ACCESS_TOKEN_FIELD_INSETS);
Bindings.bind(accessTokenField, oAuth2Form.getPresentationModel().getModel(OAuth2Profile.ACCESS_TOKEN_PROPERTY));
return accessTokenField;
}
private JLabel createAccessTokenStatusIcon() {
accessTokenStatusIcon = new JLabel();
accessTokenStatusIcon.setVisible(false);
return accessTokenStatusIcon;
}
private JLabel createAccessTokenStatusText() {
accessTokenStatusText = new JLabel();
accessTokenStatusText.setFont(scaledFont(accessTokenStatusText, ACCESS_TOKEN_STATUS_TEXT_FONT_SCALE));
accessTokenStatusText.setVisible(false);
accessTokenStatusText.setAlignmentX(Component.CENTER_ALIGNMENT);
return accessTokenStatusText;
}
private JButton createRefreshButton() {
final JButton refreshAccessTokenButton = new JButton("Refresh");
refreshAccessTokenButton.setName(REFRESH_ACCESS_TOKEN_BUTTON_NAME);
refreshAccessTokenButton.addActionListener(new RefreshOAuthAccessTokenAction(profile));
boolean enabled = profile.getRefreshAccessTokenMethod().equals(OAuth2Profile.RefreshAccessTokenMethods.MANUAL)
&& (!StringUtils.isNullOrEmpty(profile.getRefreshToken()));
refreshAccessTokenButton.setVisible(enabled);
return refreshAccessTokenButton;
}
private JPanel createAccessTokenRowPanel(JTextField accessTokenField, JLabel accessTokenStatusIcon, JLabel accessTokenStatusText, JButton refreshAccessTokenButton) {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
panel.setBackground(CARD_BACKGROUND_COLOR);
panel.add(accessTokenField);
panel.add(Box.createRigidArea(HORIZONTAL_SPACING_IN_ACCESS_TOKEN_ROW));
panel.add(accessTokenStatusIcon);
panel.add(Box.createRigidArea(HORIZONTAL_SPACING_IN_ACCESS_TOKEN_ROW));
panel.add(accessTokenStatusText);
panel.add(Box.createRigidArea(HORIZONTAL_SPACING_IN_ACCESS_TOKEN_ROW));
panel.add(refreshAccessTokenButton);
return panel;
}
private Font scaledFont(JComponent component, float scale) {
Font currentFont = component.getFont();
return currentFont.deriveFont((float) currentFont.getSize() * scale);
}
private String setWrappedText(String text) {
return String.format("<html><div WIDTH=%d>%s</div><html>", OAuth2Form.ACCESS_TOKEN_STATUS_TEXT_WIDTH, text);
}
private void setAccessTokenFormDialogBoundsBelowTheButton(Point disclosureButtonLocation, JDialog accessTokenFormDialog, int disclosureButtonHeight) {
accessTokenFormDialog.setLocation((int) disclosureButtonLocation.getX() - ACCESS_TOKEN_DIALOG_HORIZONTAL_OFFSET,
(int) disclosureButtonLocation.getY() + disclosureButtonHeight);
}
private void setAccessTokenFormDialogBoundsAboveTheButton(Point disclosureButtonLocation, JDialog accessTokenFormDialog) {
accessTokenFormDialog.setLocation((int) disclosureButtonLocation.getX() - ACCESS_TOKEN_DIALOG_HORIZONTAL_OFFSET,
(int) disclosureButtonLocation.getY() - accessTokenFormDialog.getHeight());
}
private void setAccessTokenStatusFeedback(OAuth2Profile.AccessTokenStatus status) {
switch (status) {
case UNKNOWN:
setDefaultFeedback();
break;
case ENTERED_MANUALLY:
setEnteredManuallyFeedback(status);
break;
case RETRIEVED_FROM_SERVER:
setSucessfulFeedback(status);
break;
case RETRIEVAL_CANCELED:
setCanceledFeedback();
break;
case EXPIRED:
setFailedFeedback(status);
break;
}
}
private void setEnteredManuallyFeedback(OAuth2Profile.AccessTokenStatus status) {
accessTokenField.setBackground(DEFAULT_COLOR);
accessTokenStatusIcon.setIcon(null);
accessTokenStatusIcon.setVisible(false);
accessTokenStatusText.setText("");
accessTokenStatusText.setVisible(false);
disclosureButton.setText(GET_ACCESS_TOKEN_BUTTON_DEFAULT_LABEL);
inspector.setIcon(ProfileSelectionForm.AUTH_ENABLED_ICON);
}
private void setSucessfulFeedback(OAuth2Profile.AccessTokenStatus status) {
accessTokenField.setBackground(SUCCESS_COLOR);
accessTokenStatusIcon.setIcon(SUCCESS_ICON);
accessTokenStatusIcon.setVisible(true);
accessTokenStatusText.setText(setWrappedText(status.toString()));
accessTokenStatusText.setVisible(true);
disclosureButton.setText(GET_ACCESS_TOKEN_BUTTON_DEFAULT_LABEL);
inspector.setIcon(ProfileSelectionForm.AUTH_ENABLED_ICON);
}
private void setFailedFeedback(OAuth2Profile.AccessTokenStatus status) {
accessTokenField.setBackground(FAIL_COLOR);
accessTokenStatusIcon.setIcon(FAIL_ICON);
accessTokenStatusIcon.setVisible(true);
accessTokenStatusText.setText(setWrappedText(status.toString()));
accessTokenStatusText.setVisible(true);
disclosureButton.setText(GET_ACCESS_TOKEN_BUTTON_DEFAULT_LABEL);
inspector.setIcon(FAIL_ICON);
}
private void setCanceledFeedback() {
setAccessTokenStatusFeedback(profile.getAccessTokenStartingStatus());
disclosureButton.setText(GET_ACCESS_TOKEN_BUTTON_RESUME_LABEL);
}
private void setDefaultFeedback() {
accessTokenField.setBackground(DEFAULT_COLOR);
accessTokenStatusIcon.setIcon(null);
accessTokenStatusIcon.setVisible(false);
accessTokenStatusText.setText("");
accessTokenStatusText.setVisible(false);
disclosureButton.setText(GET_ACCESS_TOKEN_BUTTON_DEFAULT_LABEL);
inspector.setIcon(ProfileSelectionForm.AUTH_ENABLED_ICON);
}
private void hideAccessTokenFormDialogAndEnableDisclosureButton(JDialog accessTokenFormDialog) {
accessTokenFormDialog.setVisible(false);
disclosureButton.setIcon(UISupport.createImageIcon("/pop-down-open.png"));
// If the focus is lost due to click on the disclosure button then don't enable it yet, since it
// will then show the dialog directly again.
if (!isMouseOnDisclosureLabel) {
disclosureButtonDisabled = false;
}
}
private class DisclosureButtonMouseListener extends MouseAdapter {
private final JDialog accessTokenFormDialog;
private final JLabel disclosureButton;
public DisclosureButtonMouseListener(JDialog accessTokenFormDialog, JLabel disclosureButton) {
this.accessTokenFormDialog = accessTokenFormDialog;
this.disclosureButton = disclosureButton;
}
@Override
public void mouseClicked(MouseEvent e) {
// Check if this click is to hide the access token form dialog
if (disclosureButtonDisabled) {
disclosureButtonDisabled = false;
return;
}
JLabel source = (JLabel) e.getSource();
Point disclosureButtonLocation = source.getLocationOnScreen();
accessTokenFormDialog.pack();
accessTokenFormDialog.setVisible(true);
disclosureButton.setIcon(UISupport.createImageIcon("/pop-down-close.png"));
if (UISupport.isEnoughSpaceAvailableBelowComponent(disclosureButtonLocation, accessTokenFormDialog.getHeight(), source.getHeight())) {
setAccessTokenFormDialogBoundsBelowTheButton(disclosureButtonLocation, accessTokenFormDialog, source.getHeight());
} else {
setAccessTokenFormDialogBoundsAboveTheButton(disclosureButtonLocation, accessTokenFormDialog);
}
}
@Override
public void mouseEntered(MouseEvent e) {
isMouseOnDisclosureLabel = true;
}
@Override
public void mouseExited(MouseEvent e) {
isMouseOnDisclosureLabel = false;
}
}
private class AccessTokenFormDialogWindowListener implements WindowFocusListener {
private final JDialog accessTokenFormDialog;
public AccessTokenFormDialogWindowListener(JDialog accessTokenFormDialog) {
this.accessTokenFormDialog = accessTokenFormDialog;
}
@Override
public void windowGainedFocus(WindowEvent e) {
disclosureButtonDisabled = true;
}
@Override
public void windowLostFocus(WindowEvent e) {
if (isMouseOnComponent(SoapUI.getFrame()) && !isMouseOnComponent(accessTokenFormDialog)) {
hideAccessTokenFormDialogAndEnableDisclosureButton(accessTokenFormDialog);
}
}
// TODO This might be extracted to a common utils class
private boolean isMouseOnComponent(Component component) {
if (!component.isShowing()) {
return false;
}
Point mouseLocation = MouseInfo.getPointerInfo().getLocation();
Point componentLocationOnScreen = component.getLocationOnScreen();
return component.contains(mouseLocation.x - componentLocationOnScreen.x, mouseLocation.y - componentLocationOnScreen.y);
}
}
private class SoapUIMainWindowFocusListener extends WindowAdapter {
private final JDialog accessTokenFormDialog;
public SoapUIMainWindowFocusListener(JDialog accessTokenFormDialog) {
this.accessTokenFormDialog = accessTokenFormDialog;
}
@Override
public void windowGainedFocus(WindowEvent e) {
if (accessTokenFormDialog.isVisible()) {
hideAccessTokenFormDialogAndEnableDisclosureButton(accessTokenFormDialog);
}
}
}
}