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, final 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 switch (type) { 447 case 'L': 448 p = getLightPattern(); 449 break; 450 case 'T': 451 p = getTurnoutPattern(); 452 break; 453 case 'H': 454 p = getHeadPattern(); 455 break; 456 case 'S': 457 p = getSensorPattern(); 458 break; 459 default: 460 log.error("cannot match type in {}, which is unexpected", systemName); 461 return NameValidity.INVALID; 462 } 463 464 // check format 465 matcher = p.matcher(systemName); 466 if (!matcher.matches()) { 467 // here if cannot parse specifically (only accepts GTnnn or GTnnnB 468 log.debug("invalid system name format: {} for type {}", systemName, type); 469 return NameValidity.INVALID; 470 } 471 472 // check for the two different formats 473 int node; 474 int bit; 475 if (matcher.group(6) != null) { 476 // name in be Gitnnxxx format 477 int num = Integer.parseInt(matcher.group(6)); 478 if (num > 0) { 479 node = num / 1000; 480 bit = num % 1000; 481 } else { 482 log.debug("invalid value in system name: {}", systemName); 483 return NameValidity.INVALID; 484 } 485 } else { 486 // This is a Gitnnaxxxx address, get values 487 node = Integer.parseInt(matcher.group(3)); 488 bit = Integer.parseInt(matcher.group(5)); 489 } 490 491 // check values 492 if ((node < 1) || (node > 127)) { 493 log.debug("invalid node number {} in {}", node, systemName); 494 return NameValidity.INVALID; 495 } 496 497 // check bit numbers 498 if ((type == 'T') || (type == 'H') || (type == 'L')) { 499 if (!((bit >= 101 && bit <= 124) 500 || (bit >= 201 && bit <= 224) 501 || (bit >= 301 && bit <= 324) 502 || (bit >= 401 && bit <= 424))) { 503 log.debug("invalid bit number {} in {}", bit, systemName); 504 return NameValidity.INVALID; 505 } 506 } else { // type MUST be 'S', see earlier logic to get Pattern 507 // sort on subtype 508 String subtype = matcher.group(4); 509 if (subtype == null) { // no subtype, just look at total 510 if ((bit < 1) || (bit > 224)) { 511 log.debug("invalid bit number {} in {}", bit, systemName); 512 return NameValidity.INVALID; 513 } else { 514 return NameValidity.VALID; 515 } 516 } 517 subtype = subtype.toUpperCase(); 518 if (subtype.equals("A")) { // advanced serial occ 519 if ((bit < 1) || (bit > 24)) { 520 log.debug("invalid bit number {} in {}", bit, systemName); 521 return NameValidity.INVALID; 522 } 523 } else if (subtype.equals("M")) { // advanced serial motion 524 if ((bit < 1) || (bit > 24)) { 525 log.debug("invalid bit number {} in {}", bit, systemName); 526 return NameValidity.INVALID; 527 } 528 } else if (subtype.equals("S")) { // old serial 529 if ((bit < 1) || (bit > 24)) { 530 log.debug("invalid bit number {} in {}", bit, systemName); 531 return NameValidity.INVALID; 532 } 533 } else if (subtype.equals("P")) { // parallel 534 if ((bit < 1) || (bit > 96)) { 535 log.debug("invalid bit number {} in {}", bit, systemName); 536 return NameValidity.INVALID; 537 } 538 } 539 } 540 541 // finally, return VALID 542 return NameValidity.VALID; 543 } 544 545 /** 546 * Public static method to validate system name for configuration. 547 * 548 * @param systemName system name to validate. 549 * @param type bean type, S, T or L. 550 * @param tc system connection traffic controller. 551 * @return 'true' if system name has a valid meaning in current configuration, 552 * else returns 'false'. 553 * 554 */ 555 public static boolean validSystemNameConfig(String systemName, char type, SerialTrafficController tc) { 556 String prefix = tc.getSystemConnectionMemo().getSystemPrefix(); 557 if (validSystemNameFormat(systemName, type, prefix) != NameValidity.VALID) { 558 // No point in trying if a valid system name format is not present 559 log.debug("invalid system name {}", systemName); 560 return false; 561 } 562 SerialNode node = getNodeFromSystemName(systemName, tc); 563 if (node == null) { 564 log.warn("invalid system name {}; no such node", systemName); 565 // The node indicated by this system address is not present 566 return false; 567 } 568 int bit = getBitFromSystemName(systemName, prefix); 569 if ((type == 'T') || (type == 'L')) { 570 if ((bit <= 0) || (bit > SerialNode.outputBits[node.nodeType])) { 571 // The bit is not valid for this defined Serial node 572 log.warn("invalid system name {}; bad output bit number {} > {}", 573 systemName, bit, SerialNode.outputBits[node.nodeType]); 574 return false; 575 } 576 } else if (type == 'S') { 577 if ((bit <= 0) || (bit > SerialNode.inputBits[node.nodeType])) { 578 // The bit is not valid for this defined Serial node 579 log.warn("invalid system name {}; bad input bit number {} > {}", 580 systemName, bit, SerialNode.inputBits[node.nodeType]); 581 return false; 582 } 583 } else { 584 log.error("Invalid type specification in validSystemNameConfig call"); 585 return false; 586 } 587 // System name has passed all tests 588 return true; 589 } 590 591 /** 592 * Public static method to convert any format system name for the alternate 593 * format (nnBnn). 594 * <p> 595 * If the supplied system name does not have a valid format, 596 * or if there is no representation in the alternate naming scheme, 597 * an empty string is returned. 598 * @param systemName system name to convert. 599 * @param prefix system prefix. 600 * @return alternate string. 601 */ 602 public static String convertSystemNameToAlternate(String systemName, String prefix) { 603 // ensure that input system name has a valid format 604 if (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID) { 605 // No point in normalizing if a valid system name format is not present 606 return ""; 607 } 608 609 Matcher matcher = getAllPattern().matcher(systemName); 610 matcher.matches(); // known to work, just need values 611 // check format 612 if (matcher.group(6) != null) { 613 int num = Integer.parseInt(matcher.group(6)); 614 return prefix + matcher.group(1) + (num / 1000) + "B" + (num % 1000); 615 } else { 616 int node = Integer.parseInt(matcher.group(3)); 617 int bit = Integer.parseInt(matcher.group(5)); 618 return prefix + matcher.group(1) + node + "B" + bit; 619 } 620 } 621 622 /** 623 * Public static method to normalize a system name 624 * <p> 625 * This routine is used to ensure that each system name is uniquely linked 626 * to one bit, by removing extra zeros inserted by the user. 627 * <p> 628 * If the supplied system name does not have a valid format, an empty string 629 * is returned. Otherwise a normalized name is returned in the same format 630 * as the input name. 631 * @param systemName system name to normalize. 632 * @param prefix system prefix. 633 * @return normalized string. 634 */ 635 public static String normalizeSystemName(String systemName, String prefix) { 636 // ensure that input system name has a valid format 637 try { 638 if (validSystemNameFormat(systemName, systemName.charAt(prefix.length()), prefix) != NameValidity.VALID) { 639 // No point in normalizing if a valid system name format is not present 640 return ""; 641 } 642 643 Matcher matcher = getAllPattern().matcher(systemName); 644 matcher.matches(); // known to work, just need values 645 646 // check format 647 if (matcher.group(6) != null) { 648 int num = Integer.parseInt(matcher.group(6)); 649 return prefix + matcher.group(1) + num; 650 } else { 651 // there are alternate forms... 652 int offset = typeOffset(matcher.group(4)); 653 int node = Integer.parseInt(matcher.group(3)); 654 int bit = Integer.parseInt(matcher.group(5)); 655 return prefix + matcher.group(1) + (node * 1000 + bit + offset); 656 } 657 } catch(java.lang.StringIndexOutOfBoundsException sobe){ 658 throw new IllegalArgumentException("Invalid System Name Format: " + systemName); 659 } 660 } 661 662 private final static Logger log = LoggerFactory.getLogger(SerialAddress.class); 663 664}