package org.openslx.dozmod.gui.control;
import java.awt.Color;
import java.awt.Insets;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EventListener;
import java.util.EventObject;
import java.util.Iterator;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextPane;
import javax.swing.event.EventListenerList;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import org.apache.log4j.Logger;
import org.openslx.bwlp.thrift.iface.NetDirection;
import org.openslx.bwlp.thrift.iface.NetRule;
import org.openslx.dozmod.gui.Gui;
import org.openslx.dozmod.gui.helper.GridManager;
import org.openslx.dozmod.gui.helper.MessageType;
import org.openslx.dozmod.gui.helper.TextChangeListener;
/**
* Widget for advanced configuration options for lectures. This handles
* following options - Network rules - Runscript - USB
*/
public class AdvancedConfigurator extends AdvancedConfiguratorLayout {
private static final long serialVersionUID = -3497629601818983994L;
private final static Logger LOGGER = Logger
.getLogger(AdvancedConfigurator.class);
private String originalRawRuleText = null;
private String originalRunScript = null;
/**
* Character defining how the rules are parsed, e.g. for whitespace \\s
* Example: "8.8.8.8 80 in" would be split in -hostname "8.8.8.8" -port "80"
* -direction "in"
*/
private static final String FIELD_DELIMITER = "\\s";
public AdvancedConfigurator() {
super();
final TextChangeListener docListener = new TextChangeListener() {
@Override
public void changed() {
fireAdvancedConfigurationChangeEvent(new AdvancedConfigurationChangeEvent(
new Object()));
}
};
tpNetworkRules.getDocument().addDocumentListener(docListener);
taRunScript.getDocument().addDocumentListener(docListener);
}
public boolean hasChanged() {
return !originalRawRuleText.equalsIgnoreCase(tpNetworkRules.getText())
|| !originalRunScript.equalsIgnoreCase(taRunScript.getText());
}
/**
* Gets the state of the widget. This will first try to parse the
* tpNetworkRules and taRunScript and build the corresponding
* AdvancedConfiguration Object returned.
*
* @return advanced configuration object composed of the parsed network
* rules as List<NetRule> and the raw runscript text as String
* @see org.openslx.dozmod.gui.control.AdvancedConfigurator.AdvancedConfiguration
*/
public AdvancedConfiguration getState() {
// cleanup the TextPane for network rules if needed
String input = tpNetworkRules.getText().trim();
List<NetRule> rules = parseNetRules(input);
if (rules != null) {
return new AdvancedConfiguration(rules, taRunScript.getText());
}
return null;
}
/**
* Sets the state of this widget to the given AdvancedConfiguration. Basicly
* this sets the content of the text areas to the corresponding network
* rules/runscript as given by the AdvancedConfiguration object
*
* @param config
* AdvancedConfiguration to set the state to
*/
public void setState(final AdvancedConfiguration config) {
// setText() blanks the text area if null is given, so no null checks
originalRawRuleText = decodeNetRulesToText(config.netRulesList);
originalRunScript = config.runScriptText != null ? config.runScriptText
: "";
this.tpNetworkRules.setText(originalRawRuleText);
this.taRunScript.setText(originalRunScript);
}
/**
* "Decodes" the given list of NetRule to a single String. This should be
* used to set the text in the TextPane for the network rules
*
* @param netRulesList
* list of NetRule to decode
* @return String representation of the list of rules
*/
public static String decodeNetRulesToText(final List<NetRule> netRulesList) {
if (netRulesList == null || netRulesList.isEmpty())
return "";
String decodedRules = "";
Iterator<NetRule> it = netRulesList.iterator();
while (it.hasNext()) {
String currentLine = "";
NetRule currentRule = it.next();
// simple test for validity (since this comes from the server it
// should be correct anyways)
if (currentRule.host.isEmpty() || currentRule.port > 65535) {
LOGGER.error("Invalid rule! Ignoring: " + currentRule.host
+ ":" + currentRule.port);
continue;
}
currentLine += currentRule.host + " ";
currentLine += currentRule.port + " ";
currentLine += currentRule.direction.name();
decodedRules += currentLine
+ (it.hasNext() ? System.lineSeparator() : "");
}
return decodedRules;
}
/**
* Parsed the given rawNetRules String to a list of NetRule
*
* @param rawNetRules
* the raw text to be parsed
* @return list of valid net rules parsed from the given rawNetRules,
* invalid ones are not included
* @throws Exception
* when parsing fails
*/
public List<NetRule> parseNetRules(final String rawNetRules) {
if (rawNetRules == null)
return null;
List<NetRule> rulesList = new ArrayList<NetRule>();
if (rawNetRules.isEmpty()) {
return rulesList;
}
// prune the text first
String prunedRawNetRules = rawNetRules.replaceAll("(?m)^\\s*", "");
prunedRawNetRules = prunedRawNetRules.replaceAll("(?m)\\s*$", "");
// split it line by line
List<String> netRules = Arrays.asList(prunedRawNetRules.split("["
+ System.lineSeparator() + "]"));
for (int i = 0; i < netRules.size(); i++) {
final String ruleLine = netRules.get(i);
if (ruleLine == null || ruleLine.isEmpty()) {
netRules.remove(i);
continue;
}
LOGGER.debug("Parsing rule: " + ruleLine);
// split the fields and check if we have 3 as expected.
String[] fields = ruleLine.split(FIELD_DELIMITER);
if (fields.length != 3) {
markText(ruleLine, Color.RED);
// log numbers for fields independently...
LOGGER.debug("Invalid number of fields! Expected 3, got: "
+ fields.length);
Gui.showMessageBox(
"Ungültige Syntax: Nutzen Sie: <host> <port> [in|out]",
MessageType.ERROR, LOGGER, null);
break;
}
// start to check fields one by one from the last to the first ....
// check net direction: accept either 'in' or 'out' (case
// insensitive)
// TODO support combined 'in/out' rules
String ruleDirection = fields[2].toLowerCase();
if (!ruleDirection.matches("^(\\bin\\b|\\bout\\b)$")) {
markText(ruleLine, Color.RED);
LOGGER.debug("Invalid net direction! Expected 'in' or out'. Got: "
+ ruleDirection);
Gui.showMessageBox(
"Ungültige Richtung: nur 'in', 'out' erlaubt!",
MessageType.ERROR, LOGGER, null);
break;
}
// check port: accept if > -2 and < 65535
int port = -2;
try {
port = Integer.parseInt(fields[1]);
} catch (NumberFormatException e) {
LOGGER.error("Could not parse '" + fields[1] + "' to an int.");
}
// TODO: port = -1 for all ports?
if (port <= -2 || port > 65535) {
markText(ruleLine, Color.RED);
LOGGER.debug("Invalid port number! Got: " + port);
Gui.showMessageBox(
"Ungültiges Port! Muss zwischen 0 und 65536 sein.",
MessageType.ERROR, LOGGER, null);
break;
}
// check hostname: bit more to do here
// for IPs and/or resolvable hostnames we make use of java.net's
// InetAddress.getByName() method which checks the validity of
// an IP-Address represented by a string or if the given hostname
// is resolvable. If any of these happen, we have a valid hostname.
// Non-resolvable hostnames are handled differently, see after the
// try/catch-block
InetAddress ruleHost = null;
try {
ruleHost = InetAddress.getByName(fields[0]);
} catch (UnknownHostException e) {
// might be good to see this exception in the log file
// LOGGER.debug("Invalid hostname (java.net): ", e);
}
if (ruleHost == null) {
// either invalid IP-Address or an invalid resolvable hostname
// however it might also be a non-resolvable hostname that would
// be
// valid in the actual pool-rooms, so lets check its syntax
// according to: http://tools.ietf.org/html/rfc1034#section-3.1
LOGGER.debug("Invalid host/IP! Got: " + fields[0]);
if (checkHostnameSimple(fields[0])) {
markText(ruleLine, Color.ORANGE);
if (!Gui.showMessageBox(
"Konnte '"
+ fields[0]
+ "' nicht verifizieren. Wollen Sie es trotzdem verwenden?",
MessageType.WARNING_RETRY, LOGGER, null)) {
break;
}
} else {
markText(ruleLine, Color.RED);
continue;
}
} else {
markText(ruleLine, Color.GREEN);
}
// valid, put it in the list
rulesList.add(new NetRule(NetDirection.valueOf(fields[2]
.toUpperCase()), fields[0], port));
}
if (netRules.size() == rulesList.size()) {
// pruned rules were successfully parsed so they are valid: set the
// textpane to it
tpNetworkRules.setText(prunedRawNetRules);
LOGGER.debug("Success");
return rulesList;
}
return null;
}
/**
* Very simple hostname check for the given String. This will only check for
* some requirements of valid hostnames as stated in
* http://tools.ietf.org/html/rfc1034#section-3.1 To recap: max length of
* the whole hostname must be < 254 ASCII characters, all domain labels
* (between two dots) must be between 1 and 63 chars long and domain labels
* can only contain digits, letters and hyphen. (Note: we also accept
* forward slash to accept subnets!)
*
* @param hostname
* as String to check the syntax of
* @return
*/
private boolean checkHostnameSimple(final String hostname) {
if (hostname.length() > 254) {
Gui.showMessageBox("Hostname ist zu lang!", MessageType.ERROR,
LOGGER, null);
return false;
}
// split by '.' to get domain levels
String[] domainLabels = hostname.split("\\.");
for (String domainLabel : domainLabels) {
if (domainLabel.length() > 63) {
// fail since domain level should be max 63 chars
Gui.showMessageBox("Domain-Ebene '" + domainLabel
+ "' länger als 63 Zeichen!", MessageType.ERROR,
LOGGER, null);
return false;
}
// length is ok, check for invalid characters
for (int i = 0; i < domainLabel.length(); i++) {
Character c = Character.valueOf(domainLabel.charAt(i));
// only accepts numbers, letters, hyphen
// forward slash as a special case to accept subnets...
if (!(Character.isDigit(c) || Character.isLetter(c)
|| c.equals('-') || c.equals('/'))) {
Gui.showMessageBox("Ungültiges Zeichen '" + c
+ "' in hostname!", MessageType.ERROR, LOGGER, null);
return false;
}
}
}
return true;
}
/**
* Searches the given txt within tpNetworkRules and changes its color to the
* given color
*
* @param txt
* text to search within the tpNetworkRules
* @param color
* to set the given txt to
*/
// TODO still buggy with text colors: the marking works fine
// but when trying to input new text, funny things happen
private void markText(final String txt, final Color color) {
SimpleAttributeSet set = new SimpleAttributeSet();
StyleConstants.setForeground(set, color);
StyleConstants.setBold(set, color == Color.red);
StyledDocument doc = tpNetworkRules.getStyledDocument();
try {
for (int pos = 0; pos < doc.getLength() - txt.length() + 1; ++pos) {
String current = doc.getText(pos, txt.length());
if (current.endsWith(System.lineSeparator())) {
current = current.substring(0, current.length() - 1);
}
if (current.equals(txt)) {
doc.setCharacterAttributes(pos, txt.length(), set, true);
break;
}
}
} catch (BadLocationException e) {
LOGGER.error(
"Failed to set '" + txt + "' to color " + color.toString(),
e);
}
// resetting the char attr to what they were before (buggy)
tpNetworkRules.setStyledDocument(doc);
StyleConstants.setForeground(set, Color.WHITE);
StyleConstants.setBold(set, false);
tpNetworkRules.setCharacterAttributes(set, true);
}
/**
* Wrapper class for the advanced configuration information needed since we
* need to return a single object from the runAndReturn routine. This class
* has two members: the list of NetRule(s) (as List<NetRule>) and the
* runScriptText (as String)
*/
public static class AdvancedConfiguration {
public List<NetRule> netRulesList;
public String runScriptText;
public AdvancedConfiguration(List<NetRule> netRulesList,
String runScriptText) {
this.netRulesList = netRulesList;
this.runScriptText = runScriptText;
}
}
/**
* Custom event mechanism to detect changes to the user list (Mostly needed
* for the reactToChange() stuff in LectureDetailsWindow)
*/
protected EventListenerList listenerList = new EventListenerList();
public class AdvancedConfigurationChangeEvent extends EventObject {
private static final long serialVersionUID = -8779550754760035845L;
public AdvancedConfigurationChangeEvent(Object source) {
super(source);
}
}
public interface AdvancedConfigurationChangeEventListener extends
EventListener {
public void stateChanged(AdvancedConfigurationChangeEvent event);
}
public void addAdvancedConfigurationChangeEventListener(
AdvancedConfigurationChangeEventListener listener) {
listenerList.add(AdvancedConfigurationChangeEventListener.class,
listener);
}
public void removeAdvancedConfigurationChangeEventListener(
AdvancedConfigurationChangeEventListener listener) {
listenerList.remove(AdvancedConfigurationChangeEventListener.class,
listener);
}
void fireAdvancedConfigurationChangeEvent(
AdvancedConfigurationChangeEvent evt) {
Object[] listeners = listenerList.getListenerList();
for (int i = 0; i < listeners.length; i++) {
if (listeners[i] == AdvancedConfigurationChangeEventListener.class) {
((AdvancedConfigurationChangeEventListener) listeners[i + 1])
.stateChanged(evt);
}
}
}
}
/**
* Internal layout class for the advanced configurator (to keep it clean even
* for widgets)
*/
class AdvancedConfiguratorLayout extends JPanel {
private static final long serialVersionUID = 648729071828404053L;
private final static String txtNetworkOptionsTitle = "Netzwerk Einstellungen";
private final static String txtNetworkOptionsDesc = "Hier können Sie Firewall-Regeln festlegen mit folgendem Format (TODO)";
private final static String txtNetworkRulesTitle = "Netzwerk-Regeln";
private final static String txtRunScriptTitle = "Run-Skript";
private final static String txtRunScriptDesc = "Ein hier abgelegtes Skript wird beim Start einer Windows-VM automatisch ausgeführt.";
private final JPanel pnlNetworkOptions;
private final JPanel pnlRunScript;
protected final JTextPane tpNetworkRules;
protected final JTextArea taRunScript;
public AdvancedConfiguratorLayout() {
GridManager grid = new GridManager(this, 1, true,
new Insets(5, 5, 5, 5));
// middle panel for network rules
pnlNetworkOptions = new JPanel();
GridManager gridNetworkOptions = new GridManager(pnlNetworkOptions, 1,
true, new Insets(2, 2, 2, 2));
pnlNetworkOptions.setBorder(BorderFactory
.createTitledBorder(txtNetworkOptionsTitle));
tpNetworkRules = new JTextPane();
JScrollPane scpNetworkRules = new JScrollPane(tpNetworkRules,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
pnlNetworkOptions.setBorder(BorderFactory
.createTitledBorder(txtNetworkRulesTitle));
gridNetworkOptions
.add(new WordWrapLabel(txtNetworkOptionsDesc, false, true))
.fill(true, false).expand(true, false);
gridNetworkOptions.nextRow();
gridNetworkOptions.add(scpNetworkRules).fill(true, true)
.expand(true, true);
gridNetworkOptions.finish(false);
// second middle panel for the run script textpane
pnlRunScript = new JPanel();
GridManager gridRunScript = new GridManager(pnlRunScript, 1, true,
new Insets(2, 2, 2, 2));
taRunScript = new JTextArea("", 5, 20);
taRunScript.setLineWrap(true);
taRunScript.setWrapStyleWord(true);
JScrollPane scpRunScript = new JScrollPane(taRunScript,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
pnlRunScript.setBorder(BorderFactory
.createTitledBorder(txtRunScriptTitle));
gridRunScript.add(new WordWrapLabel(txtRunScriptDesc, false, true))
.fill(true, false).expand(true, false);
gridRunScript.nextRow();
gridRunScript.add(scpRunScript).fill(true, true).expand(true, true);
gridRunScript.finish(false);
// build the final grid
grid.add(pnlNetworkOptions).fill(true, true).expand(true, true);
grid.nextRow();
grid.add(pnlRunScript).fill(true, true).expand(true, true);
grid.finish(false);
}
}