001package jmri.jmrit.logixng.implementation; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.awt.GraphicsEnvironment; 006import java.beans.*; 007import java.io.PrintWriter; 008import java.util.*; 009 010import javax.annotation.Nonnull; 011import javax.annotation.OverridingMethodsMustInvokeSuper; 012 013import jmri.*; 014import jmri.jmrit.logixng.*; 015import jmri.jmrit.logixng.Base.PrintTreeSettings; 016import jmri.jmrit.logixng.Module; 017import jmri.managers.AbstractManager; 018import jmri.util.LoggingUtil; 019import jmri.util.ThreadingUtil; 020import jmri.util.swing.JmriJOptionPane; 021 022import org.apache.commons.lang3.mutable.MutableInt; 023 024/** 025 * Class providing the basic logic of the LogixNG_Manager interface. 026 * 027 * @author Dave Duchamp Copyright (C) 2007 028 * @author Daniel Bergqvist Copyright (C) 2018 029 */ 030public class DefaultLogixNGManager extends AbstractManager<LogixNG> 031 implements LogixNG_Manager { 032 033 034 private final Map<String, Manager<? extends MaleSocket>> _managers = new HashMap<>(); 035 private final Clipboard _clipboard = new DefaultClipboard(); 036 private boolean _isActive = false; 037 private boolean _startLogixNGsOnLoad = true; 038 private boolean _loadDisabled = false; 039 private final List<Runnable> _setupTasks = new ArrayList<>(); 040 041 042 public DefaultLogixNGManager() { 043 // The LogixNGPreferences class may load plugins so we must ensure 044 // it's loaded here. 045 InstanceManager.getDefault(LogixNGPreferences.class); 046 } 047 048 @Override 049 public int getXMLOrder() { 050 return LOGIXNGS; 051 } 052 053 @Override 054 public char typeLetter() { 055 return 'Q'; 056 } 057 058 /** 059 * Test if parameter is a properly formatted system name. 060 * 061 * @param systemName the system name 062 * @return enum indicating current validity, which might be just as a prefix 063 */ 064 @Override 065 public NameValidity validSystemNameFormat(String systemName) { 066 return LogixNG_Manager.validSystemNameFormat( 067 getSubSystemNamePrefix(), systemName); 068// if (systemName.matches(getSubSystemNamePrefix()+"(:AUTO:)?\\d+")) { 069// return NameValidity.VALID; 070// } else { 071// return NameValidity.INVALID; 072// } 073 } 074 075 /** 076 * Method to create a new LogixNG if the LogixNG does not exist. 077 * <p> 078 * Returns null if 079 * a Logix with the same systemName or userName already exists, or if there 080 * is trouble creating a new LogixNG. 081 */ 082 @Override 083 public LogixNG createLogixNG(String systemName, String userName) 084 throws IllegalArgumentException { 085 return createLogixNG(systemName, userName, false); 086 } 087 088 /** 089 * Method to create a new LogixNG if the LogixNG does not exist. 090 * <p> 091 * Returns null if 092 * a Logix with the same systemName or userName already exists, or if there 093 * is trouble creating a new LogixNG. 094 */ 095 @Override 096 public LogixNG createLogixNG(String systemName, String userName, boolean inline) 097 throws IllegalArgumentException { 098 099 // Check that LogixNG does not already exist 100 LogixNG x; 101 if (userName != null && !userName.equals("")) { 102 x = getByUserName(userName); 103 if (x != null) { 104 return null; 105 } 106 } 107 x = getBySystemName(systemName); 108 if (x != null) { 109 return null; 110 } 111 // Check if system name is valid 112 if (this.validSystemNameFormat(systemName) != NameValidity.VALID) { 113 throw new IllegalArgumentException("SystemName " + systemName + " is not in the correct format"); 114 } 115 // LogixNG does not exist, create a new LogixNG 116 x = new DefaultLogixNG(systemName, userName, inline); 117 // save in the maps 118 register(x); 119 120 // Keep track of the last created auto system name 121 updateAutoNumber(systemName); 122 123 return x; 124 } 125 126 @Override 127 public LogixNG createLogixNG(String userName) throws IllegalArgumentException { 128 return createLogixNG(getAutoSystemName(), userName); 129 } 130 131 @Override 132 public LogixNG createLogixNG(String userName, boolean inline) 133 throws IllegalArgumentException { 134 return createLogixNG(getAutoSystemName(), userName, inline); 135 } 136 137 @Override 138 public LogixNG getLogixNG(String name) { 139 LogixNG x = getByUserName(name); 140 if (x != null) { 141 return x; 142 } 143 return getBySystemName(name); 144 } 145 146 @Override 147 public LogixNG getByUserName(String name) { 148 return _tuser.get(name); 149 } 150 151 @Override 152 public LogixNG getBySystemName(String name) { 153 return _tsys.get(name); 154 } 155 156 /** {@inheritDoc} */ 157 @Override 158 public String getBeanTypeHandled(boolean plural) { 159 return Bundle.getMessage(plural ? "BeanNameLogixNGs" : "BeanNameLogixNG"); 160 } 161 162 /** {@inheritDoc} */ 163 @Override 164 public void setLoadDisabled(boolean value) { 165 _loadDisabled = value; 166 } 167 168 /** {@inheritDoc} */ 169 @Override 170 public void startLogixNGsOnLoad(boolean value) { 171 _startLogixNGsOnLoad = value; 172 } 173 174 /** {@inheritDoc} */ 175 @Override 176 public boolean isStartLogixNGsOnLoad() { 177 return _startLogixNGsOnLoad; 178 } 179 180 /** {@inheritDoc} */ 181 @Override 182 public void setupAllLogixNGs() { 183 List<String> errors = new ArrayList<>(); 184 boolean result = true; 185 for (LogixNG logixNG : _tsys.values()) { 186 logixNG.setup(); 187 result = result && logixNG.setParentForAllChildren(errors); 188 } 189 for (Module module : InstanceManager.getDefault(ModuleManager.class).getNamedBeanSet()) { 190 module.setup(); 191 result = result && module.setParentForAllChildren(errors); 192 } 193 _clipboard.setup(); 194 for (Runnable r : _setupTasks) { 195 r.run(); 196 } 197 if (!errors.isEmpty()) { 198 messageDialog("SetupErrorsTitle", errors, null); 199 } 200 checkItemsHaveParents(); 201 202 // Notify listeners that setupAllLogixNGs() is completed. 203 firePropertyChange(PROPERTY_SETUP, false, true); 204 } 205 206 /** 207 * Display LogixNG setup errors when not running in headless mode. 208 * @param titleKey The bundle key for the dialog title. 209 * @param messages A ArrayList of messages that have been localized. 210 * @param helpKey The bundle key for additional information about the errors 211 */ 212 private void messageDialog(String titleKey, List<String> messages, String helpKey) { 213 if (!GraphicsEnvironment.isHeadless() && !Boolean.getBoolean("jmri.test.no-dialogs")) { 214 StringBuilder sb = new StringBuilder("<html>"); 215 messages.forEach(msg -> { 216 sb.append(msg); 217 sb.append("<br>"); 218 }); 219 if (helpKey != null) { 220 sb.append("<br>"); 221 sb.append(Bundle.getMessage(helpKey)); 222 } 223 sb.append("/<html>"); 224 JmriJOptionPane.showMessageDialog(null, 225 sb.toString(), 226 Bundle.getMessage(titleKey), 227 JmriJOptionPane.WARNING_MESSAGE); 228 } 229 } 230 231 private void checkItemsHaveParents(SortedSet<? extends MaleSocket> set, List<MaleSocket> beansWithoutParentList) { 232 for (MaleSocket bean : set) { 233 if (bean.getParent() == null) { 234 beansWithoutParentList.add(bean); 235 } 236 } 237 } 238 239 private void checkItemsHaveParents() { 240 List<MaleSocket> beansWithoutParentList = new ArrayList<>(); 241 checkItemsHaveParents(InstanceManager.getDefault(AnalogActionManager.class).getNamedBeanSet(), beansWithoutParentList); 242 checkItemsHaveParents(InstanceManager.getDefault(DigitalActionManager.class).getNamedBeanSet(), beansWithoutParentList); 243 checkItemsHaveParents(InstanceManager.getDefault(DigitalBooleanActionManager.class).getNamedBeanSet(), beansWithoutParentList); 244 checkItemsHaveParents(InstanceManager.getDefault(StringActionManager.class).getNamedBeanSet(), beansWithoutParentList); 245 checkItemsHaveParents(InstanceManager.getDefault(AnalogExpressionManager.class).getNamedBeanSet(), beansWithoutParentList); 246 checkItemsHaveParents(InstanceManager.getDefault(DigitalExpressionManager.class).getNamedBeanSet(), beansWithoutParentList); 247 checkItemsHaveParents(InstanceManager.getDefault(StringExpressionManager.class).getNamedBeanSet(), beansWithoutParentList); 248 249 if (!beansWithoutParentList.isEmpty()) { 250 List<String> errors = new ArrayList<>(); 251 List<String> msgs = new ArrayList<>(); 252 for (Base b : beansWithoutParentList) { 253 b.setup(); 254 b.setParentForAllChildren(errors); 255 } 256 for (Base b : beansWithoutParentList) { 257 if (b.getParent() == null) { 258 log.error("Item has no parent: {}, {}, {}", 259 b.getSystemName(), 260 b.getUserName(), 261 b.getLongDescription()); 262 msgs.add(Bundle.getMessage("NoParentMessage", 263 b.getSystemName(), 264 b.getUserName(), 265 b.getLongDescription())); 266 267 for (int i=0; i < b.getChildCount(); i++) { 268 if (b.getChild(i).isConnected()) { 269 log.error(" Child: {}, {}, {}", 270 b.getChild(i).getConnectedSocket().getSystemName(), 271 b.getChild(i).getConnectedSocket().getUserName(), 272 b.getChild(i).getConnectedSocket().getLongDescription()); 273 } 274 } 275 log.error(" End Item"); 276 List<String> cliperrors = new ArrayList<String>(); 277 _clipboard.add((MaleSocket) b, cliperrors); 278 } 279 } 280 messageDialog("ParentErrorsTitle", msgs, "NoParentHelp"); 281 } 282 } 283 284 /** {@inheritDoc} */ 285 @Override 286 public void activateAllLogixNGs() { 287 activateAllLogixNGs(true, true); 288 } 289 290 /** {@inheritDoc} */ 291 @Override 292 public void activateAllLogixNGs(boolean runDelayed, boolean runOnSeparateThread) { 293 294 _isActive = true; 295 296 if (_loadDisabled) { 297 for (LogixNG logixNG : _tsys.values()) { 298 logixNG.setEnabled(false); 299 } 300 _loadDisabled = false; 301 } 302 303 // This may take a long time so it must not be done on the GUI thread. 304 // Therefore we create a new thread for this task. 305 Runnable runnable = () -> { 306 307 // Initialize the values of the global variables 308 Set<GlobalVariable> globalVariables = 309 InstanceManager.getDefault(GlobalVariableManager.class) 310 .getNamedBeanSet(); 311 312 for (GlobalVariable gv : globalVariables) { 313 try { 314 gv.initialize(); 315 } catch (JmriException | IllegalArgumentException e) { 316 log.warn("Variable {} could not be initialized", gv.getUserName(), e); 317 } 318 } 319 320 Set<LogixNG> activeLogixNGs = new HashSet<>(); 321 322 // Activate and execute the initialization LogixNGs first. 323 List<LogixNG> initLogixNGs = 324 InstanceManager.getDefault(LogixNG_InitializationManager.class) 325 .getList(); 326 327 for (LogixNG logixNG : initLogixNGs) { 328 logixNG.activate(); 329 if (logixNG.isActive()) { 330 logixNG.registerListeners(); 331 logixNG.execute(false, true); 332 activeLogixNGs.add(logixNG); 333 } else { 334 logixNG.unregisterListeners(); 335 } 336 } 337 338 // Activate and execute all the rest of the LogixNGs. 339 _tsys.values().stream() 340 .sorted() 341 .filter((logixNG) -> !(activeLogixNGs.contains(logixNG))) 342 .forEachOrdered((logixNG) -> { 343 344 logixNG.activate(); 345 346 if (logixNG.isActive()) { 347 logixNG.registerListeners(); 348 logixNG.execute(true, true); 349 } else { 350 logixNG.unregisterListeners(); 351 } 352 }); 353 354 // Clear the startup flag of the LogixNGs. 355 _tsys.values().stream().forEach((logixNG) -> { 356 logixNG.clearStartup(); 357 }); 358 }; 359 360 if (runOnSeparateThread) new Thread(runnable).start(); 361 else runnable.run(); 362 } 363 364 /** {@inheritDoc} */ 365 @Override 366 public void deActivateAllLogixNGs() { 367 for (LogixNG logixNG : _tsys.values()) { 368 logixNG.unregisterListeners(); 369 } 370 _isActive = false; 371 } 372 373 /** {@inheritDoc} */ 374 @Override 375 public boolean isActive() { 376 return _isActive; 377 } 378 379 /** {@inheritDoc} */ 380 @Override 381 public void deleteLogixNG(LogixNG x) { 382 // delete the LogixNG 383 deregister(x); 384 x.dispose(); 385 } 386 387 /** {@inheritDoc} */ 388 @Override 389 public void printTree( 390 PrintTreeSettings settings, 391 PrintWriter writer, 392 String indent, 393 MutableInt lineNumber) { 394 395 printTree(settings, Locale.getDefault(), writer, indent, lineNumber); 396 } 397 398 /** {@inheritDoc} */ 399 @Override 400 public void printTree( 401 PrintTreeSettings settings, 402 Locale locale, 403 PrintWriter writer, 404 String indent, 405 MutableInt lineNumber) { 406 407 for (LogixNG logixNG : getNamedBeanSet()) { 408 if (logixNG.isInline()) continue; 409 logixNG.printTree(settings, locale, writer, indent, "", lineNumber); 410 writer.println(); 411 } 412 413 for (LogixNG logixNG : getNamedBeanSet()) { 414 if (!logixNG.isInline()) continue; 415 logixNG.printTree(settings, locale, writer, indent, "", lineNumber); 416 writer.println(); 417 } 418 InstanceManager.getDefault(ModuleManager.class).printTree(settings, locale, writer, indent, lineNumber); 419 InstanceManager.getDefault(NamedTableManager.class).printTree(locale, writer, indent); 420 InstanceManager.getDefault(GlobalVariableManager.class).printTree(locale, writer, indent); 421 InstanceManager.getDefault(LogixNG_InitializationManager.class).printTree(locale, writer, indent); 422 } 423 424 425 static volatile DefaultLogixNGManager _instance = null; 426 427 @InvokeOnGuiThread // this method is not thread safe 428 static public DefaultLogixNGManager instance() { 429 if (!ThreadingUtil.isGUIThread()) { 430 LoggingUtil.warnOnce(log, "instance() called on wrong thread"); 431 } 432 433 if (_instance == null) { 434 _instance = new DefaultLogixNGManager(); 435 } 436 return (_instance); 437 } 438 439 /** {@inheritDoc} */ 440 @Override 441 public Class<LogixNG> getNamedBeanClass() { 442 return LogixNG.class; 443 } 444 445 /** {@inheritDoc} */ 446 @Override 447 public Clipboard getClipboard() { 448 return _clipboard; 449 } 450 451 /** {@inheritDoc} */ 452 @Override 453 public void registerManager(Manager<? extends MaleSocket> manager) { 454 _managers.put(manager.getClass().getName(), manager); 455 } 456 457 /** {@inheritDoc} */ 458 @Override 459 public Manager<? extends MaleSocket> getManager(String className) { 460 return _managers.get(className); 461 } 462 463 /** 464 * Inform all registered listeners of a vetoable change.If the propertyName 465 * is "CanDelete" ALL listeners with an interest in the bean will throw an 466 * exception, which is recorded returned back to the invoking method, so 467 * that it can be presented back to the user.However if a listener decides 468 * that the bean can not be deleted then it should throw an exception with 469 * a property name of "DoNotDelete", this is thrown back up to the user and 470 * the delete process should be aborted. 471 * 472 * @param p The programmatic name of the property that is to be changed. 473 * "CanDelete" will inquire with all listeners if the item can 474 * be deleted. "DoDelete" tells the listener to delete the item. 475 * @param old The old value of the property. 476 * @throws java.beans.PropertyVetoException If the recipients wishes the 477 * delete to be aborted (see above) 478 */ 479 @OverridingMethodsMustInvokeSuper 480 public void fireVetoableChange(String p, Object old) throws PropertyVetoException { 481 PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, null); 482 for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) { 483 vc.vetoableChange(evt); 484 } 485 } 486 487 /** {@inheritDoc} */ 488 @Override 489 @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER", 490 justification = "LogixNG is a tree that must be deleted recursively") 491 public final void deleteBean(@Nonnull LogixNG logixNG, @Nonnull String property) throws PropertyVetoException { 492 for (int i=logixNG.getNumConditionalNGs()-1; i >= 0; i--) { 493 ConditionalNG child = logixNG.getConditionalNG(i); 494 InstanceManager.getDefault(ConditionalNG_Manager.class).deleteBean(child, property); 495 } 496 497 // throws PropertyVetoException if vetoed 498 fireVetoableChange(property, logixNG); 499 if (property.equals("DoDelete")) { // NOI18N 500 deregister(logixNG); 501 logixNG.dispose(); 502 } 503 } 504 505 /** {@inheritDoc} */ 506 @Override 507 public void registerSetupTask(Runnable task) { 508 _setupTasks.add(task); 509 } 510 511 /** {@inheritDoc} */ 512 @Override 513 public void executeModule(Module module, Object parameter) 514 throws IllegalArgumentException { 515 516 if (module == null) { 517 throw new IllegalArgumentException("The parameter \"module\" is null"); 518 } 519 // Get the parameters for the module 520 Collection<Module.Parameter> parameterNames = module.getParameters(); 521 522 // Ensure that there is only one parameter 523 if (parameterNames.size() != 1) { 524 throw new IllegalArgumentException("The module doesn't take exactly one parameter"); 525 } 526 527 // Get the parameter 528 Module.Parameter param = parameterNames.toArray(Module.Parameter[]::new)[0]; 529 if (!param.isInput()) { 530 throw new IllegalArgumentException("The module's parameter is not an input parameter"); 531 } 532 533 // Set the value of the parameter 534 Map<String, Object> parameters = new HashMap<>(); 535 parameters.put(param.getName(), parameter); 536 537 // Execute the module 538 executeModule(module, parameters); 539 } 540 541 /** {@inheritDoc} */ 542 @Override 543 public void executeModule(Module module, Map<String, Object> parameters) 544 throws IllegalArgumentException { 545 DefaultConditionalNG.executeModule(module, parameters); 546 } 547 548 /** {@inheritDoc} */ 549 @Override 550 public FemaleSocket getErrorHandlingModuleSocket() { 551 return AbstractMaleSocket.getErrorHandlingModuleSocket(); 552 } 553 554 /** {@inheritDoc} */ 555 @Override 556 public boolean isErrorHandlingModuleEnabled() { 557 return AbstractMaleSocket.isErrorHandlingModuleEnabled(); 558 } 559 560 /** 561 * The PropertyChangeListener interface in this class is intended to keep 562 * track of user name changes to individual NamedBeans. It is not completely 563 * implemented yet. In particular, listeners are not added to newly 564 * registered objects. 565 * 566 * @param e the event 567 */ 568 @Override 569 @OverridingMethodsMustInvokeSuper 570 public void propertyChange(PropertyChangeEvent e) { 571 super.propertyChange(e); 572 if (LogixNG.PROPERTY_INLINE.equals(e.getPropertyName())) { 573 // If a LogixNG changes its "inline" state, the number of items 574 // listed in the LogixNG table might change. 575 firePropertyChange("length", null, _beans.size()); 576 } 577 } 578 579 580 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultLogixNGManager.class); 581 582}