001package jmri.jmrix.openlcb; 002 003import java.util.regex.Matcher; 004import java.util.regex.Pattern; 005 006import javax.annotation.CheckReturnValue; 007 008import jmri.NamedBean.BadSystemNameException; 009import jmri.jmrix.can.CanMessage; 010import jmri.jmrix.can.CanReply; 011 012import org.openlcb.EventID; 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016import javax.annotation.Nonnull; 017 018/** 019 * Utilities for handling OpenLCB event messages as addresses. 020 * <p> 021 * OpenLCB event messages have header information, plus an EventID in the data 022 * part. JMRI maps these into address strings. 023 * <p> 024 * Forms: 025 * <dl> 026 * <dt>Full hex string preceeded by "x"<dd>Needs to be pairs of digits: 0123, 027 * not 123 028 * <dt>Full 8 byte ID as pairs separated by "." 029 * </dl> 030 * Note: the {@link #check()} routine does a full, expensive 031 * validity check of the name. All other operations 032 * assume correctness, diagnose some invalid-format strings, but 033 * may appear to successfully handle other invalid forms. 034 * 035 * @author Bob Jacobsen Copyright (C) 2008, 2010, 2018 036 */ 037public class OlcbAddress { 038 039 // groups 040 static final int GROUP_FULL_HEX = 1; // xhhhhhh 041 static final int GROUP_DOT_HEX = 3; // dotted hex form 042 043 static final String singleAddressPattern = "([xX](\\p{XDigit}\\p{XDigit}){1,8})|((\\p{XDigit}?\\p{XDigit}.){7}\\p{XDigit}?\\p{XDigit})"; 044 045 private Matcher hCode = null; 046 047 private Matcher getMatcher() { 048 if (hCode == null) hCode = Pattern.compile("^" + singleAddressPattern + "$").matcher(""); 049 return hCode; 050 } 051 052 String aString; 053 int[] aFrame = null; 054 boolean match = false; 055 056 static final int NODEFACTOR = 100000; 057 058 /** 059 * Construct from OlcbEvent. 060 * 061 * @param e the event ID. 062 */ 063 public OlcbAddress(EventID e) { 064 byte[] contents = e.getContents(); 065 aFrame = new int[contents.length]; 066 int i = 0; 067 for (byte b : contents) { 068 aFrame[i++] = b; 069 } 070 aString = toCanonicalString(); 071 } 072 073 /** 074 * Construct from string without leading system or type letters 075 * @param s hex coded string of address 076 */ 077 public OlcbAddress(String s) { 078 // This is done manually, rather than via regular expressions, for performance reasons. 079 080 // check for leading T, if so convert to numeric form 081 if (s.startsWith("T")) { 082 int from; 083 try { 084 from = Integer.parseInt(s.substring(1)); 085 } catch (NumberFormatException e) { 086 from = 0; 087 } 088 089 int DD = (from-1) & 0x3; 090 int aaaaaa = (( (from-1) >> 2)+1 ) & 0x3F; 091 int AAA = ( (from) >> 8) & 0x7; 092 long event = 0x0101020000FF0000L | (AAA << 9) | (aaaaaa << 3) | (DD << 1); 093 094 s = String.format("%016X;%016X", event, event+1); 095 log.debug(" converted to {}", s); 096 } 097 098 aString = s; 099 100 // numeric address string format 101 if (aString.contains(";")) { 102 // multi-part address; leave match false and aFrame null 103 } else if (aString.contains(".")) { 104 // dotted form, 7 dots 105 String[] terms = s.split("\\."); 106 if (terms.length != 8) { 107 log.debug("unexpected number of terms: {}, address is {}", terms.length, s); 108 } 109 int[] tFrame = new int[terms.length]; 110 try { 111 for (int i = 0; i < terms.length; i++) { 112 tFrame[i] = Integer.parseInt(terms[i], 16); 113 } 114 } catch (NumberFormatException ex) { return; } // leaving the string unparsed 115 aFrame = tFrame; 116 match = true; 117 } else { 118 // assume single hex string - drop leading x if present 119 if (aString.startsWith("x")) aString = aString.substring(1); 120 if (aString.startsWith("X")) aString = aString.substring(1); 121 int len = aString.length() / 2; 122 int[] tFrame = new int[len]; 123 // get the frame data 124 try { 125 for (int i = 0; i < len; i++) { 126 String two = aString.substring(2 * i, 2 * i + 2); 127 tFrame[i] = Integer.parseInt(two, 16); 128 } 129 } catch (NumberFormatException ex) { return; } // leaving the string unparsed 130 aFrame = tFrame; 131 match = true; 132 } 133 } 134 135 /** 136 * Two addresses are equal if they result in the same numeric contents 137 */ 138 @Override 139 public boolean equals(Object r) { 140 if (r == null) { 141 return false; 142 } 143 if (!(r.getClass().equals(this.getClass()))) { 144 return false; 145 } 146 OlcbAddress opp = (OlcbAddress) r; 147 if (opp.aFrame.length != this.aFrame.length) { 148 return false; 149 } 150 for (int i = 0; i < this.aFrame.length; i++) { 151 if (this.aFrame[i] != opp.aFrame[i]) { 152 return false; 153 } 154 } 155 return true; 156 } 157 158 @Override 159 public int hashCode() { 160 int ret = 0; 161 for (int value : this.aFrame) { 162 ret += value; 163 } 164 return ret; 165 } 166 167 public int compare(@Nonnull OlcbAddress opp) { 168 // if neither matched, just do a lexical sort 169 if (!match && !opp.match) return aString.compareTo(opp.aString); 170 171 // match sorts before non-matched 172 if (match && !opp.match) return -1; 173 if (!match && opp.match) return +1; 174 175 // usual case: comparing on content 176 for (int i = 0; i < Math.min(aFrame.length, opp.aFrame.length); i++) { 177 if (aFrame[i] != opp.aFrame[i]) return Integer.signum(aFrame[i] - opp.aFrame[i]); 178 } 179 // check for different length (shorter sorts first) 180 return Integer.signum(aFrame.length - opp.aFrame.length); 181 } 182 183 public CanMessage makeMessage() { 184 CanMessage c = new CanMessage(aFrame, 0x195B4000); 185 c.setExtended(true); 186 return c; 187 } 188 189 /** 190 * Confirm that the address string (provided earlier) is fully 191 * valid. 192 * <p> 193 * This is an expensive call. It's complete-compliance done 194 * using a regular expression. It can reject some 195 * forms that the code will normally handle OK. 196 * @return true if valid, else false. 197 */ 198 public boolean check() { 199 return getMatcher().reset(aString).matches(); 200 } 201 202 boolean match(CanReply r) { 203 // check address first 204 if (r.getNumDataElements() != aFrame.length) { 205 return false; 206 } 207 for (int i = 0; i < aFrame.length; i++) { 208 if (aFrame[i] != r.getElement(i)) { 209 return false; 210 } 211 } 212 // check for event message type 213 if (!r.isExtended()) { 214 return false; 215 } 216 return (r.getHeader() & 0x1FFFF000) == 0x195B4000; 217 } 218 219 boolean match(CanMessage r) { 220 // check address first 221 if (r.getNumDataElements() != aFrame.length) { 222 return false; 223 } 224 for (int i = 0; i < aFrame.length; i++) { 225 if (aFrame[i] != r.getElement(i)) { 226 return false; 227 } 228 } 229 // check for event message type 230 if (!r.isExtended()) { 231 return false; 232 } 233 return (r.getHeader() & 0x1FFFF000) == 0x195B4000; 234 } 235 236 /** 237 * Split a string containing one or more addresses into individual ones. 238 * 239 * @return null if entire string can't be parsed. 240 */ 241 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS", 242 justification = "Documented API, no resources to improve") 243 public OlcbAddress[] split() { 244 // reject strings ending in ";" 245 if (aString.endsWith(";")) { 246 return null; 247 } 248 249 // split string at ";" points 250 String[] pStrings = aString.split(";"); 251 252 OlcbAddress[] retval = new OlcbAddress[pStrings.length]; 253 254 for (int i = 0; i < pStrings.length; i++) { 255 // check validity of each 256 if (pStrings[i].equals("")) { 257 return null; 258 } 259 260 // too expensive to do full regex check here, as this is used a lot in e.g. sorts 261 // if (!getMatcher().reset(pStrings[i]).matches()) return null; 262 263 retval[i] = new OlcbAddress(pStrings[i]); 264 if (!retval[i].match) { 265 return null; 266 } 267 } 268 return retval; 269 } 270 271 public boolean checkSplit() { 272 return (split() != null); 273 } 274 275 int[] elements() { 276 return aFrame; 277 } 278 279 @Override 280 public String toString() { 281 return aString; 282 } 283 284 public String toCanonicalString() { 285 String retval = "x"; 286 for (int value : aFrame) { 287 retval = jmri.util.StringUtil.appendTwoHexFromInt(value, retval); 288 } 289 return retval; 290 } 291 292 /** 293 * Provide as dotted pairs. 294 * @return dotted pair form off string. 295 */ 296 public String toDottedString() { 297 String retval = ""; 298 for (int value : aFrame) { 299 if (!retval.isEmpty()) 300 retval += "."; 301 retval = jmri.util.StringUtil.appendTwoHexFromInt(value, retval); 302 } 303 return retval; 304 } 305 306 public EventID toEventID() { 307 byte[] b = new byte[8]; 308 for (int i = 0; i < Math.min(8, aFrame.length); ++i) b[i] = (byte)aFrame[i]; 309 return new EventID(b); 310 } 311 312 /** 313 * Validates Strings for OpenLCB format. 314 * @param name the system name to validate. 315 * @param locale the locale for a localized exception. 316 * @param prefix system prefix, eg. MT for OpenLcb turnout. 317 * @return the unchanged value of the name parameter. 318 * @throws jmri.NamedBean.BadSystemNameException if provided name is an invalid format. 319 */ 320 @Nonnull 321 public static String validateSystemNameFormat(@Nonnull String name, @Nonnull java.util.Locale locale, 322 @Nonnull String prefix) throws BadSystemNameException { 323 String oAddr = name.substring(prefix.length()); 324 OlcbAddress a = new OlcbAddress(oAddr); 325 OlcbAddress[] v = a.split(); 326 if (v == null) { 327 throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Did not find usable system name: " + name + " to a valid Olcb address"); 328 } 329 switch (v.length) { 330 case 1: 331 case 2: 332 break; 333 default: 334 throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Wrong number of events in address: " + name); 335 } 336 return name; 337 } 338 339 /** 340 * Validates 2 part Hardware Address Strings for OpenLCB format. 341 * @param name the system name to validate. 342 * @param locale the locale for a localized exception. 343 * @param prefix system prefix, eg. MT for OpenLcb turnout. 344 * @return the unchanged value of the name parameter. 345 * @throws jmri.NamedBean.BadSystemNameException if provided name is an invalid format. 346 */ 347 @Nonnull 348 public static String validateSystemNameFormat2Part(@Nonnull String name, @Nonnull java.util.Locale locale, 349 @Nonnull String prefix) throws BadSystemNameException { 350 String oAddr = name.substring(prefix.length()); 351 OlcbAddress a = new OlcbAddress(oAddr); 352 OlcbAddress[] v = a.split(); 353 if (v == null) { 354 throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Did not find usable system name: " + name + " to a valid Olcb address"); 355 } 356 if ( v.length == 2 ) { 357 return name; 358 } 359 throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Address requires 2 Events: " + name); 360 } 361 362 /** 363 * See {@link jmri.NamedBean#compareSystemNameSuffix} for background. 364 * This is a common implementation for OpenLCB Sensors and Turnouts 365 * of the comparison method. 366 * 367 * @param suffix1 1st suffix to compare. 368 * @param suffix2 2nd suffix to compare. 369 * @return true if suffixes match, else false. 370 */ 371 @CheckReturnValue 372 public static int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2) { 373 374 // extract addresses 375 OlcbAddress[] array1 = new OlcbAddress(suffix1).split(); 376 OlcbAddress[] array2 = new OlcbAddress(suffix2).split(); 377 378 // compare on content 379 for (int i = 0; i < Math.min(array1.length, array2.length); i++) { 380 int c = array1[i].compare(array2[i]); 381 if (c != 0) return c; 382 } 383 // check for different length (shorter sorts first) 384 return Integer.signum(array1.length - array2.length); 385 } 386 387 private final static Logger log = LoggerFactory.getLogger(OlcbAddress.class); 388 389} 390 391 392