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