/*
* Copyright © 2010 Martin Riedel
*
* This file is part of TransFile.
*
* TransFile is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TransFile 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TransFile. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sourceforge.transfile.ui.swing;
import static net.sourceforge.jenerics.i18n.Translator.Helpers.translate;
import static net.sourceforge.transfile.ui.swing.StatusService.StatusMessage;
import static net.sourceforge.jenerics.Tools.getLoggerForThisMethod;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingWorker;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import net.sourceforge.transfile.exceptions.SerializationException;
import net.sourceforge.transfile.exceptions.SerializationFileInUseException;
import net.sourceforge.transfile.network.Peer;
import net.sourceforge.transfile.network.exceptions.BilateralConnectException;
import net.sourceforge.transfile.network.exceptions.PeerURLFormatException;
import net.sourceforge.transfile.operations.Connection;
import net.sourceforge.transfile.operations.Connection.State;
import net.sourceforge.transfile.settings.Settings;
import net.sourceforge.transfile.settings.exceptions.IllegalConfigValueException;
/**
* The area where the user enters the remote PeerURL, selects their local port and ip address
* and finally initiates a connection.
*
* @author Martin Riedel
*
*/
public class NetworkPanel extends TopLevelPanel {
private static final long serialVersionUID = -6730149437479457030L;
/*
* Represents the local IP address selected by the user or the application,
* or null if the selected address is invalid / no address has been selected
*/
private String selectedLocalAddress = null;
/*
* Represents the last local IP address selected before the current one
*/
private String lastSelectedLocalAddress = null;
/*
* Represents the local LAN IP addresses known to the application - or null if none have been found (yet)
*/
private Set<String> localLANAddresses = new HashSet<String>();
/*
* Represents the local internet/external/public IP address - or null if it hasn't been discovered yet
*/
private String localInternetIPAddress = null;
/*
* True if the user has selected a local IP address during this execution of the program
*/
private boolean userHasSelectedALocalIP = false;
/*
* Set to true by SwingWorker done()-methods executed in the Swing event dispatch thread if they
* have an intention of performing any action on localIPAddrBox that would otherwise be interpreted
* as the user selecting an item from the drop-down menu (or selecting an item via their keyboard).
*/
private boolean disregardNextLocalIPChange = false;
/*
* GUI subpanels
*/
private JPanel remoteURLPanel;
private JPanel localURLPanel;
/*
* TODO doc
*/
private PeerURLBar remoteURLBar;
/*
* TODO doc
*/
private PortSpinner localPort;
/*
* Dynamic GUI elements
*/
private JComboBox localIPAddressBox;
private JTextField localInternetAddressField;
private JTextField localURLField;
private JButton connectButton;
private JButton stopButton;
/**
* Creates a NetworkPanel
*
* @param window the main GUI class aggregating this NetworkPanel
*/
public NetworkPanel(final SwingGUI window) {
super(window);
this.setup();
}
/**
* {@inheritDoc}
*/
@Override
protected void onShow() {
// do nothing
}
/**
* {@inheritDoc}
*/
@Override
protected void onHide() {
// do nothing
}
/**
* {@inheritDoc}
*/
@Override
protected void onInit() {
retrieveLocalInternetIPAddress();
retrieveLocalLANAddresses();
}
/**
* {@inheritDoc}
*/
@Override
protected void onQuit() {
// do nothing
}
/**
* {@inheritDoc}
*/
@Override
protected void loadState() {
// load last entered local port (property always exists because there is a default)
this.getLocalPort().setValue(Settings.getPreferences().getInt("local_port", Settings.LOCAL_PORT));
// selected local IP address is loaded in onLANAddressesDiscovered() (if applicable)
}
/**
* {@inheritDoc}
*/
@Override
protected void saveState() {
// save PeerURLBar state
try {
getLoggerForThisMethod().log(Level.FINER, "attempting to save PeerURLBar state");
this.remoteURLBar.saveModel();
getLoggerForThisMethod().log(Level.FINE, "successfully saved PeerURLBar state");
} catch (final SerializationException e) {
getLoggerForThisMethod().log(Level.WARNING, "failed to save PeerURLBar state", e);
}
// save local port
Settings.getPreferences().put("local_port", this.getLocalPort().getValue().toString());
// save selected local IP address
if (this.selectedLocalAddress != null && !("".equals(this.selectedLocalAddress))) {
Settings.getPreferences().put("selected_local_ip", this.selectedLocalAddress);
}
}
/**
*
* @return
* <br>A possibly null value
*/
final JPanel getRemoteURLPanel() {
return this.remoteURLPanel;
}
/**
*
* @return
* <br>A possibly null value
*/
final JPanel getLocalURLPanel() {
return this.localURLPanel;
}
/**
*
* @return
* <br>A possibly null value
*/
final JButton getConnectButton() {
return this.connectButton;
}
/**
*
* @return
* <br>A possibly null value
*/
final JButton getStopButton() {
return this.stopButton;
}
/**
* Getter for {@code localLANAddresses}
*
* @return the local LAN addresses
*/
final Set<String> getLocalLANAddresses() {
return this.localLANAddresses;
}
/**
* Setter for {@code localLANAddresses}
*
* @param localLANAddresses
* <br />The local LAN addresses to set
* <br />Should not be null
*/
final void setLocalLANAddresses(Set<String> localLANAddresses) {
this.localLANAddresses = localLANAddresses;
}
/**
* Getter for {@code localInternetAddrField}
*
* @return the local Internet address text field
*/
final JTextField getLocalInternetAddrField() {
return this.localInternetAddressField;
}
/**
* Setter for {@code localInternetIPAddress}
*
* @param localInternetIPAddress
* <br />The local internet IP address to set
* <br />May be null
*/
final void setLocalInternetIPAddress(String localInternetIPAddress) {
this.localInternetIPAddress = localInternetIPAddress;
}
/**
* Getter for {@code localInternetIPAddress}
*
* @return the local internet IP address
*/
final String getLocalInternetIPAddress() {
return this.localInternetIPAddress;
}
/**
* Checks whether the next local IP address change will be disregarded in terms of updating the local
* addresses combobox's selected item
*
* @return whether the next local IP address change will be disregarded
*/
final boolean isDisregardNextLocalIPChange() {
return this.disregardNextLocalIPChange;
}
/**
* Makes sure the next local IP address change is disregarded in terms of updating the local
* addresses combobox's selected item
*
* @param disregardNextLocalIPChange whether to disregard the next local IP address change
*/
final void setDisregardNextLocalIPChange(boolean disregardNextLocalIPChange) {
this.disregardNextLocalIPChange = disregardNextLocalIPChange;
}
/**
* Getter for {@code localPort}
*
* @return the local port
*/
final PortSpinner getLocalPort() {
return this.localPort;
}
/**
* Updates the ComboBox containing the local IP addresses so that it contains
* exactly the elements from the provided Set<String> of addresses
*
*/
void updateLocalIPAddrBox() {
this.localIPAddressBox.removeAllItems();
if (this.localInternetIPAddress != null && !("".equals(this.localInternetIPAddress)))
if (!(this.localLANAddresses.contains(this.localInternetIPAddress)))
this.localIPAddressBox.addItem(this.localInternetIPAddress);
for (String address: this.localLANAddresses)
this.localIPAddressBox.addItem(address);
onLocalIPAddrBoxUpdated();
}
/**
* Shows the "Connect" button, hiding the "Stop" button
*
*/
void showConnectButton() {
this.connectButton.setVisible(true);
this.stopButton.setVisible(false);
}
/**
* Invoked when a connection has been established successfully
*
*/
void onConnectSuccessful() {
//TODO cancel the SwingWorkers retrieving the local and local external IP addresses
// if they're still running
// inform the main GUI class
getWindow().onConnectSuccessful();
}
/*
* BEGIN USER ACTION EVENT HANDLERS
*/
/**
* Invoked when the user selects a local IP address from the drop-down menu
*
* ACTUALLY INVOKED WHENEVER THE SELECTED LOCAL IP ADDRESS CHANGES, even if the user didn't
* initiate it.
*
* TODO RENAME TO onSelectedLocalAddressChanged or similar
*
* @param selectedItem the IP address selected by the user
*/
void onUserActionSelectLocalAddress(final String selectedItem) {
this.lastSelectedLocalAddress = this.selectedLocalAddress;
this.selectedLocalAddress = selectedItem;
updateLocalURL();
if (!this.disregardNextLocalIPChange)
if (!this.userHasSelectedALocalIP)
this.userHasSelectedALocalIP = true;
}
/**
* Invoked when the user changes the local port
*
*/
void onUserActionChangeLocalPort() {
updateLocalURL();
}
/**
* Invoked when the user initializes a connection attempt, i.e. by pressing the "Connect" button
*
*/
final void onUserActionConnect() {
final String remoteURL = (String) this.remoteURLBar.getSelectedItem();
if (remoteURL == null || "".equals(remoteURL)) {
this.getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("error_invalid_peerurl")));
return;
}
this.getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("status_connecting")));
final Connection connection = this.getWindow().getSession().getConnection();
connection.setLocalPeer(this.localURLField.getText());
connection.setRemotePeer((String) this.remoteURLBar.getSelectedItem());
connection.connect();
}
/**
* Invoked when the user interrupts a running connection attempt
*
*/
final void onUserActionInterrupt() {
this.getWindow().getSession().getConnection().disconnect();
}
/**
* TODO doc
*
* @param throwable
* <br>Maybe null
* <br>Maybe shared
*/
final void postErrorMessage(final Throwable throwable) {
if (throwable == null) {
return;
}
try {
throw throwable;
} catch (final PeerURLFormatException exception) {
getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("connect_fail_invalid_peerurl")));
getLoggerForThisMethod().log(Level.INFO, "failed to connect", exception);
} catch (final UnknownHostException exception) {
getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("connect_fail_unknown_host")));
getLoggerForThisMethod().log(Level.INFO, "failed to connect", exception);
} catch (final IllegalStateException exception) {
getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("connect_fail_illegal_state"), exception));
getLoggerForThisMethod().log(Level.SEVERE, "failed to connect", exception);
} catch (final BilateralConnectException exception) {
// TODO...
getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("connect_fail_no_link")));
getLoggerForThisMethod().log(Level.INFO, "failed to connect", exception);
} catch (final InterruptedException exception) {
// ignore, situation handled by CancellationException
} catch (final SocketTimeoutException exception) {
getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("connect_fail_timeout")));
getLoggerForThisMethod().log(Level.INFO, "failed to connect", exception);
} catch (final Throwable throwable2) {
throwable2.printStackTrace();
getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("connect_fail_unknown")));
getLoggerForThisMethod().log(Level.SEVERE, "failed to connect (unknown error)", throwable2);
}
}
/*
* END USER ACTION EVENT HANDLERS
*/
private final void setup() {
setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
this.remoteURLPanel = new JPanel();
this.remoteURLPanel.setBorder(translate(BorderFactory.createTitledBorder("section_remote_peerurl")));
c.gridx = 0;
c.gridy = 0;
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
c.insets = new Insets(5, 5, 5, 5);
GUITools.add(this, this.remoteURLPanel, c);
this.setupRemoteURLPanel();
this.localURLPanel = new JPanel();
this.localURLPanel.setBorder(translate(BorderFactory.createTitledBorder("section_local_peerurl")));
c.gridx = 0;
c.gridy = 1;
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
c.insets = new Insets(5, 5, 5, 5);
GUITools.add(this, this.localURLPanel, c);
this.setupLocalURLPanel();
this.connectButton = translate(new JButton("button_connect"));
this.connectButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
NetworkPanel.this.onUserActionConnect();
}
});
c.gridx = 0;
c.gridy = 2;
c.insets = new Insets(5, 5, 5, 5);
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
GUITools.add(this, this.connectButton, c);
this.stopButton = translate(new JButton("button_interrupt_connect"));
this.stopButton.setVisible(false);
this.stopButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
NetworkPanel.this.onUserActionInterrupt();
}
});
GUITools.add(this, this.stopButton, c);
this.getWindow().getSession().getConnection().addConnectionListener(new Connection.AbstractListener() {
@Override
protected final void doStateChanged() {
final Connection connection = NetworkPanel.this.getWindow().getSession().getConnection();
NetworkPanel.this.postErrorMessage(connection.getConnectionError());
final State state = connection.getState();
NetworkPanel.this.getConnectButton().setVisible(state == State.DISCONNECTED);
NetworkPanel.this.getStopButton().setVisible(state != State.DISCONNECTED);
if (state == State.CONNECTED) {
NetworkPanel.this.getWindow().onConnectSuccessful();
}
}
});
}
/**
* Sets up the "Remote PeerURL" panel
*
*/
private final void setupRemoteURLPanel() {
this.remoteURLBar = this.createRemoteURLBar();
final GridBagConstraints constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 1;
constraints.anchor = GridBagConstraints.CENTER;
constraints.insets = new Insets(5, 5, 5, 5);
constraints.gridx = 0;
constraints.gridy = 0;
constraints.gridwidth = 1;
GUITools.add(this.remoteURLPanel,this.remoteURLBar, constraints);
}
/**
* TODO doc
*
* @return
* <br>Not null
* <br>New
* @throws IllegalConfigValueException if the preference value for {@code "peerurlbar_max_retained_items"} is less than {@code 1}
*/
private final PeerURLBar createRemoteURLBar() {
final int maxRetainedItems = Settings.getPreferences().getInt("peerurlbar_max_retained_items", Settings.PEERURLBAR_MAX_RETAINED_ITEMS);
if (maxRetainedItems < 1) {
throw new IllegalConfigValueException("peerurlbar_max_retained_items", Integer.toString(maxRetainedItems));
}
PeerURLBar peerURLBar;
try {
peerURLBar = new PeerURLBar(Settings.getPreferences().get("remote_peerurlbar_state_file_name", Settings.REMOTE_PEERURLBAR_STATE_FILE_NAME), maxRetainedItems);
} catch (final SerializationFileInUseException exception) {
getLoggerForThisMethod().log(Level.WARNING, "remote PeerURLBar state file already in use, falling back to a non-persistent PeerURLBar", exception);
peerURLBar = new PeerURLBar(maxRetainedItems);
}
return peerURLBar;
}
/**
* Sets up the "Local PeerURL" panel
*
*/
private void setupLocalURLPanel() {
this.localURLField = createLocalURLField();
final JPanel localURLDetails = new JPanel();
final GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = new Insets(5, 5, 5, 5);
constraints.gridx = 0;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 1;
constraints.anchor = GridBagConstraints.CENTER;
GUITools.add(this.getLocalURLPanel(), new FoldableComponent(this.localURLField, localURLDetails), constraints);
this.localIPAddressBox = this.createLocalIPAddressBox();
this.localInternetAddressField = createLocalInternetAddressField();
this.localPort = this.createLocalPortSpinner();
gridBagTable(localURLDetails, new Component[][] {
{ translate(new JLabel("label_local_lan_addresses")), this.localIPAddressBox },
{ translate(new JLabel("label_local_internet_address")), this.localInternetAddressField },
{ translate(new JLabel("label_local_port")), this.getLocalPort() },
});
}
/**
* TODO doc
*
* @return
* <br>Not null
* <br>New
*/
private final PortSpinner createLocalPortSpinner() {
final PortSpinner localPortSpinner = new PortSpinner();
localPortSpinner.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
onUserActionChangeLocalPort();
}
});
return localPortSpinner;
}
/**
* TODO doc
*
* @return
* <br>Not null
* <br>New
*/
private final JComboBox createLocalIPAddressBox() {
final JComboBox result = new JComboBox();
result.setEditable(false);
result.addActionListener(new ActionListener() {
@Override
public final void actionPerformed(final ActionEvent event) {
final JComboBox source = (JComboBox) event.getSource();
final String selectedItem = (String) source.getSelectedItem();
if (event.getActionCommand().equals("comboBoxChanged")) {
if (selectedItem != null) {
NetworkPanel.this.onUserActionSelectLocalAddress(selectedItem);
}
}
}
});
return result;
}
/**
* Retrieves the local internet/external/public IP addresses in a separate thread in order to not block GUI creation.
* Also updates the localIPAddrBox from the Swing event dispatch thread after retrieving the necessary data
*
*/
private void retrieveLocalInternetIPAddress() {
new SwingWorker<String, Void>() {
@Override
protected String doInBackground() throws Exception {
return getWindow().getSession().getLocalExternalAddress();
}
@Override
protected void done() {
try {
NetworkPanel.this.setLocalInternetIPAddress(get());
NetworkPanel.this.getLocalInternetAddrField().setText(NetworkPanel.this.getLocalInternetIPAddress());
NetworkPanel.this.setDisregardNextLocalIPChange(true);
updateLocalIPAddrBox();
} catch (InterruptedException e) {
//TODO when exactly does this happen. should be while the third thread
// involved with this SwingWorker gets interrupted while waiting for get to
// stop blocking - handle? if yes, how?
} catch (ExecutionException e) {
Throwable cause = e.getCause();
getLoggerForThisMethod().log(Level.WARNING, "failed to discover external IP address", cause);
if (cause instanceof UnknownHostException) {
getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("error_discover_internet_unknown_host")));
NetworkPanel.this.getLocalInternetAddrField().setText(translate("not_available"));
} else if (cause instanceof MalformedURLException) {
getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("error_discover_internet_malformed_url"), cause));
NetworkPanel.this.getLocalInternetAddrField().setText(translate("not_available"));
} else if (cause instanceof IOException) {
getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("error_discover_internet_io_error")));
NetworkPanel.this.getLocalInternetAddrField().setText(translate("not_available"));
} else {
getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("error_discover_internet_unknown")));
NetworkPanel.this.getLocalInternetAddrField().setText(translate("N/A"));
}
}
}
}.execute();
}
/**
* Retrieves the local LAN IP addresses in a separate thread in order to not block GUI creation.
* Also updates the localIPAddrBox from the Swing event dispatch thread after retrieving the necessary data
*
*/
private void retrieveLocalLANAddresses() {
new SwingWorker<Set<String>, Void>() {
@Override
protected Set<String> doInBackground() throws Exception {
return getWindow().getSession().getLocalAddresses(true);
}
@Override
protected void done() {
try {
NetworkPanel.this.setLocalLANAddresses(get());
NetworkPanel.this.setDisregardNextLocalIPChange(true);
updateLocalIPAddrBox();
} catch (InterruptedException e) {
//TODO when exactly does this happen. should be while the third thread
// involved with this SwingWorker gets interrupted while waiting for get to
// stop blocking - handle? if yes, how?
} catch (ExecutionException e) {
Throwable cause = e.getCause();
getLoggerForThisMethod().log(Level.WARNING, "failed to discover local LAN addresses", cause);
if (cause instanceof SocketException)
NetworkPanel.this.getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("error_discover_lan_sockets")));
else
NetworkPanel.this.getWindow().getStatusService().postStatusMessage(translate(new StatusMessage("error_discover_lan_unknown")));
}
}
}.execute();
}
/**
* Updates the "Local PeerURL" field using the IP address and port selected by the user
*
*/
private void updateLocalURL() {
if (this.selectedLocalAddress == null || "".equals(this.selectedLocalAddress)) {
this.localURLField.setText("N/A");
return;
}
this.localURLField.setText(Peer.makePeerURL(this.selectedLocalAddress, ((Number) this.getLocalPort().getValue()).intValue()));
}
/**
* Invoked whenever the JComboBox containing the local IP addresses is updated by the application
*
*/
private void onLocalIPAddrBoxUpdated() {
// if the user has selected an IP before during this execution of the application,
// re-select that one (if still present). if not, use the one stored in Settings if there is one and the
// IP is still present.
String ipToSelect = this.userHasSelectedALocalIP ?
this.lastSelectedLocalAddress :
Settings.getPreferences().get("selected_local_ip", "");
if (ipToSelect == null || "".equals(ipToSelect))
return;
if (this.localLANAddresses.contains(ipToSelect))
this.localIPAddressBox.setSelectedItem(ipToSelect);
else
if (ipToSelect.equals(this.localInternetIPAddress))
this.localIPAddressBox.setSelectedItem(ipToSelect);
// selection events not to be processed by the
if (this.disregardNextLocalIPChange)
this.disregardNextLocalIPChange = false;
}
/**
* TODO doc
*
* @return
* <br>Not null
* <br>New
*/
private static final JTextField createLocalInternetAddressField() {
final JTextField result = new JTextField();
result.setEditable(false);
return result;
}
/**
* TODO doc
*
* @return
* <br>Not null
* <br>New
*/
private static final JTextField createLocalURLField() {
final JTextField result = new JTextField();
result.setEditable(false);
return result;
}
/**
* TODO doc
*
* @param result
* <br>Not null
* <br>Input-output
* @param components
* <br>Not null
* @return {@code result}
* <br>Not null
*/
private static final JPanel gridBagTable(final JPanel result, final Component[][] components) {
final GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = new Insets(5, 5, 5, 5);
constraints.anchor = GridBagConstraints.LINE_START;
constraints.gridy = 0;
for (final Component[] row : components) {
switch (row.length) {
case 1:
constraints.gridwidth = 2;
constraints.gridx = 0;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 1;
GUITools.add(result, row[0], constraints);
break;
case 2:
constraints.gridwidth = 1;
constraints.gridx = 0;
constraints.fill = GridBagConstraints.NONE;
constraints.weightx = 0;
GUITools.add(result, row[0], constraints);
constraints.gridx = 1;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weightx = 1;
GUITools.add(result, row[1], constraints);
break;
case 3:
throw new IllegalArgumentException("Each row must have 1 or 2 cells, but row " + constraints.gridy + " has " + row.length);
}
++constraints.gridy;
}
return result;
}
}