/*
* Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved.
*
* This file is part of the Jspresso framework.
*
* Jspresso is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Jspresso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Jspresso. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jspresso.framework.security.swing;
import java.awt.Component;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.ConfirmationCallback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.TextOutputCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import org.jspresso.framework.util.i18n.ITranslationProvider;
import org.jspresso.framework.util.swing.SwingUtil;
import org.jspresso.framework.view.IIconFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* Uses a Swing dialog to query the user for answers to authentication
* questions. This can be used by a JAAS application to instantiate a
* CallbackHandler
*/
public class DialogCallbackHandler implements CallbackHandler {
private static final Logger LOG = LoggerFactory
.getLogger(DialogCallbackHandler.class);
private static final int DEFAULT_FIELD_LENGTH = 32;
private static final Insets DEFAULT_INSETS = new Insets(5, 5, 5, 5);
private IIconFactory<Icon> iconFactory;
private Locale locale;
private Component parentComponent;
private ITranslationProvider translationProvider;
/**
* Handles the specified set of callbacks.
*
* @param callbacks
* the callbacks to handle
* @throws UnsupportedCallbackException
* if the callback is not an instance of NameCallback or
* PasswordCallback
*/
@Override
public void handle(final Callback[] callbacks)
throws UnsupportedCallbackException {
try {
SwingUtilities.invokeAndWait(new Runnable() {
@SuppressWarnings("ConstantConditions")
@Override
public void run() {
try {
Callback[] varCallbacks = callbacks;
boolean tocFound = false;
boolean pcFound = false;
for (Callback callback : varCallbacks) {
if (callback instanceof TextOutputCallback) {
tocFound = true;
} else if (callback instanceof PasswordCallback) {
pcFound = true;
}
}
if (pcFound && !tocFound) {
TextOutputCallback defaultToc = new TextOutputCallback(
TextOutputCallback.INFORMATION, "credentialMessage");
List<Callback> completedCallBacks = new ArrayList<>(
Arrays.asList(varCallbacks));
completedCallBacks.add(defaultToc);
varCallbacks = completedCallBacks
.toArray(new Callback[varCallbacks.length + 1]);
}
String dialogTitle = null;
final JDialog callbackDialog;
if (parentComponent != null) {
Window parentWindow = SwingUtil.getVisibleWindow(parentComponent);
if (parentWindow instanceof Dialog) {
callbackDialog = new JDialog((Dialog) parentWindow, true);
} else {
callbackDialog = new JDialog((Frame) parentWindow, true);
}
} else {
callbackDialog = new JDialog((Frame) null, true);
}
callbackDialog
.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
List<ActionListener> proceedActions = new ArrayList<>(
2);
JPanel messagePanel = null;
JPanel inputPanel = null;
JPanel optionPanel = null;
for (Callback callback : varCallbacks) {
if (callback instanceof TextOutputCallback) {
if (messagePanel == null) {
messagePanel = new JPanel();
messagePanel.setLayout(new GridBagLayout());
}
processTextOutputCallback(messagePanel,
(TextOutputCallback) callback);
if (dialogTitle == null) {
dialogTitle = translationProvider.getTranslation(
((TextOutputCallback) callback).getMessage(), locale);
}
} else if (callback instanceof NameCallback) {
if (inputPanel == null) {
inputPanel = new JPanel();
inputPanel.setLayout(new GridBagLayout());
}
processNameCallback(proceedActions, inputPanel,
(NameCallback) callback);
} else if (callback instanceof PasswordCallback) {
if (inputPanel == null) {
inputPanel = new JPanel();
inputPanel.setLayout(new GridBagLayout());
}
processPasswordCallback(proceedActions, inputPanel,
(PasswordCallback) callback);
} else if (callback instanceof ConfirmationCallback) {
if (optionPanel == null) {
optionPanel = new JPanel();
optionPanel.setLayout(new GridBagLayout());
}
processConfirmationCallback(callbackDialog, proceedActions,
optionPanel, (ConfirmationCallback) callback,
inputPanel != null);
} else {
throw new UnsupportedCallbackException(callback,
"Unrecognized Callback");
}
}
JPanel dialogPanel = new JPanel();
dialogPanel.setLayout(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = DEFAULT_INSETS;
constraints.gridx = GridBagConstraints.RELATIVE;
constraints.gridy = GridBagConstraints.RELATIVE;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.weightx = 1.0d;
constraints.weighty = 0.0d;
constraints.fill = GridBagConstraints.BOTH;
if (messagePanel != null) {
dialogPanel.add(messagePanel, constraints);
}
if (inputPanel != null) {
dialogPanel.add(inputPanel, constraints);
}
if (optionPanel == null) {
optionPanel = new JPanel();
optionPanel.setLayout(new GridBagLayout());
ConfirmationCallback cc = new ConfirmationCallback(
ConfirmationCallback.INFORMATION,
ConfirmationCallback.OK_CANCEL_OPTION,
ConfirmationCallback.OK);
processConfirmationCallback(callbackDialog, proceedActions,
optionPanel, cc, inputPanel != null);
}
dialogPanel.add(optionPanel, constraints);
if (dialogTitle != null) {
callbackDialog.setTitle(dialogTitle);
}
callbackDialog.getContentPane().add(dialogPanel);
int screenRes = Toolkit.getDefaultToolkit().getScreenResolution();
callbackDialog.setSize(new Dimension(4 * screenRes, screenRes
* (varCallbacks.length + 1) / 2));
callbackDialog.pack();
SwingUtil.centerOnScreen(callbackDialog);
callbackDialog.setVisible(true);
} catch (UnsupportedCallbackException ex) {
throw new RuntimeException(ex);
}
}
});
} catch (InterruptedException ex) {
LOG.error("An unexpected error occurred when handing callbacks.", ex);
} catch (InvocationTargetException ex) {
if (ex.getCause() instanceof RuntimeException
&& ex.getCause().getCause() instanceof UnsupportedCallbackException) {
throw (UnsupportedCallbackException) ex.getCause().getCause();
}
LOG.error("An unexpected error occurred when handing callbacks.", ex);
}
}
/**
* Sets the iconFactory.
*
* @param iconFactory
* the iconFactory to set.
*/
public void setIconFactory(IIconFactory<Icon> iconFactory) {
this.iconFactory = iconFactory;
}
/**
* Sets the locale.
*
* @param locale
* the locale to set.
*/
public void setLocale(Locale locale) {
this.locale = locale;
}
/**
* Sets the parentComponent.
*
* @param parentComponent
* the parentComponent to set.
*/
public void setParentComponent(Component parentComponent) {
this.parentComponent = parentComponent;
}
/**
* Sets the translationProvider.
*
* @param translationProvider
* the translationProvider to set.
*/
public void setTranslationProvider(ITranslationProvider translationProvider) {
this.translationProvider = translationProvider;
}
private JButton createOptionButton(final JDialog callbackDialog,
final ConfirmationCallback cc, final int option, String text,
final List<ActionListener> proceedActions) {
JButton optionButton = new JButton(text);
if (option == ConfirmationCallback.YES || option == ConfirmationCallback.OK) {
optionButton.setIcon(iconFactory.getOkYesIcon(iconFactory
.getSmallIconSize()));
optionButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for (ActionListener proceedAction : proceedActions) {
proceedAction.actionPerformed(e);
}
cc.setSelectedIndex(option);
callbackDialog.dispose();
}
});
} else {
if (option == ConfirmationCallback.NO) {
optionButton.setIcon(iconFactory.getNoIcon(iconFactory
.getSmallIconSize()));
} else if (option == ConfirmationCallback.CANCEL) {
optionButton.setIcon(iconFactory.getCancelIcon(iconFactory
.getSmallIconSize()));
}
optionButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
cc.setSelectedIndex(option);
callbackDialog.dispose();
}
});
}
if (cc.getDefaultOption() == option) {
callbackDialog.getRootPane().setDefaultButton(optionButton);
}
return optionButton;
}
private Icon getIcon(TextOutputCallback callback)
throws UnsupportedCallbackException {
switch (callback.getMessageType()) {
case TextOutputCallback.INFORMATION:
return iconFactory.getInfoIcon(iconFactory.getSmallIconSize());
case TextOutputCallback.WARNING:
return iconFactory.getWarningIcon(iconFactory.getSmallIconSize());
case TextOutputCallback.ERROR:
return iconFactory.getErrorIcon(iconFactory.getSmallIconSize());
default:
throw new UnsupportedCallbackException(callback,
"Unrecognized message type");
}
}
private void processConfirmationCallback(final JDialog callbackDialog,
List<ActionListener> proceedActions, JPanel optionPanel,
final ConfirmationCallback cc, boolean hasInput)
throws UnsupportedCallbackException {
GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = DEFAULT_INSETS;
constraints.gridx = GridBagConstraints.RELATIVE;
constraints.gridy = GridBagConstraints.RELATIVE;
constraints.gridwidth = 1;
constraints.weightx = 0.0d;
constraints.fill = GridBagConstraints.NONE;
int confirmationOptionType = cc.getOptionType();
if (confirmationOptionType == ConfirmationCallback.UNSPECIFIED_OPTION) {
for (int i = 0; i < cc.getOptions().length; i++) {
final int optionIndex = i;
JButton optionButton = new JButton(cc.getOptions()[i]);
optionButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
cc.setSelectedIndex(optionIndex);
callbackDialog.dispose();
}
});
optionPanel.add(optionButton, constraints);
}
} else {
if (locale == null) {
locale = Locale.getDefault();
}
switch (confirmationOptionType) {
case ConfirmationCallback.YES_NO_OPTION:
optionPanel.add(
createOptionButton(callbackDialog, cc, ConfirmationCallback.YES,
translationProvider.getTranslation("yes", locale),
proceedActions), constraints);
optionPanel.add(
createOptionButton(callbackDialog, cc, ConfirmationCallback.NO,
translationProvider.getTranslation("no", locale),
proceedActions), constraints);
break;
case ConfirmationCallback.YES_NO_CANCEL_OPTION:
optionPanel.add(
createOptionButton(callbackDialog, cc, ConfirmationCallback.YES,
translationProvider.getTranslation("yes", locale),
proceedActions), constraints);
optionPanel.add(
createOptionButton(callbackDialog, cc, ConfirmationCallback.NO,
translationProvider.getTranslation("no", locale),
proceedActions), constraints);
optionPanel.add(
createOptionButton(callbackDialog, cc,
ConfirmationCallback.CANCEL,
translationProvider.getTranslation("cancel", locale),
proceedActions), constraints);
break;
case ConfirmationCallback.OK_CANCEL_OPTION:
optionPanel.add(
createOptionButton(callbackDialog, cc, ConfirmationCallback.OK,
translationProvider.getTranslation("ok", locale),
proceedActions), constraints);
if (hasInput) {
optionPanel.add(
createOptionButton(callbackDialog, cc,
ConfirmationCallback.CANCEL,
translationProvider.getTranslation("cancel", locale),
proceedActions), constraints);
}
break;
default:
throw new UnsupportedCallbackException(cc,
"Unrecognized option type: " + confirmationOptionType);
}
}
}
private void processNameCallback(final List<ActionListener> proceedActions,
JPanel inputPanel, final NameCallback nc) {
// JLabel promptLabel = new JLabel(nc.getPrompt());
JLabel promptLabel = new JLabel(translationProvider.getTranslation("user",
locale) + " :");
final JTextField nameTextField = new JTextField(DEFAULT_FIELD_LENGTH);
// String defaultName = nc.getDefaultName();
// if (defaultName != null) {
// nameTextField.setText(defaultName);
// }
GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = DEFAULT_INSETS;
constraints.gridx = GridBagConstraints.RELATIVE;
constraints.gridy = GridBagConstraints.RELATIVE;
constraints.gridwidth = 1;
inputPanel.add(promptLabel, constraints);
constraints.weightx = 1.0d;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridwidth = GridBagConstraints.REMAINDER;
inputPanel.add(nameTextField, constraints);
proceedActions.add(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
nc.setName(nameTextField.getText());
}
});
}
private void processPasswordCallback(
final List<ActionListener> proceedActions, JPanel inputPanel,
final PasswordCallback pc) {
// JLabel promptLabel = new JLabel(pc.getPrompt());
JLabel promptLabel = new JLabel(translationProvider.getTranslation(
"password", locale) + " :");
final JPasswordField passwordField = new JPasswordField(
DEFAULT_FIELD_LENGTH);
if (!pc.isEchoOn()) {
passwordField.setEchoChar('*');
}
GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = DEFAULT_INSETS;
constraints.gridx = GridBagConstraints.RELATIVE;
constraints.gridy = GridBagConstraints.RELATIVE;
constraints.gridwidth = 1;
inputPanel.add(promptLabel, constraints);
constraints.weightx = 1.0d;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridwidth = GridBagConstraints.REMAINDER;
inputPanel.add(passwordField, constraints);
proceedActions.add(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
pc.setPassword(passwordField.getPassword());
}
});
}
private void processTextOutputCallback(JPanel messagePanel,
TextOutputCallback toc) throws UnsupportedCallbackException {
JLabel messageLabel = new JLabel(translationProvider.getTranslation(
toc.getMessage(), locale), getIcon(toc), SwingConstants.LEADING);
GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = DEFAULT_INSETS;
constraints.gridx = GridBagConstraints.RELATIVE;
constraints.gridy = GridBagConstraints.RELATIVE;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.weightx = 1.0d;
constraints.fill = GridBagConstraints.HORIZONTAL;
messagePanel.add(messageLabel, constraints);
}
}