yetanotheraprsc/yetanotheraprsc-code/.svn/pristine/0a/0a9420474db809e150c00612b80...

898 lines
36 KiB
Plaintext

package org.ka2ddo.yaac.io;
/*
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
* This file is part of YAAC (Yet Another APRS Client).
*
* YAAC 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.
*
* YAAC 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
* and GNU Lesser General Public License along with YAAC. If not,
* see <http://www.gnu.org/licenses/>.
*/
import org.ka2ddo.ax25.AX25Message;
import org.ka2ddo.ax25.Connector;
import org.ka2ddo.yaac.util.Localizer;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
/**
* This class contains all the parameters for configuring a PortConnector in
* this application, and the encoding and decoding code for saving its value
* in the persisted configuration data.
* @author Andrew Pavlin, KA2DDO
*/
public class PortConfig implements Serializable, Comparable<PortConfig> {
private static final long serialVersionUID = -162577201270002899L;
/**
* Empty array for ports that have no supported aliases.
* @see Cfg#digiAliases
*/
public static final String[] NO_ALIASES = new String[0];
/**
* Flag bit indicating this port allows transmitting APRS packets.
* @see Cfg#acceptableProtocolsMask
*/
public static final int PROTOCOL_APRS = 1;
/**
* Flag bit indicating this port allows transmitting OpenTRAC packets.
* @see Cfg#acceptableProtocolsMask
*/
public static final int PROTOCOL_OPENTRAC = 2;
/**
* Flag bit indicating this port allows transmitting AX.25 frames containing traffic of types
* other than APRS or OpenTRAC UI frames.
* @see #PROTOCOL_APRS
* @see #PROTOCOL_OPENTRAC
* @see Cfg#acceptableProtocolsMask
*/
public static final int PROTOCOL_AX25 = 4;
/**
* Highest bit number supported in protocol bit mask.
* @see Cfg#acceptableProtocolsMask
*/
public static final int MAX_PROTOCOL_BIT = 2;
/**
* No waypoint sentences sent to GPS on this port.
*/
public static final int SENTENCE_NONE = 0;
/**
* Send NMEA standard waypoint message back to GPS.
*/
public static final int SENTENCE_GPWPL = 1;
/**
* Send Kenwood-specific waypoint message back to GPS.
*/
public static final int SENTENCE_PKWDWPL = 2;
/**
* Indicates this is a TNC port over HF (low bandwidth, wide geographic coverage).
* @see Cfg#flags
*/
public static final int FLAGS_HF = 1;
/**
* Indicates this is a UDP socket (not TCP).
* @see Cfg#flags
*/
public static final int FLAGS_UDP = 2;
/**
* Indicates that this data should be consumed locally as well as transmitted.
* @see Cfg#flags
*/
public static final int FLAGS_LOCAL = 4;
/**
* Indicates that non-flow-control serial ports should not raise the DTR and RTS signals.
* @see Cfg#flags
*/
public static final int FLAGS_NODTR = 8;
/**
* Indicates that port opening should always retry if it fails, even on initial opening.
* Meant for APRS-IS port driver when used with flaky and/or intermittent Internet connection.
* @see Cfg#flags
*/
public static final int FLAGS_ALWAYS_RETRY = 16;
/**
* Indicates that work-around for strange device features should be used on this port.
* For example, the KISS protocol ports use this to control the work-around for Kenwood
* APRS/TNC radios that could interpret certain characters in a KISS packet as commands
* from the memory programming application and thereby screw up the radio's settings. However,
* other TNCs do not accept the work-around.
* @see Cfg#flags
*/
public static final int FLAGS_USE_WORKAROUND = 32;
/**
* Indicates that this port does not require the port driver to wait for the external
* device to finish processing the last outbound traffic before sending more traffic.
* This is for ports such as Serial_TNC, which could be connected to quarter-duplex devices
* like old TNCs that can't handle receiving another outbound packet from the computer
* while still transmitting the last packet to RF.
* @see Cfg#flags
*/
public static final int FLAGS_PIPELINED = 64;
/**
* When using timeslotting on a TNC port, don't coalesce duplicate packets (such as beacons or
* status frames). This is useful for cases such as meteor-scatter operation. Not all TNC port types
* may support this capability. Note this is coded as a negative value to preserve backwards compatibility
* with YAAC installations configured at an earlier build.
* @see Cfg#flags
*/
public static final int FLAGS_DONT_COALESCE = 128;
/**
* These four bits contain the KISS device ID to be used in KISS frames sent through this port.
* This supports the {@link KissOverTcpConnector} port type when talking to the DireWolf
* software TNC which can support up to 6 audio devices (and therefore up to 6 device IDs in
* KISS frames). Conveniently, since these bits weren't used before, the backwards-compatible
* default KISS device ID is zero.
* @see Cfg#flags
*/
public static final int FLAGS_MASK_KISSPORT = 0xF00;
/**
* This constant gets the number of bits to shift the above {@link #FLAGS_MASK_KISSPORT} bits right
* to put them in the least significant bits of an integer value.
* @see Cfg#flags
*/
public static final int FLAGS_SHIFT_KISSPORT = 8;
/**
* When looking up a TCP/IP network object, the {@link Cfg#deviceName deviceName} is not a fully-qualified domain name (FQDN)
* or numeric IP address of the target, but rather a service instance name for some service supported
* by the port per Internet RFC 6763 (DNS-Based Service Discovery), such that the code needs to do a
* service discovery to get the correct host name prior to using DNS to translate the host name.
* @see Cfg#flags
*/
public static final int FLAGS_IS_SRV_INST_NAME = 0x1000;
/**
* Indicate that {@link Cfg#deviceName} is of a hardware device rather than
* some software concept (like a network port).
*/
public static final int FLAGS_HARDWARE = 0x2000;
/**
* Enumeration identifying all the fields in a Cfg sub-record. Used by port driver classes to support
* configuration transfer by XML to indicate which fields are required and also which are to be suppressed (to
* block duplicate station callsigns, etc.).
* @see PortConnector
* @see ConfigImporter
* @author Andrew Pavlin, KA2DDO
*/
public enum Fields {
/** @see Cfg#enabled */
enabled,
/** @see Cfg#deviceName */
deviceName,
/** @see Cfg#baudRate */
baudRate,
/** @see Cfg#callsign */
callsign,
/** @see Cfg#passcode */
passcode,
/** @see Cfg#filter */
filter,
/** @see Cfg#transmitAllowed */
transmitAllowed,
/** @see Cfg#flowControlled */
flowControlled,
/** @see Cfg#flags */
flags,
/** @see Cfg#digiAliases */
digiAliases,
/** @see Cfg#acceptableProtocolsMask */
acceptableProtocolsMask,
/** @see Cfg#timeslotCycleLength */
timeslotCycleLength,
/** @see Cfg#timeslotOffset */
timeslotOffset,
/** @see Cfg#beaconNames */
beaconNames,
/** @see Cfg#timeslotLength */
timeslotLength }
/**
* For the {@link ConfigImporter}, provide hints of how required but non-transferable config fields
* should be asked for.
* @author Andrew Pavlin, KA2DDO
* @see RequireHints
*/
public enum HintType {
/**
* Just prompt with a plain text edit field.
*/
TEXT,
/**
* Just prompt with a plain text edit field, but only accept uppercase.
*/
TEXT_UC,
/**
* Prompt with a combo box pre-loaded with available serial ports.
*/
SERIAL_PORTS,
/**
* Prompt with a combo box pre-loaded with standard APRS-IS Tier 2 rotator hostnames.
*/
APRSIS_HOSTS,
/**
* Prompt with a combo box pre-loaded with the localhost name.
*/
LOCALHOST,
/**
* Prompt with a file chooser. {@link RequireHints} for this type must be a {@link RequireFile}.
*/
FILE_CHOOSER,
/**
* Prompt with a combo box loaded with a String array obtained from a static
* getDeviceNames() method provided by the {@link PortConnector} subclass.
*/
GENERIC_CALL_FOR_NAMELIST,
/**
* Just prompt with a plain text edit field, but only accept characters legal in a callsign-SSID.
*/
CALLSIGN_SSID
}
/**
* Data structure describing how the ConfigImporter should ask for missing (or
* blanked-out) required port configuration parameters.
* @author Andrew Pavlin, KA2DDO
* @see ConfigImporter
*/
public static class RequireHints {
/**
* The ResourceBundle key for a localized prompt string to display in a dialog box.
*/
public final String promptTag;
/**
* The data entry field type to be used in the prompting dialog box.
*/
public final HintType typeOfDialogPrompt;
/**
* Create a RequireHints object for the specified prompt string and a default text field.
* @param promptTag String key to a ResourceBundle localized prompt text
*/
public RequireHints(String promptTag) {
this.promptTag = promptTag;
this.typeOfDialogPrompt = HintType.TEXT;
}
/**
* Create a RequireHints object for the specified prompt string and the specified type
* of data entry field.
* @param promptTag String key to a ResourceBundle localized prompt text
* @param typeOfDialogPrompt HintType enum for the type of data entry to use
*/
public RequireHints(String promptTag, HintType typeOfDialogPrompt) {
this.promptTag = promptTag;
this.typeOfDialogPrompt = typeOfDialogPrompt;
}
}
/**
* Data structure describing how the ConfigImporter should ask for missing (or
* blanked-out) required port configuration parameters when they are a file in the
* filesystem.
* @author Andrew Pavlin, KA2DDO
* @see ConfigImporter
* @see HintType#FILE_CHOOSER
*/
public static class RequireFile extends RequireHints {
/**
* ResourceBundle key to localized text to display on file chooser's select button.
*/
public final String fileSelectTag;
/**
* Boolean true indicates chooser should select a directory, false for files.
*/
public final boolean isDirectory;
/**
* Create a RequireFile object for the specified prompt string and a file chooser.
* @param promptTag String key to a ResourceBundle localized prompt text
* @param fileSelectTag String text to a ResourceBundle localized text to display on the file chooser's select button
* @param isDirectory boolean true if user must select directory, false for file
*/
public RequireFile(String promptTag, String fileSelectTag, boolean isDirectory) {
super(promptTag, HintType.FILE_CHOOSER);
this.fileSelectTag = fileSelectTag;
this.isDirectory = isDirectory;
}
}
private String displayName = "";
private int portNumber = -1;
/**
* Type of PortConnector used to implement this port, generally defined as a String constant
* named PORT_TYPE on the PortConnector subclass.
*/
public String portType = "";
/**
* Whether port should be opened automatically upon startup.
*/
public boolean enabled = true;
/**
* Port-type-specific configuration parameters for a PortConnector.
* @author Andrew Pavlin, KA2DDO
*/
public static class Cfg implements Serializable, Comparable<Cfg> {
private static final long serialVersionUID = 885437494458611609L;
/**
* The device name or network host name/address associated with this port.
*/
public String deviceName = "";
/**
* The baud rate used for the port, if needed, or port number for TCP and UDP socket connections.
*/
public int baudRate = 0;
/**
* The amateur radio station callsign associated with this port, if needed.
* Also used for weather station model name.
*/
public String callsign = "";
/**
* The authentication passcode associated with this port, if needed.
*/
public String passcode = "";
/**
* Any filter expression associated with this port, if needed.
*/
public String filter = "";
/**
* Indicates whether messages can be transmitted from YAAC via this port.
*/
public boolean transmitAllowed;
/**
* Indicates whether flow control is enabled on this port.
*/
public boolean flowControlled;
/**
* A collection of flag bits indicating other attributes of the port configuration.
* @see #FLAGS_HF
* @see #FLAGS_UDP
* @see #FLAGS_LOCAL
* @see #FLAGS_NODTR
* @see #FLAGS_ALWAYS_RETRY
* @see #FLAGS_USE_WORKAROUND
* @see #FLAGS_PIPELINED
* @see #FLAGS_DONT_COALESCE
* @see #FLAGS_MASK_KISSPORT
* @see #FLAGS_IS_SRV_INST_NAME
* @see #FLAGS_HARDWARE
*/
public int flags;
/**
* Array of digipeat alias Strings for which this port will digipeat (if transmitAllowed is true).
*
* @see #transmitAllowed
*/
public String[] digiAliases = NO_ALIASES;
/**
* Bit mask or enum number of protocols that can be transmitted through this port. Only meaningful for
* ports with CAP_XMT_PACKET_DATA set (for protocol bitmask) or CAP_WAYPOINT_SENDER (for enum).
* @see Connector#CAP_XMT_PACKET_DATA
* @see Connector#CAP_WAYPOINT_SENDER
* @see #PROTOCOL_APRS
* @see #PROTOCOL_OPENTRAC
* @see #PROTOCOL_AX25
* @see #SENTENCE_NONE
* @see #SENTENCE_GPWPL
* @see #SENTENCE_PKWDWPL
*/
public int acceptableProtocolsMask = PROTOCOL_APRS;
/**
* Number of seconds in a timeslot cycle, which will be aligned to UTC and the Unix epoch time
* (if some weird prime number is used). Negative values means timeslotting is disabled, but is
* preserving the last-used cycle length in case it gets re-enabled.
*/
public int timeslotCycleLength = -120;
/**
* Number of seconds since the start of a cycle when this port is allowed to transmit.
* This should be an integer multiple of {@link #timeslotLength} greater than or equal to
* zero, but less than the {@link #timeslotCycleLength}.
*/
public int timeslotOffset = 0;
/**
* Number of seconds in a timeslot. This should be a positive integer fraction of the
* {@link #timeslotCycleLength}. Zero or negative means that once a station enters its
* timeslot, it can transmit until its queue for the port is empty, rather than having
* to stop because another station's timeslot has started.
*/
public int timeslotLength = 10;
/**
* Names of beacon instances to send through this port (zero-length array means only default beacon).
* @see BeaconData
* @see BeaconData#MYCALL
* @see BeaconData#beaconName
*/
public String[] beaconNames = new String[0];
/**
* Make a deep copy of this Cfg object.
*
* @return duplicate Cfg
*/
public Cfg dup() {
Cfg cfg = new Cfg();
cfg.deviceName = deviceName;
cfg.baudRate = baudRate;
cfg.callsign = callsign;
cfg.passcode = passcode;
cfg.transmitAllowed = transmitAllowed;
cfg.digiAliases = new String[digiAliases.length];
System.arraycopy(digiAliases, 0, cfg.digiAliases, 0, digiAliases.length);
cfg.filter = filter;
cfg.flowControlled = flowControlled;
cfg.acceptableProtocolsMask = acceptableProtocolsMask;
cfg.timeslotCycleLength = timeslotCycleLength;
cfg.timeslotOffset = timeslotOffset;
cfg.timeslotLength = timeslotLength;
cfg.flags = flags;
cfg.beaconNames = Arrays.copyOf(beaconNames, beaconNames.length);
return cfg;
}
/**
* Compares this object with the specified object for order. Returns a
* negative integer, zero, or a positive integer as this object is less
* than, equal to, or greater than the specified object.
*
* @param o the object to be compared.
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*
* @throws ClassCastException if the specified object's type prevents it
* from being compared to this object.
*/
public int compareTo(Cfg o) {
return deviceName.compareTo(o.deviceName);
}
/**
* Indicates whether some other object is "equal to" this one.
*
* @param obj the reference object with which to compare.
* @return {@code true} if this object is the same as the obj
* argument; {@code false} otherwise.
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof Cfg) {
Cfg other = (Cfg)obj;
return other.deviceName.equals(deviceName) &&
other.baudRate == baudRate &&
other.callsign.equals(callsign) &&
other.passcode.equals(passcode) &&
other.transmitAllowed == transmitAllowed &&
Arrays.equals(digiAliases, other.digiAliases) &&
other.filter.equals(filter) &&
other.flowControlled == flowControlled &&
other.acceptableProtocolsMask == acceptableProtocolsMask &&
other.timeslotCycleLength == timeslotCycleLength &&
other.timeslotOffset == timeslotOffset &&
other.timeslotLength == timeslotLength &&
other.flags == flags &&
Arrays.equals(other.beaconNames, beaconNames);
}
return false;
}
/**
* Returns a hash code value for the object.
*
* @return a hash code value for this object.
* @see #equals(Object)
*/
@Override
public int hashCode() {
return deviceName.hashCode();
}
/**
* Returns a string representation of the object.
* @return a string representation of the object.
*/
@Override
public String toString() {
return "PortConfig.Cfg[" + deviceName + ',' + baudRate + ',' + callsign + ',' + (transmitAllowed ? "xmt" : "") +
",digi=" + Arrays.toString(digiAliases) + ',' + (null != filter && filter.length() > 0 ? "filt=\"" + filter + '"' : "") +
(flags != 0 ? ",flags=0x" + Integer.toHexString(flags) : "") +
(timeslotCycleLength > 0 ? ",ts=" + timeslotOffset + '(' + timeslotLength + ")/" + timeslotCycleLength : "") + ']';
}
}
private final HashMap<String, Cfg> variations = new HashMap<String, Cfg>(2);
/**
* Get the port-type-specific configuration parameters for the currently specified port type.
* @return type-specific Cfg record
*/
public final Cfg current() {
Cfg cfg;
if ((cfg = variations.get(portType)) == null) {
variations.put(portType, cfg = new Cfg());
}
return cfg;
}
/**
* Get the port-type-specific configuration parameters for a specific port type.
* @param portType the name of the port type whose configuration parameters should be obtained
* @return type-specific Cfg record (created if it did not previously exist)
*/
public final Cfg specific(String portType) {
Cfg cfg;
if ((cfg = variations.get(portType)) == null) {
variations.put(portType, cfg = new Cfg());
}
return cfg;
}
/**
* Create a new PortConfig record with a new port instance identifier.
*/
public PortConfig() {}
/**
* Allocate a new port number for this PortConfig object.
*/
public void assignPortNumber() {
portNumber = PortManager.getNextAvailablePortNumber();
displayName = "Port" + portNumber;
}
/**
* Craete a PortConfig record with the specified port instance identifier.
* @param displayName port identifier name to use
*/
public PortConfig(String displayName) {
this.displayName = displayName;
portNumber = Integer.parseInt(displayName.substring(4).trim());
}
/**
* Indicates whether some other object is "equal to" this one.
*
* @param obj the reference object with which to compare.
* @return <code>true</code> if this object is the same as the obj
* argument; <code>false</code> otherwise.
* @see #hashCode()
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof PortConfig) {
PortConfig o = (PortConfig)obj;
return displayName.equals(o.displayName);
}
return false;
}
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hashtables such as those provided by
* <code>java.util.Hashtable</code>.
*
* @return a hash code value for this object.
* @see #equals(Object)
*/
@Override
public int hashCode() {
return displayName.hashCode();
}
/**
* Get the name by which this port will be identified.
* @return String port name
*/
public String getDisplayName() {
return displayName;
}
/**
* Get the arbitrary sequence port number assigned to this configuration record.
* @return port number
*/
public int getPortNumber() {
return portNumber;
}
/**
* Returns a string representation of the object.
*
* @return a string representation of the object.
*/
@Override
public String toString() {
Cfg cfg = current();
return "PortConfig[" + displayName + '>' + portType + ',' + cfg.deviceName + ',' + cfg.baudRate + ',' + cfg.callsign + ']';
}
/**
* Make a deep copy of this PortConfig object.
* @return duplicate PortConfig
*/
public PortConfig dup() {
PortConfig cfg = new PortConfig(displayName);
cfg.portType = portType;
cfg.enabled = enabled;
for (Map.Entry<String, Cfg> entry : variations.entrySet()) {
cfg.variations.put(entry.getKey(), entry.getValue().dup());
}
return cfg;
}
/**
* Do a deep copy of this PortConfig object into another object..
* @param dest destination PortConfig
*/
public void copyInto(PortConfig dest) {
dest.portType = portType;
dest.enabled = enabled;
dest.variations.clear();
dest.variations.put(portType, variations.get(portType));
}
/**
* Compares this object with the specified object for order. Returns a
* negative integer, zero, or a positive integer as this object is less
* than, equal to, or greater than the specified object.
*
* @param other the object to be compared.
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*
* @throws ClassCastException if the specified object's type prevents it
* from being compared to this object.
*/
public int compareTo(PortConfig other) {
int diff = portType.compareTo(other.portType);
if (0 == diff) {
diff = current().compareTo(other.current());
}
return diff;
}
/**
* Write this PortConfig object to Java Preferences on the Ports sub-node of the
* specified Preferences node.
* @param root Preferences node to use to store the Ports sub-node
*/
public void writeToPreferences(Preferences root) {
// write or overwrite preferences
Preferences portNode = root.node("Ports");
portNode.put(displayName, generatePrefsString());
try {
portNode.flush();
} catch (BackingStoreException e) {
e.printStackTrace(System.out);
}
}
/**
* <p>Generate the encoded String for storing the PortConfig into Java Preferences.
* The preferences String consists of multiple expressions separated by semicolon ";"
* characters. The first expression (required) will have the following format:</p>
* <code><i>portType</i>,<i>deviceName</i>,<i>baudRateOrPortNumber</i>,<i>callsign</i>,<i>passcode</i>,<i>transmitEnabled</i>[,<i>digipeatAlias[,...]]</i></code>
* <p>where <i>deviceName</i> and <i>passcode</i> are escaped to be legitimate text in an XML tag body,
* <i>transmitEnabled</i> is either "true" or "false", and there can be any number of digipeat aliases
* in the syntax of display-format AX.25 callsign-SSID values.</p>
* <p>The second expression is a APRS-IS filter expression.</p>
* <p>The third expression is "true" or "false" for whether this port is enabled for operation.</p>
* <p>The fourth expression is "true" or "false" for whether flow control (or something overloading
* the meaning of this field) should be enabled or not.</p>
* <p>The fifth expression is a decimal number of the value of the {@link Cfg#acceptableProtocolsMask} bitmask.</p>
* <p>The sixth expression is two decimal numbers separated by a slash "/" character representing
* the timeslot offset (relative to the top of the cycle) in seconds and the length of the timeslot cycle in seconds.
* Negative cycle length indicates that timeslotting is not used, but the values are preserved
* in case the user wants to turn timeslotting back on.</p>
* <p>The seventh expression is a decimal number of the value of the {@link Cfg#flags} bitmask.
* Bit meanings are portType-specific.</p>
* <p>The eighth expression is a pipe "|" separated list of names of {@link BeaconData}
* definitions that should be transmitted through this port. Only meaningful for ports
* capable of transmitting APRS or OpenTRAC packets.</p>
* @return encoded String
* @see #decodePreferenceValue(String)
*/
public String generatePrefsString() {
Cfg cfg = current();
StringBuilder b = new StringBuilder(portType).append(',').append(passcodeEncode(cfg.deviceName)).append(',').
append(cfg.baudRate).append(',').append(cfg.callsign).append(',').append(passcodeEncode(cfg.passcode)).append(',').append(cfg.transmitAllowed);
for (String r : cfg.digiAliases) {
b.append(',').append(r);
}
b.append(';').append(cfg.filter).append(';').append(enabled).append(';').append(cfg.flowControlled).append(';').append(cfg.acceptableProtocolsMask);
b.append(';').append(cfg.timeslotOffset).append('/').append(cfg.timeslotCycleLength).append('/').append(cfg.timeslotLength);
b.append(';').append(cfg.flags).append(';');
for (int i = 0; i < cfg.beaconNames.length; i++) {
if (0 != i) {
b.append('|');
}
b.append(cfg.beaconNames[i]);
}
return b.toString();
}
/**
* Encode the passcode so characters that would break XML or our field concatenation are escaped.
* @param passcode String containing passcode
* @return escaped passcode
*/
private static String passcodeEncode(String passcode) {
boolean needsEscape = false;
int len = passcode.length();
for (int i = 0; i < len; i++) {
char ch = passcode.charAt(i);
if (ch < ' ' || ch >= '\u007F' || '&' == ch || '"' == ch || '\\' == ch || ',' == ch || ';' == ch) {
needsEscape = true;
break;
}
}
if (needsEscape) {
StringBuilder b = new StringBuilder(len + 20);
for (int i = 0; i < len; i++) {
char ch = passcode.charAt(i);
if (ch < ' ' || ch >= '\u007F' || '&' == ch || '"' == ch || '\\' == ch || ',' == ch || ';' == ch) {
b.append("\\u").append(Integer.toHexString(0x10000 + (int)ch).substring(1));
} else {
b.append(ch);
}
}
passcode = b.toString();
}
return passcode;
}
/**
* Delete this PortConfig from the Java Preferences backing store.
* @param root Preferences node used to store the Ports sub-node
*/
public void removeFromPreferences(Preferences root) {
// remove this PortConfig from the Preferences
Preferences portNode = root.node("Ports");
portNode.remove(displayName);
}
/**
* Read a PortConfig object from Java Preferences.
* @param root Preferences node used to store the Ports sub-node
* @param displayName the String port identifier name to read
* @return a populated PortConfig object for the specified name, or null if no such record
* exists in the specified node of Preferences
*/
public static PortConfig readFromPreferences(Preferences root, String displayName) {
PortConfig config = null;
Preferences portNode = root.node("Ports");
String values = portNode.get(displayName, null);
if (values != null) {
config = new PortConfig(displayName);
config.decodePreferenceValue(values);
}
return config;
}
/**
* Decode the storage format used to store a PortConfig in Java Preferences into
* this PortConfig object.
* @param values String of encoded values
* @see #generatePrefsString()
*/
public void decodePreferenceValue(String values) {
String[] prePostFilter = AX25Message.split(values, ';');
String[] tk = AX25Message.split(prePostFilter[0], ',');
portType = tk[0];
if (portType.equals("I-Gate")) {
portType = "APRS-IS";
}
Cfg cfg = current();
cfg.deviceName = passcodeDecode(tk[1]);
if (tk.length >= 3) {
cfg.baudRate = Integer.parseInt(tk[2]);
if (tk.length >= 4) {
cfg.callsign = tk[3];
if (tk.length >= 5) {
cfg.passcode = passcodeDecode(tk[4]);
if (tk.length >= 6) {
cfg.transmitAllowed = Boolean.parseBoolean(tk[5]);
if (tk.length >= 7) {
cfg.digiAliases = new String[tk.length - 6];
System.arraycopy(tk, 6, cfg.digiAliases, 0, cfg.digiAliases.length);
}
}
}
}
}
if (prePostFilter.length > 1) {
cfg.filter = prePostFilter[1];
if (prePostFilter.length > 2) {
if (prePostFilter[2].length() > 0) {
enabled = Boolean.parseBoolean(prePostFilter[2]);
}
if (prePostFilter.length > 3) {
if (prePostFilter[3].length() > 0) {
cfg.flowControlled = Boolean.parseBoolean(prePostFilter[3]);
}
if (prePostFilter.length > 4) {
if (prePostFilter[4].length() > 0) {
cfg.acceptableProtocolsMask = Integer.parseInt(prePostFilter[4]);
}
if (prePostFilter.length > 5) {
if (prePostFilter[5].length() > 0) {
String[] timeslotParams = AX25Message.split(prePostFilter[5], '/');
cfg.timeslotOffset = Integer.parseInt(timeslotParams[0]);
cfg.timeslotCycleLength = Integer.parseInt(timeslotParams[1]);
if (timeslotParams.length >= 3) {
cfg.timeslotLength = Integer.parseInt(timeslotParams[2]);
} else {
cfg.timeslotLength = 0; // for backwards compatibility
}
}
if (prePostFilter.length > 6) {
if (prePostFilter[6].length() > 0) {
cfg.flags = Integer.parseInt(prePostFilter[6]);
}
if (prePostFilter.length > 7) {
if (prePostFilter[7].length() > 0) {
cfg.beaconNames = AX25Message.split(prePostFilter[7], '\u001E');
//TODO: temporary repair until damage from build#107 is removed
String defaultBeaconDisplayName = Localizer.getMsg("configure.tab.Beacon.DefaultBeaconName");
for (int i = 0; i < cfg.beaconNames.length; i++) {
if (cfg.beaconNames[i].equals(defaultBeaconDisplayName)) {
cfg.beaconNames[i] = BeaconData.MYCALL;
}
}
} else {
cfg.beaconNames = new String[0];
}
}
}
}
}
}
}
}
}
/**
* Decode the passcode so characters that would break XML or our field concatenation are un-escaped.
* @param passcode String containing escaped passcode
* @return unescaped passcode
*/
private static String passcodeDecode(String passcode) {
boolean hasEscape = false;
int len = passcode.length();
for (int i = 0; i < len; i++) {
char ch = passcode.charAt(i);
if ('\\' == ch) {
hasEscape = true;
break;
}
}
if (hasEscape) {
StringBuilder b = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char ch = passcode.charAt(i);
if ('\\' == ch) {
if (passcode.charAt(i+1) != 'u' || i + 6 > len) {
throw new IllegalArgumentException("invalid encoded string \"" + passcode + '"');
}
ch = (char)Integer.parseInt(passcode.substring(i+2,i+6), 16);
i += 5;
}
b.append(ch);
}
passcode = b.toString();
}
return passcode;
}
}