/**
* UPnP PortMapper - A tool for managing port forwardings via UPnP
* Copyright (C) 2015 Christoph Pirkl <christoph at users.sourceforge.net>
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
*
*/
package org.chris.portmapper.gui;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.border.Border;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import net.miginfocom.swing.MigLayout;
import org.chris.portmapper.PortMapperApp;
import org.chris.portmapper.model.PortMapping;
import org.chris.portmapper.model.PortMappingPreset;
import org.chris.portmapper.router.IRouter;
import org.chris.portmapper.router.RouterException;
import org.jdesktop.application.Action;
import org.jdesktop.application.FrameView;
import org.jdesktop.application.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The main view.
*/
public class PortMapperView extends FrameView {
private static final String ACTION_SHOW_ABOUT_DIALOG = "mainFrame.showAboutDialog";
private static final String ACTION_DISPLAY_ROUTER_INFO = "mainFrame.router.info";
private static final String ACTION_CONNECT_ROUTER = "mainFrame.router.connect";
private static final String ACTION_DISCONNECT_ROUTER = "mainFrame.router.disconnect";
private static final String ACTION_COPY_INTERNAL_ADDRESS = "mainFrame.router.copyInternalAddress";
private static final String ACTION_COPY_EXTERNAL_ADDRESS = "mainFrame.router.copyExternalAddress";
private static final String ACTION_UPDATE_ADDRESSES = "mainFrame.router.updateAddresses";
private static final String ACTION_UPDATE_PORT_MAPPINGS = "mainFrame.mappings.update";
private static final String ACTION_PORTMAPPER_SETTINGS = "mainFrame.portmapper.settings";
private static final String ACTION_REMOVE_MAPPINGS = "mainFrame.mappings.remove";
private static final String ACTION_CREATE_PRESET_MAPPING = "mainFrame.preset_mappings.create";
private static final String ACTION_EDIT_PRESET_MAPPING = "mainFrame.preset_mappings.edit";
private static final String ACTION_REMOVE_PRESET_MAPPING = "mainFrame.preset_mappings.remove";
private static final String ACTION_USE_PRESET_MAPPING = "mainFrame.preset_mappings.use";
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static final String PROPERTY_MAPPING_SELECTED = "mappingSelected";
private static final String PROPERTY_ROUTER_CONNECTED = "connectedToRouter";
private static final String PROPERTY_PRESET_MAPPING_SELECTED = "presetMappingSelected";
private PortMappingsTableModel tableModel;
private JTable mappingsTable;
private JLabel externalIPLabel, internalIPLabel;
private JButton connectDisconnectButton;
private JList<PortMappingPreset> portMappingPresets;
private final PortMapperApp app;
public PortMapperView(final PortMapperApp app) {
super(app);
this.app = app;
initView();
}
private void initView() {
// Create and set up the window.
final JPanel panel = new JPanel();
panel.setLayout(new MigLayout("", "[fill, grow]", "[grow 50]unrelated[]unrelated[grow 50]"));
panel.add(getMappingsPanel(), "wrap");
panel.add(getRouterPanel(), "grow 0, split 2");
panel.add(getPresetPanel(), "wrap");
panel.add(getLogPanel(), "wrap");
this.setComponent(panel);
}
private JComponent getRouterPanel() {
final ActionMap actionMap = this.getContext().getActionMap(this.getClass(), this);
final JPanel routerPanel = new JPanel(new MigLayout("", "[fill, grow][]", ""));
routerPanel
.setBorder(BorderFactory.createTitledBorder(app.getResourceMap().getString("mainFrame.router.title")));
routerPanel.add(new JLabel(app.getResourceMap().getString("mainFrame.router.external_address")), "align label"); //$NON-NLS-2$
externalIPLabel = new JLabel(app.getResourceMap().getString("mainFrame.router.not_connected"));
routerPanel.add(externalIPLabel, "width 130!");
routerPanel.add(new JButton(actionMap.get(ACTION_COPY_EXTERNAL_ADDRESS)), "sizegroup router");
routerPanel.add(new JButton(actionMap.get(ACTION_UPDATE_ADDRESSES)),
"wrap, spany 2, aligny base, sizegroup router");
routerPanel.add(new JLabel(app.getResourceMap().getString("mainFrame.router.internal_address")), "align label");
internalIPLabel = new JLabel(app.getResourceMap().getString("mainFrame.router.not_connected"));
routerPanel.add(internalIPLabel, "width 130!");
routerPanel.add(new JButton(actionMap.get(ACTION_COPY_INTERNAL_ADDRESS)), "wrap, sizegroup router");
connectDisconnectButton = new JButton(actionMap.get(ACTION_CONNECT_ROUTER));
routerPanel.add(connectDisconnectButton, "");
routerPanel.add(new JButton(actionMap.get(ACTION_DISPLAY_ROUTER_INFO)), "sizegroup router");
routerPanel.add(new JButton(actionMap.get(ACTION_SHOW_ABOUT_DIALOG)), "sizegroup router, wrap");
this.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(final PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(PROPERTY_ROUTER_CONNECTED)) {
logger.debug("Connection state changed to " + evt.getNewValue());
if (evt.getNewValue().equals(Boolean.TRUE)) {
connectDisconnectButton.setAction(actionMap.get(ACTION_DISCONNECT_ROUTER));
} else {
connectDisconnectButton.setAction(actionMap.get(ACTION_CONNECT_ROUTER));
}
}
}
});
routerPanel.add(new JButton(actionMap.get(ACTION_PORTMAPPER_SETTINGS)), "");
return routerPanel;
}
private JComponent getLogPanel() {
final LogTextArea logTextArea = new LogTextArea();
logTextArea.setEditable(false);
logTextArea.setWrapStyleWord(true);
logTextArea.setLineWrap(true);
app.setLogMessageListener(logTextArea);
final JScrollPane scrollPane = new JScrollPane();
scrollPane.setViewportView(logTextArea);
final JPanel logPanel = new JPanel(new MigLayout("", "[grow, fill]", "[grow, fill]"));
logPanel.setBorder(
BorderFactory.createTitledBorder(app.getResourceMap().getString("mainFrame.log_messages.title")));
logPanel.add(scrollPane, "height 100::");
return logPanel;
}
private JComponent getPresetPanel() {
final ActionMap actionMap = this.getContext().getActionMap(this.getClass(), this);
final JPanel presetPanel = new JPanel(new MigLayout("", "[grow, fill][]", ""));
presetPanel.setBorder(BorderFactory
.createTitledBorder(app.getResourceMap().getString("mainFrame.port_mapping_presets.title")));
portMappingPresets = new JList<>(new PresetListModel(app.getSettings()));
portMappingPresets.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
portMappingPresets.setLayoutOrientation(JList.VERTICAL);
portMappingPresets.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(final ListSelectionEvent e) {
logger.trace("Selection of preset list has changed: " + isPresetMappingSelected());
firePropertyChange(PROPERTY_PRESET_MAPPING_SELECTED, false, isPresetMappingSelected());
}
});
presetPanel.add(new JScrollPane(portMappingPresets), "spany 4, grow");
presetPanel.add(new JButton(actionMap.get(ACTION_CREATE_PRESET_MAPPING)), "wrap, sizegroup preset_buttons");
presetPanel.add(new JButton(actionMap.get(ACTION_EDIT_PRESET_MAPPING)), "wrap, sizegroup preset_buttons");
presetPanel.add(new JButton(actionMap.get(ACTION_REMOVE_PRESET_MAPPING)), "wrap, sizegroup preset_buttons");
presetPanel.add(new JButton(actionMap.get(ACTION_USE_PRESET_MAPPING)), "wrap, sizegroup preset_buttons");
return presetPanel;
}
private JComponent getMappingsPanel() {
// Mappings panel
final ActionMap actionMap = this.getContext().getActionMap(this.getClass(), this);
tableModel = new PortMappingsTableModel(app);
mappingsTable = new JTable(tableModel);
mappingsTable.setAutoCreateRowSorter(true);
mappingsTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
mappingsTable.setSize(new Dimension(400, 100));
mappingsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(final ListSelectionEvent e) {
firePropertyChange(PROPERTY_MAPPING_SELECTED, false, isMappingSelected());
}
});
final JScrollPane mappingsTabelPane = new JScrollPane();
mappingsTabelPane.setViewportView(mappingsTable);
final JPanel mappingsPanel = new JPanel(new MigLayout("", "[fill,grow]", "[grow,fill][]"));
mappingsPanel.setName("port_mappings");
final Border panelBorder = BorderFactory
.createTitledBorder(app.getResourceMap().getString("mainFrame.port_mappings.title"));
mappingsPanel.setBorder(panelBorder);
mappingsPanel.add(mappingsTabelPane, "height 100::, span 2, wrap");
mappingsPanel.add(new JButton(actionMap.get(ACTION_REMOVE_MAPPINGS)), "");
mappingsPanel.add(new JButton(actionMap.get(ACTION_UPDATE_PORT_MAPPINGS)), "wrap");
return mappingsPanel;
}
@Action(name = ACTION_UPDATE_ADDRESSES, enabledProperty = PROPERTY_ROUTER_CONNECTED)
public void updateAddresses() {
final IRouter router = app.getRouter();
if (router == null) {
externalIPLabel.setText(app.getResourceMap().getString("mainFrame.router.not_connected"));
internalIPLabel.setText(app.getResourceMap().getString("mainFrame.router.not_connected"));
return;
}
externalIPLabel.setText(app.getResourceMap().getString("mainFrame.router.updating"));
internalIPLabel.setText(app.getResourceMap().getString("mainFrame.router.updating"));
internalIPLabel.setText(router.getInternalHostName());
try {
externalIPLabel.setText(router.getExternalIPAddress());
} catch (final RouterException e) {
externalIPLabel.setText("");
logger.error("Did not get external IP address", e);
}
}
@Action(name = ACTION_CONNECT_ROUTER)
public Task<Void, Void> connectRouter() {
return new ConnectTask(app);
}
@Action(name = ACTION_DISCONNECT_ROUTER)
public void disconnectRouter() {
app.disconnectRouter();
updateAddresses();
updatePortMappings();
}
private void addMapping(final Collection<PortMapping> portMappings) {
final IRouter router = app.getRouter();
if (router == null) {
return;
}
try {
router.addPortMappings(portMappings);
logger.info(portMappings.size() + " port mapping added successfully");
} catch (final RouterException e) {
logger.error("Could not add port mapping", e);
JOptionPane.showMessageDialog(this.getFrame(), "The port mapping could not be added.\n" + e.getMessage(),
"Error adding port mapping", JOptionPane.WARNING_MESSAGE);
}
this.updatePortMappings();
}
@Action(name = ACTION_REMOVE_MAPPINGS, enabledProperty = PROPERTY_MAPPING_SELECTED)
public void removeMappings() {
final Collection<PortMapping> selectedMappings = this.getSelectedPortMappings();
for (final PortMapping mapping : selectedMappings) {
logger.info("Removing mapping " + mapping);
try {
app.getRouter().removeMapping(mapping);
} catch (final RouterException e) {
logger.error("Could not remove port mapping " + mapping, e);
break;
}
logger.info("Mapping was removed successfully: " + mapping);
}
if (selectedMappings.size() > 0) {
updatePortMappings();
}
}
@Action(name = ACTION_DISPLAY_ROUTER_INFO, enabledProperty = PROPERTY_ROUTER_CONNECTED)
public void displayRouterInfo() {
final IRouter router = app.getRouter();
if (router == null) {
logger.warn("Not connected to router, could not get router info");
return;
}
try {
router.logRouterInfo();
} catch (final RouterException e) {
logger.error("Could not get router info", e);
}
}
@Action(name = ACTION_SHOW_ABOUT_DIALOG)
public void showAboutDialog() {
app.show(new AboutDialog(app));
}
@Action(name = ACTION_COPY_INTERNAL_ADDRESS, enabledProperty = PROPERTY_ROUTER_CONNECTED)
public void copyInternalAddress() {
this.copyTextToClipboard(this.internalIPLabel.getText());
}
@Action(name = ACTION_COPY_EXTERNAL_ADDRESS, enabledProperty = PROPERTY_ROUTER_CONNECTED)
public void copyExternalAddress() {
this.copyTextToClipboard(this.externalIPLabel.getText());
}
@Action(name = ACTION_UPDATE_PORT_MAPPINGS, enabledProperty = PROPERTY_ROUTER_CONNECTED)
public void updatePortMappings() {
final IRouter router = app.getRouter();
if (router == null) {
this.tableModel.setMappings(Collections.<PortMapping> emptyList());
return;
}
try {
final Collection<PortMapping> mappings = router.getPortMappings();
logger.info("Found " + mappings.size() + " mappings");
this.tableModel.setMappings(mappings);
} catch (final RouterException e) {
logger.error("Could not get port mappings", e);
}
}
@Action(name = ACTION_USE_PRESET_MAPPING, enabledProperty = PROPERTY_PRESET_MAPPING_SELECTED)
public void addPresetMapping() {
final PortMappingPreset selectedItem = this.portMappingPresets.getSelectedValue();
if (selectedItem != null) {
final String localHostAddress = app.getLocalHostAddress();
if (selectedItem.useLocalhostAsInternalClient() && localHostAddress == null) {
JOptionPane.showMessageDialog(this.getFrame(),
app.getResourceMap().getString("messages.error_getting_localhost_address"),
app.getResourceMap().getString("messages.error"), JOptionPane.ERROR_MESSAGE);
} else {
addMapping(selectedItem.getPortMappings(localHostAddress));
}
}
}
@Action(name = ACTION_CREATE_PRESET_MAPPING)
public void createPresetMapping() {
app.show(new EditPresetDialog(app, new PortMappingPreset()));
}
@Action(name = ACTION_EDIT_PRESET_MAPPING, enabledProperty = PROPERTY_PRESET_MAPPING_SELECTED)
public void editPresetMapping() {
final PortMappingPreset selectedPreset = this.portMappingPresets.getSelectedValue();
app.show(new EditPresetDialog(app, selectedPreset));
}
@Action(name = ACTION_PORTMAPPER_SETTINGS)
public void changeSettings() {
logger.debug("Open Settings dialog");
app.show(new SettingsDialog(app));
}
@Action(name = ACTION_REMOVE_PRESET_MAPPING, enabledProperty = PROPERTY_PRESET_MAPPING_SELECTED)
public void removePresetMapping() {
final PortMappingPreset selectedPreset = this.portMappingPresets.getSelectedValue();
app.getSettings().removePresets(selectedPreset);
}
public void fireConnectionStateChange() {
firePropertyChange(PROPERTY_ROUTER_CONNECTED, !isConnectedToRouter(), isConnectedToRouter());
}
public boolean isConnectedToRouter() {
return app.isConnected();
}
public boolean isMappingSelected() {
return this.isConnectedToRouter() && this.getSelectedPortMappings().size() > 0;
}
public boolean isPresetMappingSelected() {
return this.portMappingPresets.getSelectedValue() != null;
}
/**
* Get the port mappings currently selected in the table.
*
* @return the currently selected port mappings.
*/
public Collection<PortMapping> getSelectedPortMappings() {
final int[] selectedRows = mappingsTable.getSelectedRows();
if (selectedRows == null || selectedRows.length == 0) {
return Collections.emptyList();
}
final Collection<PortMapping> selectedMappings = new ArrayList<>(selectedRows.length);
for (final int rowIndex : selectedRows) {
if (rowIndex >= 0) {
// The table could be sorted, so convert the row index for
// the model
final int modelRowIndex = mappingsTable.convertRowIndexToModel(rowIndex);
final PortMapping mapping = tableModel.getPortMapping(modelRowIndex);
if (mapping != null) {
selectedMappings.add(mapping);
}
}
}
return selectedMappings;
}
private void copyTextToClipboard(final String text) {
final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
logger.trace("Copy text '" + text + "' to clipbord");
clipboard.setContents(new StringSelection(text), new ClipboardOwner() {
@Override
public void lostOwnership(final Clipboard clip, final Transferable contents) {
logger.trace("Lost clipboard ownership");
}
});
}
private class ConnectTask extends Task<Void, Void> {
private final PortMapperApp app;
public ConnectTask(final PortMapperApp app) {
super(app);
this.app = app;
}
@Override
protected Void doInBackground() throws Exception {
logger.trace("Connecting to router...");
app.connectRouter();
message("updateAddresses");
logger.trace("Updating addresses...");
updateAddresses();
message("updatePortMappings");
logger.trace("Updating port mappings...");
updatePortMappings();
logger.trace("done");
return null;
}
@Override
protected void failed(final Throwable cause) {
logger.warn("Could not connect to router: " + cause.getMessage(), cause);
logger.warn("Could not connect to router: " + cause.getMessage());
}
}
}