001package jmri.managers; 002 003import java.util.*; 004import java.util.regex.Matcher; 005import java.util.regex.Pattern; 006 007import javax.annotation.CheckForNull; 008import javax.annotation.Nonnull; 009 010import jmri.Conditional; 011import jmri.ConditionalManager; 012import jmri.InstanceManager; 013import jmri.Logix; 014import jmri.implementation.DefaultConditional; 015import jmri.implementation.SensorGroupConditional; 016import jmri.jmrit.sensorgroup.SensorGroupFrame; 017import jmri.jmrix.internal.InternalSystemConnectionMemo; 018 019/** 020 * Basic Implementation of a ConditionalManager. 021 * <p> 022 * Note that Conditionals always have an associated parent Logix. 023 * <p> 024 * Logix system names must begin with IX, and be followed by a string, usually, 025 * but not always, a number. The system names of Conditionals always begin with 026 * the parent Logix's system name, then there is a capital C and a number. 027 * <p> 028 * Conditional system names are set automatically when the Conditional is 029 * created. All alphabetic characters in a Conditional system name must be upper 030 * case. This is enforced when a new Conditional is created via 031 * {@link jmri.jmrit.beantable.LogixTableAction} 032 * <p> 033 * Conditional user names have specific requirements that are 034 * addressed in the {@link jmri.Conditional} class. 035 * 036 * @author Dave Duchamp Copyright (C) 2007 037 * @author Pete Cresman Copyright (C) 2009 038 */ 039public class DefaultConditionalManager extends AbstractManager<Conditional> 040 implements ConditionalManager { 041 042 public DefaultConditionalManager(InternalSystemConnectionMemo memo) { 043 super(memo); 044 } 045 046 @Override 047 public int getXMLOrder() { 048 return jmri.Manager.CONDITIONALS; 049 } 050 051 @Override 052 public char typeLetter() { 053 return 'X'; 054 } 055 056 /** 057 * Method to create a new Conditional if the Conditional does not exist If 058 * the parent Logix cannot be found, the userName cannot be checked, but the 059 * Conditional is still created. The scenario can happen when a Logix is 060 * loaded from a file after its Conditionals. 061 * 062 * @param systemName properly formatted system name for the new Conditional 063 * @param userName must not be null, use "" instead 064 * @return null if a Conditional with the same systemName or userName 065 * already exists, or if there is trouble creating a new Conditional 066 */ 067 @Override 068 public Conditional createNewConditional(String systemName, String userName) { 069 070 // Check system name 071 if (systemName == null || systemName.length() < 1) { 072 log.error("createNewConditional: systemName is null or empty"); 073 return null; 074 } 075 Conditional c = getBySystemName(systemName); 076 if (c != null) { 077 return null; // Conditional already exists 078 } 079 080 // Get the potential parent Logix 081 Logix lgx = getParentLogix(systemName); 082 if (lgx == null) { 083 log.error("Unable to find the parent logix for condtional '{}'", systemName); 084 return null; 085 } 086 087 // Check the user name 088 if (userName != null && userName.length() > 0) { 089 c = getByUserName(lgx, userName); 090 if (c != null) { 091 return null; // Duplicate user name within the parent Logix 092 } 093 } 094 095 // Conditional does not exist, create a new Conditional 096 if (systemName.startsWith(SensorGroupFrame.logixSysName)) { 097 c = new SensorGroupConditional(systemName, userName); 098 } else { 099 c = new DefaultConditional(systemName, userName); 100 } 101 // save in the maps 102//@ register(c); 103 104 boolean addCompleted = lgx.addConditional(systemName, c); 105 if (!addCompleted) { 106 return null; 107 } 108 109 return c; 110 } 111 112 /** 113 * Do not insist that Conditional user names are unique, 114 * unlike the usual NamedBean support 115 */ 116 @Override 117 protected void handleUserNameUniqueness(jmri.Conditional s) { 118 // eventually needs error checking and reporting 119 } 120 121 /** 122 * Regex patterns to derive the logix system name from the conditional system name 123 * The 3 route patterns deal with Route Logix names that end with a number, 124 * such as Logix RTX123 with Conditional RTX1231T. 125 */ 126 private static final Pattern[] PATTERNS = { 127 Pattern.compile("(.*?)(C\\d+$)"), // Standard IX 128 Pattern.compile("(.*?)([1-9]{1}[ALT]$)"), // LRoute/Route, 1-9 129 Pattern.compile("(.*?)([0-9]{2}[ALT]$)"), // LRoute/Route, 10-99 130 Pattern.compile("(.*?)([0-9]{3}[ALT]$)") // LRoute/Route, 100-999 131 }; 132 133 /** 134 * Parent Logix of Conditionals whose system names do not match the 135 * patterns above, as found by scanning all the Logixes. An entry is 136 * checked each time it is used, since Logixes and their Conditionals can 137 * change without this manager being told. Package-private for testing. 138 */ 139 final Map<String, Logix> nonStandardParents = new java.util.concurrent.ConcurrentHashMap<>(); 140 141 /** 142 * {@inheritDoc} 143 * <p> 144 * Only Conditionals with non-standard system names are remembered here. 145 */ 146 @Override 147 public void forgetParentLogix(Logix x) { 148 nonStandardParents.values().removeIf(lgx -> lgx == x); 149 } 150 151 /** 152 * Parses the Conditional system name to get the parent Logix system name, 153 * then gets the parent Logix, and returns it. For sensor groups, the parent 154 * Logix name is 'SYS'. LRoutes and exported Routes (RTX prefix) require 155 * special logic 156 * 157 * @param name system name of Conditionals 158 * @return the parent Logix or null 159 */ 160 @Override 161 @CheckForNull 162 public Logix getParentLogix(String name) { 163 if (name == null || name.length() < 4) { 164 return null; 165 } 166 167 // Check for standard names 168 for (Pattern r : PATTERNS) { 169 Matcher m = r.matcher(name); 170 if (m.find()) { 171 Logix lgx = InstanceManager.getDefault(jmri.LogixManager.class).getBySystemName(m.group(1)); 172 if (lgx != null) { 173 return lgx; 174 } 175 176 } 177 } 178 179 // Now try non-standard names, first the Logix found last time 180 jmri.LogixManager logixManager = InstanceManager.getDefault(jmri.LogixManager.class); 181 Logix found = nonStandardParents.get(name); 182 if (found != null) { 183 if (logixManager.getBySystemName(found.getSystemName()) == found && hasConditional(found, name)) { 184 return found; 185 } 186 nonStandardParents.remove(name); 187 } 188 189 // then using a brute force scan 190 for (Logix lgx : logixManager.getNamedBeanSet()) { 191 if (hasConditional(lgx, name)) { 192 nonStandardParents.put(name, lgx); 193 return lgx; 194 } 195 } 196 return null; 197 } 198 199 private static boolean hasConditional(Logix lgx, String name) { 200 for (int i = 0; i < lgx.getNumConditionals(); i++) { 201 String cdlName = lgx.getConditionalByNumberOrder(i); 202 if (cdlName.equals(name)) { 203 return true; 204 } 205 } 206 return false; 207 } 208 209 /** 210 * Remove an existing Conditional. Parent Logix must have been deactivated 211 * before invoking this. 212 */ 213 @Override 214 public void deleteConditional(Conditional c) { 215//@ deregister(c); 216 } 217 218 /** 219 * Method to get an existing Conditional. First looks up assuming that name 220 * is a User Name. Note: the parent Logix must be passed in x for user name 221 * lookup. If this fails, or if x == null, looks up assuming that name is a 222 * System Name. If both fail, returns null. 223 * 224 * @param x parent Logix (may be null) 225 * @param name name to look up 226 * @return null if no match found 227 */ 228 @Override 229 @CheckForNull 230 public Conditional getConditional(@CheckForNull Logix x, String name) { 231 if (x != null) { 232 Conditional c = getByUserName(x, name); 233 if (c != null) { 234 return c; 235 } 236 } 237 return getBySystemName(name); 238 } 239 240 @Override 241 @CheckForNull 242 public Conditional getConditional(String name) { 243 Conditional c = getBySystemName(name); 244 if (c == null) { 245 c = getByUserName(name); 246 } 247 return c; 248 } 249 250 /* 251 * Conditional user names are NOT unique. 252 * @param key The user name 253 * @return the conditional or null when not found or a duplicate 254 */ 255 @Override 256 @CheckForNull 257 public Conditional getByUserName(String key) { 258 if (key == null) { 259 return null; 260 } 261 262 Conditional c = null; 263 for (Conditional chkC : getNamedBeanSet()) { 264 if (key.equals(chkC.getUserName())) { 265 if (c == null) { 266 // Save first match 267 c = chkC; 268 continue; 269 } 270 // Found a second match, give up 271 log.warn("Duplicate conditional user names found, key = {}", key); 272 return null; 273 } 274 } 275 return c; 276 } 277 278 @Override 279 @CheckForNull 280 public Conditional getByUserName(@CheckForNull Logix x, @Nonnull String key) { 281 if (x == null) { 282 return null; 283 } 284 for (int i = 0; i < x.getNumConditionals(); i++) { 285 Conditional c = getBySystemName(x.getConditionalByNumberOrder(i)); 286 if (c != null) { 287 String uName = c.getUserName(); 288 if (key.equals(uName)) { 289 return c; 290 } 291 } 292 } 293 return null; 294 } 295 296 @Override 297 @CheckForNull 298 public Conditional getBySystemName(String name) { 299 if (name == null) { 300 return null; 301 } 302 Logix lgx = getParentLogix(name); 303 if (lgx == null) { 304 return null; 305 } 306 return lgx.getConditional(name); 307 } 308 309 /** 310 * Get a list of all Conditional system names with the specified Logix 311 * parent 312 */ 313 @Override 314 public List<String> getSystemNameListForLogix(Logix x) { 315 if (x == null) { 316 return null; 317 } 318 List<String> nameList = new ArrayList<>(); 319 320 for (int i = 0; i < x.getNumConditionals(); i++) { 321 nameList.add(x.getConditionalByNumberOrder(i)); 322 } 323 return nameList; 324 } 325 326 /** 327 * Create a named bean set for conditionals. This requires special logic since conditional 328 * beans are not registered. 329 * @since 4.17.5 330 * @return a sorted named bean set of conditionals. 331 */ 332 @Override 333 @Nonnull 334 public SortedSet<Conditional> getNamedBeanSet() { 335 TreeSet<Conditional> conditionals = new TreeSet<>(new jmri.util.NamedBeanComparator<>()); 336 337 jmri.LogixManager logixManager = InstanceManager.getDefault(jmri.LogixManager.class); 338 for (Logix lgx : logixManager.getNamedBeanSet()) { 339 for (int i = 0; i < lgx.getNumConditionals(); i++) { 340 Conditional cdl = getBySystemName(lgx.getConditionalByNumberOrder(i)); 341 if (cdl != null) { 342 conditionals.add(cdl); 343 } 344 } 345 } 346 return Collections.unmodifiableSortedSet(conditionals); 347 } 348 349 @Override 350 @Nonnull 351 public String getBeanTypeHandled(boolean plural) { 352 return Bundle.getMessage(plural ? "BeanNameConditionals" : "BeanNameConditional"); 353 } 354 355 /** 356 * {@inheritDoc} 357 */ 358 @Override 359 public Class<Conditional> getNamedBeanClass() { 360 return Conditional.class; 361 } 362 363 // --- Conditional Where Used processes --- 364 365 /** 366 * Maintain a list of conditionals that refer to a particular conditional. 367 * @since 4.7.4 368 */ 369 private final HashMap<String, ArrayList<String>> conditionalWhereUsed = new HashMap<>(); 370 371 /** 372 * Return a copy of the entire map. Used by 373 * {@link jmri.jmrit.beantable.LogixTableAction#buildWhereUsedListing} 374 * @since 4.7.4 375 * @return a copy of the map 376 */ 377 @Override 378 public HashMap<String, ArrayList<String>> getWhereUsedMap() { 379 return new HashMap<>(conditionalWhereUsed); 380 } 381 382 /** 383 * Add a conditional reference to the array indicated by the target system name. 384 * @since 4.7.4 385 * @param target The system name for the target conditional 386 * @param reference The system name of the conditional that contains the conditional reference 387 */ 388 @Override 389 public void addWhereUsed(String target, String reference) { 390 if (target == null || target.isEmpty()) { 391 log.error("Invalid target name for addWhereUsed"); 392 return; 393 } 394 if (reference == null || reference.isEmpty()) { 395 log.error("Invalid reference name for addWhereUsed"); 396 return; 397 } 398 399 if (conditionalWhereUsed.containsKey(target)) { 400 ArrayList<String> refList = conditionalWhereUsed.get(target); 401 if (!refList.contains(reference)) { 402 refList.add(reference); 403 conditionalWhereUsed.replace(target, refList); 404 } 405 } else { 406 ArrayList<String> refList = new ArrayList<>(); 407 refList.add(reference); 408 conditionalWhereUsed.put(target, refList); 409 } 410 } 411 412 /** 413 * Get a list of conditional references for the indicated conditional 414 * @since 4.7.4 415 * @param target The target conditional for a conditional reference 416 * @return an ArrayList or null if none 417 */ 418 @Override 419 public ArrayList<String> getWhereUsed(String target) { 420 if (target == null || target.isEmpty()) { 421 log.error("Invalid target name for getWhereUsed"); 422 return null; 423 } 424 return conditionalWhereUsed.get(target); 425 } 426 427 /** 428 * Remove a conditional reference from the array indicated by the target system name. 429 * @since 4.7.4 430 * @param target The system name for the target conditional 431 * @param reference The system name of the conditional that contains the conditional reference 432 */ 433 @Override 434 public void removeWhereUsed(String target, String reference) { 435 if (target == null || target.isEmpty()) { 436 log.error("Invalid target name for removeWhereUsed"); 437 return; 438 } 439 if (reference == null || reference.isEmpty()) { 440 log.error("Invalid reference name for removeWhereUsed"); 441 return; 442 } 443 444 if (conditionalWhereUsed.containsKey(target)) { 445 ArrayList<?> refList = conditionalWhereUsed.get(target); 446 refList.remove(reference); 447 if (refList.isEmpty()) { 448 conditionalWhereUsed.remove(target); 449 } 450 } 451 } 452 453 /** 454 * Display the complete structure, used for debugging purposes. 455 * @since 4.7.4 456 */ 457 @Override 458 public void displayWhereUsed() { 459 log.info("- Display Conditional Where Used "); 460 SortedSet<String> keys = new TreeSet<>(conditionalWhereUsed.keySet()); 461 for (String key : keys) { 462 log.info(" Target: {} ", key); 463 ArrayList<String> refList = conditionalWhereUsed.get(key); 464 for (String ref : refList) { 465 log.info(" Reference: {} ", ref); 466 } 467 } 468 } 469 470 /** 471 * Get the target system names used by this conditional 472 * @since 4.7.4 473 * @param reference The system name of the conditional the refers to other conditionals. 474 * @return a list of the target conditionals 475 */ 476 @Override 477 public ArrayList<String> getTargetList(String reference) { 478 ArrayList<String> targetList = new ArrayList<>(); 479 SortedSet<String> keys = new TreeSet<>(conditionalWhereUsed.keySet()); 480 for (String key : keys) { 481 ArrayList<String> refList = conditionalWhereUsed.get(key); 482 for (String ref : refList) { 483 if (ref.equals(reference)) { 484 targetList.add(key); 485 } 486 } 487 } 488 return targetList; 489 } 490 491 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultConditionalManager.class); 492}