001package jmri.jmrix.grapevine; 002 003import java.util.Locale; 004import javax.annotation.Nonnull; 005import jmri.Manager.NameValidity; 006import java.util.regex.Matcher; 007import java.util.regex.Pattern; 008import jmri.Manager; 009import jmri.NamedBean; 010import org.slf4j.Logger; 011import org.slf4j.LoggerFactory; 012 013/** 014 * Utility Class supporting parsing and testing of Grapevine addresses. 015 * <p> 016 * Multiple address formats are supported: 017 * <ul> 018 * <li>Gtnnnxxx where: G is the (multichar) system connection prefix, 019 * t is the type code: 'T' for turnouts, 'S' for sensors, 'H' for signal 020 * heads and 'L' for lights; 021 * nnn is the node address (1-127); xxx is a bit number of the input or 022 * output bit (001-999)</li> 023 * <li>Gtnnnxxx = (node address x 1000) + bit number.<br> 024 * Examples: GT1002 (node address 1, bit 2), G1S1003 (node address 1, bit 3), 025 * GL11234 (node address 11, bit234)</li> 026 * <li>Gtnnnaxxxx where: t is the type code, 'T' for turnouts, 'S' for 027 * sensors, 'H' for signal heads and 'L' for lights; nnn is the node address of the 028 * input or output bit (1-127); xxxx is a bit number of the input or output bit 029 * (1-2048); a is a subtype-specific letter: 030 * <ul> 031 * <li>'B' for a bit number (e.g. GT12B3 is a shorter form of GT12003) 032 * <li>'a' is for advanced serial occupancy sensors (only valid t = S) 033 * <li>'m' is for advanced serial motion sensors (only valid t = S) 034 * <li>'pattern' is for parallel sensors (only valid t = S) 035 <li>'s' is for serial occupancy sensors (only valid t = S) 036 * </ul> 037 * Examples: GT1B2 (node address 1, bit 2), G1S1B3 (node address 1, bit 3), 038 * G22L11B234 (node address 11, bit 234) 039 * </li> 040 * </ul> 041 * 042 * @author Dave Duchamp, Copyright (C) 2004 043 * @author Bob Jacobsen, Copyright (C) 2006, 2007, 2008 044 */ 045public class SerialAddress { 046 047 public SerialAddress() { 048 } 049 050 /** 051 * Regular expression used to parse Turnout names. 052 * <p> 053 * Groups: 054 * <ul> 055 * <li> - System letter/prefix (not captured in regex) 056 * <li>1 - Type letter 057 * <li>2 - suffix, if of nnnAnnn form 058 * <li>3 - node number in nnnAnnn form 059 * <li>4 - address type in nnnAnnn form 060 * <li>5 - bit number in nnnAnnn form 061 * <li>6 - combined number in nnnnnn form 062 * </ul> 063 */ 064 static final String turnoutRegex = "^\\w\\d*(T)(?:((\\d++)(B)(\\d++))|(\\d++))$"; 065 static volatile Pattern turnoutPattern = null; 066 067 static Pattern getTurnoutPattern() { 068 // defer compiling pattern until used, instead of at loading time 069 if (turnoutPattern == null) { 070 turnoutPattern = Pattern.compile(turnoutRegex); 071 } 072 return turnoutPattern; 073 } 074 075 /** 076 * Regular expression used to parse Light names. 077 * <p> 078 * Groups: 079 * <ul> 080 * <li> - System letter/prefix (not captured in regex) 081 * <li>1 - Type letter 082 * <li>2 - suffix, if of nnnAnnn form 083 * <li>3 - node number in nnnAnnn form 084 * <li>4 - address type in nnnAnnn form 085 * <li>5 - bit number in nnnAnnn form 086 * <li>6 - combined number in nnnnnn form 087 * </ul> 088 */ 089 static final String lightRegex = "^\\w\\d*(L)(?:((\\d++)(B)(\\d++))|(\\d++))$"; 090 static volatile Pattern lightPattern = null; 091 092 static Pattern getLightPattern() { 093 // defer compiling pattern until used, instead of at loading time 094 if (lightPattern == null) { 095 lightPattern = Pattern.compile(lightRegex); 096 } 097 return lightPattern; 098 } 099 100 /** 101 * Regular expression used to parse SignalHead names. 102 * <p> 103 * Groups: 104 * <ul> 105 * <li> - System letter/prefix (not captured in regex) 106 * <li>1 - Type letter 107 * <li>2 - suffix, if of nnnAnnn form 108 * <li>3 - node number in nnnAnnn form 109 * <li>4 - address type in nnnAnnn form 110 * <li>5 - bit number in nnnAnnn form 111 * <li>6 - combined number in nnnnnn form 112 * </ul> 113 */ 114 static final String headRegex = "^\\w\\d*(H)(?:((\\d++)(B)(\\d++))|(\\d++))$"; 115 static volatile Pattern headPattern = null; 116 117 static Pattern getHeadPattern() { 118 // defer compiling pattern until used, instead of at loading time 119 if (headPattern == null) { 120 headPattern = Pattern.compile(headRegex); 121 } 122 return headPattern; 123 } 124 125 /** 126 * Regular expression used to parse Sensor names. 127 * <p> 128 * Groups: 129 * <ul> 130 * <li> - System letter/prefix (not captured in regex) 131 * <li>1 - Type letter 132 * <li>2 - suffix, if of nnnAnnn form 133 * <li>3 - node number in nnnAnnn form 134 * <li>4 - address type in nnnAnnn form 135 * <li>5 - bit number in nnnAnnn form 136 * <li>6 - combined number in nnnnnn form 137 * </ul> 138 */ 139 static final String sensorRegex = "^\\w\\d*(S)(?:((\\d++)([BbAaMmPpSs])(\\d++))|(\\d++))$"; 140 static volatile Pattern sensorPattern = null; 141 142 static Pattern getSensorPattern() { 143 // defer compiling pattern until used, instead of at loading time 144 if (sensorPattern == null) { 145 sensorPattern = Pattern.compile(sensorRegex); 146 } 147 return sensorPattern; 148 } 149 150 /** 151 * Regular expression used to parse from any type of name. 152 * <p> 153 * Groups: 154 * <ul> 155 * <li> - System letter/prefix (not captured in regex) 156 * <li>1 - Type letter 157 * <li>2 - suffix, if of nnnAnnn form 158 * <li>3 - node number in nnnAnnn form 159 * <li>4 - address type in nnnAnnn form 160 * <li>5 - bit number in nnnAnnn form 161 * <li>6 - combined number in nnnnnn form 162 * </ul> 163 */ 164 static final String allRegex = "^\\w\\d*([SHLT])(?:((\\d++)([BbAaMmPpSs])(\\d++))|(\\d++))$"; 165 static volatile Pattern allPattern = null; 166 167 static Pattern getAllPattern() { 168 // defer compiling pattern until used, instead of at loading time 169 if (allPattern == null) { 170 allPattern = Pattern.compile(allRegex); 171 } 172 return allPattern; 173 } 174 175 /** 176 * Parse for secondary letters. 177 * 178 * @param type Secondary letter from message 179 * @return offset for type letter, or -1 if none 180 */ 181 static int typeOffset(String type) { 182 switch (type.toUpperCase().charAt(0)) { 183 case 'B': 184 return 0; 185 case 'A': 186 return SerialNode.offsetA; 187 case 'M': 188 return SerialNode.offsetM; 189 case 'P': 190 return SerialNode.offsetP; 191 case 'S': 192 return SerialNode.offsetS; 193 default: 194 return -1; 195 } 196 } 197 198 /** 199 * Public static method to parse a system name and return the Serial Node. 200 * 201 * @param systemName system name. 202 * @param tc system connection traffic controller. 203 * @return 'NULL' if illegal systemName format or if the node is not found 204 */ 205 public static SerialNode getNodeFromSystemName(String systemName, SerialTrafficController tc) { 206 // validate the System Name leader characters 207 Matcher matcher = getAllPattern().matcher(systemName); 208 if (!matcher.matches()) { 209 // here if an illegal format 210 log.error("illegal system name format in getNodeFromSystemName: {}", systemName); 211 return null; 212 } 213 214 // start decode 215 int ua; 216 if (matcher.group(6) != null) { 217 // This is a Gitnnxxx address 218 int num = Integer.parseInt(matcher.group(6)); 219 if (num > 0) { 220 ua = num / 1000; 221 } else { 222 log.error("invalid value in system name: {}", systemName); 223 return null; 224 } 225 } else { 226 ua = Integer.parseInt(matcher.group(3)); 227 } 228 return (SerialNode) tc.getNodeFromAddress(ua); 229 } 230 231 /** 232 * Public static method to parse a system name and return the bit number. 233 * Notes: Bits are numbered from 1. 234 * 235 * @param systemName system name. 236 * @param prefix unused. 237 * @return 0 if an error is found 238 */ 239 public static int getBitFromSystemName(String systemName, String prefix) { 240 // validate the System Name leader characters 241 Matcher matcher = getAllPattern().matcher(systemName); 242 if (!matcher.matches()) { 243 // here if an illegal format 244 log.error("illegal system name format in getBitFromSystemName: {} prefix: {}", systemName, prefix, new Exception("traceback")); 245 return 0; 246 } 247 248 // start decode 249 int n; 250 if (matcher.group(6) != null) { 251 // name in be Gitnnxxx format 252 int num = Integer.parseInt(matcher.group(6)); 253 if (num > 0) { 254 n = num % 1000; 255 } else { 256 log.error("invalid value in system name: {}", systemName); 257 return 0; 258 } 259 } else { 260 // This is a Gitnnaxxxx address 261 n = Integer.parseInt(matcher.group(5)); 262 } 263 return n; 264 } 265 266 /** 267 * Public static method to parse a system name to fetch the node number. 268 * <p> 269 * Note: Nodes are numbered from 1. 270 * 271 * @param systemName system name. 272 * @param prefix unused. 273 * @return node number. If an error is found, returns -1 274 */ 275 public static int getNodeAddressFromSystemName(String systemName, String prefix) { 276 // validate the System Name leader characters 277 Matcher matcher = getAllPattern().matcher(systemName); 278 if (!matcher.matches()) { 279 // here if an illegal format 280 log.error("illegal system name format in getNodeAddressFromSystemName: {}", systemName); 281 return -1; 282 } 283 284 // start decode 285 int ua; 286 if (matcher.group(6) != null) { 287 // This is a Gitnnxxx address 288 int num = Integer.parseInt(matcher.group(6)); 289 if (num > 0) { 290 ua = num / 1000; 291 } else { 292 log.error("invalid value in system name: {}", systemName); 293 return -1; 294 } 295 } else { 296 ua = Integer.parseInt(matcher.group(3)); 297 log.debug("node ua: {}", ua); 298 } 299 return ua; 300 } 301 302 /** 303 * Validate a system name. 304 * 305 * @param name the name to validate 306 * @param manager the manager requesting validation 307 * @param locale the locale for user messages 308 * @return the name, unchanged 309 * @throws IllegalArgumentException if name is not valid 310 * @see Manager#validateSystemNameFormat(java.lang.String, java.util.Locale) 311 */ 312 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = "SLF4J_FORMAT_SHOULD_BE_CONST", 313 justification = "Passing Locale.ENGLISH Bundle exception text before stack trace") 314 static String validateSystemNameFormat(String name, Manager<?> manager, Locale locale) { 315 name = manager.validateSystemNamePrefix(name, locale); 316 Pattern pattern; 317 switch (manager.typeLetter()) { 318 case 'L': 319 pattern = getLightPattern(); 320 break; 321 case 'T': 322 pattern = getTurnoutPattern(); 323 break; 324 case 'H': 325 pattern = getHeadPattern(); 326 break; 327 case 'S': 328 pattern = getSensorPattern(); 329 break; 330 default: 331 // validateSystemNamePrefix did not validate correctly, so log a stack trace 332 NamedBean.BadSystemNameException ex = new NamedBean.BadSystemNameException( 333 Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidUnknownType", name), 334 Bundle.getMessage(locale, "SystemNameInvalidUnknownType", name)); 335 log.error(ex.getMessage(), ex); // second parameter logs stack trace 336 throw ex; 337 } 338 Matcher matcher = pattern.matcher(name); 339 if (!matcher.matches()) { 340 throw new NamedBean.BadSystemNameException( 341 Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameFailedRegex", name, pattern.pattern()), 342 Bundle.getMessage(locale, "InvalidSystemNameFailedRegex", name, pattern.pattern())); 343 } 344 int node; 345 int bit; 346 if (matcher.group(6) != null) { 347 // Gitnnxxx format 348 int num = Integer.parseInt(matcher.group(6)); 349 node = num / 1000; 350 bit = num % 1000; 351 } else { 352 // Gitnnaxxxx address 353 node = Integer.parseInt(matcher.group(3)); 354 bit = Integer.parseInt(matcher.group(5)); 355 } 356 // check values 357 if ((node < 1) || (node > 127)) { 358 throw new NamedBean.BadSystemNameException( 359 Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidNode", name, bit, 1, 127), 360 Bundle.getMessage(locale, "SystemNameInvalidNode", name, bit, 1, 127)); 361 } 362 363 // check bit numbers 364 if (manager.typeLetter() != 'S') { 365 if (!((bit >= 101 && bit <= 124) 366 || (bit >= 201 && bit <= 224) 367 || (bit >= 301 && bit <= 324) 368 || (bit >= 401 && bit <= 424))) { 369 throw new NamedBean.BadSystemNameException( 370 Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameFailedRegex", name, pattern.pattern()), 371 Bundle.getMessage(locale, "InvalidSystemNameFailedRegex", name, pattern.pattern())); 372 } 373 } else { 374 // sort on subtype 375 String subtype = matcher.group(4); 376 if (null == subtype) { // no subtype, just look at total 377 if ((bit < 1) || (bit > 224)) { 378 throw new NamedBean.BadSystemNameException( 379 Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidBit", name, bit, 1, 224), 380 Bundle.getMessage(locale, "SystemNameInvalidBit", name, bit, 1, 224)); 381 } 382 } else { 383 switch (subtype.toUpperCase()) { 384 case "A": 385 // advanced serial occ 386 if ((bit < 1) || (bit > 24)) { 387 throw new NamedBean.BadSystemNameException( 388 Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidBit", name, bit, 1, 24), 389 Bundle.getMessage(locale, "SystemNameInvalidBit", name, bit, 1, 24)); 390 } 391 break; 392 case "M": 393 // advanced serial motion 394 if ((bit < 1) || (bit > 24)) { 395 throw new NamedBean.BadSystemNameException( 396 Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidBit", name, bit, 1, 24), 397 Bundle.getMessage(locale, "SystemNameInvalidBit", name, bit, 1, 24)); 398 } 399 break; 400 case "S": 401 // old serial 402 if ((bit < 1) || (bit > 24)) { 403 throw new NamedBean.BadSystemNameException( 404 Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidBit", name, bit, 1, 24), 405 Bundle.getMessage(locale, "SystemNameInvalidBit", name, bit, 1, 24)); 406 } 407 break; 408 case "P": 409 // parallel 410 if ((bit < 1) || (bit > 96)) { 411 throw new NamedBean.BadSystemNameException( 412 Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidBit", name, bit, 1, 96), 413 Bundle.getMessage(locale, "SystemNameInvalidBit", name, bit, 1, 96)); 414 } 415 break; 416 default: 417 break; 418 } 419 } 420 } 421 return name; 422 } 423 424 /** 425 * Public static method to validate system name format. 426 * Logging of handled cases no higher than WARN. 427 * 428 * @param systemName name to check 429 * @param type expected device type letter 430 * @param prefix system connection prefix from memo 431 * @return 'true' if system name has a valid format, else returns 'false' 432 */ 433 public static NameValidity validSystemNameFormat(@Nonnull String systemName, char type, String prefix) { 434 // validate the System Name leader characters 435 Matcher matcher = getAllPattern().matcher(systemName); 436 if (!matcher.matches()) { 437 // here if an illegal format, e.g. another system letter 438 // which happens all the time due to how proxy managers work 439 return NameValidity.INVALID; 440 } 441 if (matcher.group(1).charAt(0) != type) { // notice we skipped the multichar prefix 442 log.warn("type in {} does not match type {}", systemName, type); 443 return NameValidity.INVALID; 444 } 445 Pattern p; 446 if (type == 'L') { 447 p = getLightPattern(); 448 } else if (type == 'T') { 449 p = getTurnoutPattern(); 450 } else if (type == 'H') { 451 p = getHeadPattern(); 452 } else if (type == 'S') { 453 p = getSensorPattern(); 454 } else { 455 log.error("cannot match type in {}, which is unexpected", systemName); 456 return NameValidity.INVALID; 457 } 458 459 // check format 460 matcher = p.matcher(systemName); 461 if (!matcher.matches()) { 462 // here if cannot parse specifically (only accepts GTnnn or GTnnnB 463 log.debug("invalid system name format: {} for type {}", systemName, type); 464 return NameValidity.INVALID; 465 } 466 467 // check for the two different formats 468 int node; 469 int bit; 470 if (matcher.group(6) != null) { 471 // name in be Gitnnxxx format 472 int num = Integer.parseInt(matcher.group(6)); 473 if (num > 0) { 474 node = num / 1000; 475 bit = num % 1000; 476 } else { 477 log.debug("invalid value in system name: {}", systemName); 478 return NameValidity.INVALID; 479 } 480 } else { 481 // This is a Gitnnaxxxx address, get values 482 node = Integer.parseInt(matcher.group(3)); 483 bit = Integer.parseInt(matcher.group(5)); 484 } 485 486 // check values 487 if ((node < 1) || (node > 127)) { 488 log.debug("invalid node number {} in {}", node, systemName); 489 return NameValidity.INVALID; 490 } 491 492 // check bit numbers 493 if ((type == 'T') || (type == 'H') || (type == 'L')) { 494 if (!((bit >= 101 && bit <= 124) 495 || (bit >= 201 && bit <= 224) 496 || (bit >= 301 && bit <= 324) 497 || (bit >= 401 && bit <= 424))) { 498 log.debug("invalid bit number {} in {}", bit, systemName); 499 return NameValidity.INVALID; 500 } 501 } else { 502 assert type == 'S'; // see earlier decoding 503 // sort on subtype 504 String subtype = matcher.group(4); 505 if (subtype == null) { // no subtype, just look at total 506 if ((bit < 1) || (bit > 224)) { 507 log.debug("invalid bit number {} in {}", bit, systemName); 508 return NameValidity.INVALID; 509 } else { 510 return NameValidity.VALID; 511 } 512 } 513 subtype = subtype.toUpperCase(); 514 if (subtype.equals("A")) { // advanced serial occ 515 if ((bit < 1) || (bit > 24)) { 516 log.debug("invalid bit number {} in {}", bit, systemName); 517 return NameValidity.INVALID; 518 } 519 } else if (subtype.equals("M")) { // advanced serial motion 520 if ((bit < 1) || (bit > 24)) { 521 log.debug("invalid bit number {} in {}", bit, systemName); 522 return NameValidity.INVALID; 523 } 524 } else if (subtype.equals("S")) { // old serial 525 if ((bit < 1) || (bit > 24)) { 526 log.debug("invalid bit number {} in {}", bit, systemName); 527 return NameValidity.INVALID; 528 } 529 } else if (subtype.equals("P")) { // parallel 530 if ((bit < 1) || (bit > 96)) { 531 log.debug("invalid bit number {} in {}", bit, systemName); 532 return NameValidity.INVALID; 533 } 534 } 535 } 536 537 // finally, return VALID 538 return NameValidity.VALID; 539 } 540 541 /** 542 * Public static method to validate system name for configuration. 543 * 544 * @param systemName system name to validate. 545 * @param type bean type, S, T or L. 546 * @param tc system connection traffic controller. 547 * @return 'true' if system name has a valid meaning in current configuration, 548 * else returns 'false'. 549 * 550 */ 551 public static boolean validSystemNameConfig(String systemName, char type, SerialTrafficController tc) { 552 String prefix = tc.getSystemConnectionMemo().getSystemPrefix(); 553 if (validSystemNameFormat(systemName, type, prefix) != NameValidity.VALID) { 554 // No point in trying if a valid system name format is not present 555 log.debug("invalid system name {}", systemName); 556 return false; 557 } 558 SerialNode node = getNodeFromSystemName(systemName, tc); 559 if (node == null) { 560 log.warn("invalid system name {}; no such node", systemName); 561 // The node indicated by this system address is not present 562 return false; 563 } 564 int bit = getBitFromSystemName(systemName, prefix); 565 if ((type == 'T') || (type == 'L')) { 566 if ((bit <= 0) || (bit > SerialNode.outputBits[node.nodeType])) { 567 // The bit is not valid for this defined Serial node 568 log.warn("invalid system name {}; bad output bit number {} > {}", 569 systemName, bit, SerialNode.outputBits[node.nodeType]); 570 return false; 571 } 572 } else if (type == 'S') { 573 if ((bit <= 0) || (bit > SerialNode.inputBits[node.nodeType])) { 574 // The bit is not valid for this defined Serial node 575 log.warn("invalid system name {}; bad input bit number {} > {}", 576 systemName, bit, SerialNode.inputBits[node.nodeType]); 577 return false; 578 } 579 } else { 580 log.error("Invalid type specification in validSystemNameConfig call"); 581 return false; 582 } 583 // System name has passed all tests 584 return true; 585 } 586 587 /** 588 * Public static method to convert any format system name for the alternate 589 * format (nnBnn). 590 * <p> 591 * If the supplied system name does not have a valid format, 592 * or if there is no representation in the alternate naming scheme, 593 * an empty string is returned. 594 * @param systemName system name to convert. 595 * @param prefix system prefix. 596 * @return alternate string. 597 */ 598 public static String convertSystemNameToAlternate(String systemName, String prefix) { 599 // ensure that input system name has a valid format 600 if (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID) { 601 // No point in normalizing if a valid system name format is not present 602 return ""; 603 } 604 605 Matcher matcher = getAllPattern().matcher(systemName); 606 matcher.matches(); // known to work, just need values 607 // check format 608 if (matcher.group(6) != null) { 609 int num = Integer.parseInt(matcher.group(6)); 610 return prefix + matcher.group(1) + (num / 1000) + "B" + (num % 1000); 611 } else { 612 int node = Integer.parseInt(matcher.group(3)); 613 int bit = Integer.parseInt(matcher.group(5)); 614 return prefix + matcher.group(1) + node + "B" + bit; 615 } 616 } 617 618 /** 619 * Public static method to normalize a system name 620 * <p> 621 * This routine is used to ensure that each system name is uniquely linked 622 * to one bit, by removing extra zeros inserted by the user. 623 * <p> 624 * If the supplied system name does not have a valid format, an empty string 625 * is returned. Otherwise a normalized name is returned in the same format 626 * as the input name. 627 * @param systemName system name to normalize. 628 * @param prefix system prefix. 629 * @return normalized string. 630 */ 631 public static String normalizeSystemName(String systemName, String prefix) { 632 // ensure that input system name has a valid format 633 try { 634 if (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID) { 635 // No point in normalizing if a valid system name format is not present 636 return ""; 637 } 638 639 Matcher matcher = getAllPattern().matcher(systemName); 640 matcher.matches(); // known to work, just need values 641 642 // check format 643 if (matcher.group(6) != null) { 644 int num = Integer.parseInt(matcher.group(6)); 645 return prefix + matcher.group(1) + num; 646 } else { 647 // there are alternate forms... 648 int offset = typeOffset(matcher.group(4)); 649 int node = Integer.parseInt(matcher.group(3)); 650 int bit = Integer.parseInt(matcher.group(5)); 651 return prefix + matcher.group(1) + (node * 1000 + bit + offset); 652 } 653 } catch(java.lang.StringIndexOutOfBoundsException sobe){ 654 throw new IllegalArgumentException("Invalid System Name Format: " + systemName); 655 } 656 } 657 658 private final static Logger log = LoggerFactory.getLogger(SerialAddress.class); 659 660}