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.size() > 0) { 196 messageDialog("SetupErrorsTitle", errors, null); 197 } 198 checkItemsHaveParents(); 199 } 200 201 /** 202 * Display LogixNG setup errors when not running in headless mode. 203 * @param titleKey The bundle key for the dialog title. 204 * @param messages A ArrayList of messages that have been localized. 205 * @param helpKey The bundle key for additional information about the errors 206 */ 207 private void messageDialog(String titleKey, List<String> messages, String helpKey) { 208 if (!GraphicsEnvironment.isHeadless() && !Boolean.getBoolean("jmri.test.no-dialogs")) { 209 StringBuilder sb = new StringBuilder("<html>"); 210 messages.forEach(msg -> { 211 sb.append(msg); 212 sb.append("<br>"); 213 }); 214 if (helpKey != null) { 215 sb.append("<br>"); 216 sb.append(Bundle.getMessage(helpKey)); 217 } 218 sb.append("/<html>"); 219 JmriJOptionPane.showMessageDialog(null, 220 sb.toString(), 221 Bundle.getMessage(titleKey), 222 JmriJOptionPane.WARNING_MESSAGE); 223 } 224 } 225 226 private void checkItemsHaveParents(SortedSet<? extends MaleSocket> set, List<MaleSocket> beansWithoutParentList) { 227 for (MaleSocket bean : set) { 228 if (((Base)bean).getParent() == null) beansWithoutParentList.add(bean); 229 } 230 } 231 232 private void checkItemsHaveParents() { 233 List<MaleSocket> beansWithoutParentList = new ArrayList<>(); 234 checkItemsHaveParents(InstanceManager.getDefault(AnalogActionManager.class).getNamedBeanSet(), beansWithoutParentList); 235 checkItemsHaveParents(InstanceManager.getDefault(DigitalActionManager.class).getNamedBeanSet(), beansWithoutParentList); 236 checkItemsHaveParents(InstanceManager.getDefault(DigitalBooleanActionManager.class).getNamedBeanSet(), beansWithoutParentList); 237 checkItemsHaveParents(InstanceManager.getDefault(StringActionManager.class).getNamedBeanSet(), beansWithoutParentList); 238 checkItemsHaveParents(InstanceManager.getDefault(AnalogExpressionManager.class).getNamedBeanSet(), beansWithoutParentList); 239 checkItemsHaveParents(InstanceManager.getDefault(DigitalExpressionManager.class).getNamedBeanSet(), beansWithoutParentList); 240 checkItemsHaveParents(InstanceManager.getDefault(StringExpressionManager.class).getNamedBeanSet(), beansWithoutParentList); 241 242 if (!beansWithoutParentList.isEmpty()) { 243 List<String> errors = new ArrayList<>(); 244 List<String> msgs = new ArrayList<>(); 245 for (Base b : beansWithoutParentList) { 246 b.setup(); 247 b.setParentForAllChildren(errors); 248 } 249 for (Base b : beansWithoutParentList) { 250 if (b.getParent() == null) { 251 log.error("Item has no parent: {}, {}, {}", 252 b.getSystemName(), 253 b.getUserName(), 254 b.getLongDescription()); 255 msgs.add(Bundle.getMessage("NoParentMessage", 256 b.getSystemName(), 257 b.getUserName(), 258 b.getLongDescription())); 259 260 for (int i=0; i < b.getChildCount(); i++) { 261 if (b.getChild(i).isConnected()) { 262 log.error(" Child: {}, {}, {}", 263 b.getChild(i).getConnectedSocket().getSystemName(), 264 b.getChild(i).getConnectedSocket().getUserName(), 265 b.getChild(i).getConnectedSocket().getLongDescription()); 266 } 267 } 268 log.error(" End Item"); 269 List<String> cliperrors = new ArrayList<String>(); 270 _clipboard.add((MaleSocket) b, cliperrors); 271 } 272 } 273 messageDialog("ParentErrorsTitle", msgs, "NoParentHelp"); 274 } 275 } 276 277 /** {@inheritDoc} */ 278 @Override 279 public void activateAllLogixNGs() { 280 activateAllLogixNGs(true, true); 281 } 282 283 /** {@inheritDoc} */ 284 @Override 285 public void activateAllLogixNGs(boolean runDelayed, boolean runOnSeparateThread) { 286 287 _isActive = true; 288 289 if (_loadDisabled) { 290 for (LogixNG logixNG : _tsys.values()) { 291 logixNG.setEnabled(false); 292 } 293 _loadDisabled = false; 294 } 295 296 // This may take a long time so it must not be done on the GUI thread. 297 // Therefore we create a new thread for this task. 298 Runnable runnable = () -> { 299 300 // Initialize the values of the global variables 301 Set<GlobalVariable> globalVariables = 302 InstanceManager.getDefault(GlobalVariableManager.class) 303 .getNamedBeanSet(); 304 305 for (GlobalVariable gv : globalVariables) { 306 try { 307 gv.initialize(); 308 } catch (JmriException | IllegalArgumentException e) { 309 log.warn("Variable {} could not be initialized", gv.getUserName(), e); 310 } 311 } 312 313 Set<LogixNG> activeLogixNGs = new HashSet<>(); 314 315 // Activate and execute the initialization LogixNGs first. 316 List<LogixNG> initLogixNGs = 317 InstanceManager.getDefault(LogixNG_InitializationManager.class) 318 .getList(); 319 320 for (LogixNG logixNG : initLogixNGs) { 321 logixNG.activate(); 322 if (logixNG.isActive()) { 323 logixNG.registerListeners(); 324 logixNG.execute(false, true); 325 activeLogixNGs.add(logixNG); 326 } else { 327 logixNG.unregisterListeners(); 328 } 329 } 330 331 // Activate and execute all the rest of the LogixNGs. 332 _tsys.values().stream() 333 .sorted() 334 .filter((logixNG) -> !(activeLogixNGs.contains(logixNG))) 335 .forEachOrdered((logixNG) -> { 336 337 logixNG.activate(); 338 339 if (logixNG.isActive()) { 340 logixNG.registerListeners(); 341 logixNG.execute(true, true); 342 } else { 343 logixNG.unregisterListeners(); 344 } 345 }); 346 347 // Clear the startup flag of the LogixNGs. 348 _tsys.values().stream().forEach((logixNG) -> { 349 logixNG.clearStartup(); 350 }); 351 }; 352 353 if (runOnSeparateThread) new Thread(runnable).start(); 354 else runnable.run(); 355 } 356 357 /** {@inheritDoc} */ 358 @Override 359 public void deActivateAllLogixNGs() { 360 for (LogixNG logixNG : _tsys.values()) { 361 logixNG.unregisterListeners(); 362 } 363 _isActive = false; 364 } 365 366 /** {@inheritDoc} */ 367 @Override 368 public boolean isActive() { 369 return _isActive; 370 } 371 372 /** {@inheritDoc} */ 373 @Override 374 public void deleteLogixNG(LogixNG x) { 375 // delete the LogixNG 376 deregister(x); 377 x.dispose(); 378 } 379 380 /** {@inheritDoc} */ 381 @Override 382 public void printTree( 383 PrintTreeSettings settings, 384 PrintWriter writer, 385 String indent, 386 MutableInt lineNumber) { 387 388 printTree(settings, Locale.getDefault(), writer, indent, lineNumber); 389 } 390 391 /** {@inheritDoc} */ 392 @Override 393 public void printTree( 394 PrintTreeSettings settings, 395 Locale locale, 396 PrintWriter writer, 397 String indent, 398 MutableInt lineNumber) { 399 400 for (LogixNG logixNG : getNamedBeanSet()) { 401 if (logixNG.isInline()) continue; 402 logixNG.printTree(settings, locale, writer, indent, "", lineNumber); 403 writer.println(); 404 } 405 406 for (LogixNG logixNG : getNamedBeanSet()) { 407 if (!logixNG.isInline()) continue; 408 logixNG.printTree(settings, locale, writer, indent, "", lineNumber); 409 writer.println(); 410 } 411 InstanceManager.getDefault(ModuleManager.class).printTree(settings, locale, writer, indent, lineNumber); 412 InstanceManager.getDefault(NamedTableManager.class).printTree(locale, writer, indent); 413 InstanceManager.getDefault(GlobalVariableManager.class).printTree(locale, writer, indent); 414 InstanceManager.getDefault(LogixNG_InitializationManager.class).printTree(locale, writer, indent); 415 } 416 417 418 static volatile DefaultLogixNGManager _instance = null; 419 420 @InvokeOnGuiThread // this method is not thread safe 421 static public DefaultLogixNGManager instance() { 422 if (!ThreadingUtil.isGUIThread()) { 423 LoggingUtil.warnOnce(log, "instance() called on wrong thread"); 424 } 425 426 if (_instance == null) { 427 _instance = new DefaultLogixNGManager(); 428 } 429 return (_instance); 430 } 431 432 /** {@inheritDoc} */ 433 @Override 434 public Class<LogixNG> getNamedBeanClass() { 435 return LogixNG.class; 436 } 437 438 /** {@inheritDoc} */ 439 @Override 440 public Clipboard getClipboard() { 441 return _clipboard; 442 } 443 444 /** {@inheritDoc} */ 445 @Override 446 public void registerManager(Manager<? extends MaleSocket> manager) { 447 _managers.put(manager.getClass().getName(), manager); 448 } 449 450 /** {@inheritDoc} */ 451 @Override 452 public Manager<? extends MaleSocket> getManager(String className) { 453 return _managers.get(className); 454 } 455 456 /** 457 * Inform all registered listeners of a vetoable change.If the propertyName 458 * is "CanDelete" ALL listeners with an interest in the bean will throw an 459 * exception, which is recorded returned back to the invoking method, so 460 * that it can be presented back to the user.However if a listener decides 461 * that the bean can not be deleted then it should throw an exception with 462 * a property name of "DoNotDelete", this is thrown back up to the user and 463 * the delete process should be aborted. 464 * 465 * @param p The programmatic name of the property that is to be changed. 466 * "CanDelete" will inquire with all listeners if the item can 467 * be deleted. "DoDelete" tells the listener to delete the item. 468 * @param old The old value of the property. 469 * @throws java.beans.PropertyVetoException If the recipients wishes the 470 * delete to be aborted (see above) 471 */ 472 @OverridingMethodsMustInvokeSuper 473 public void fireVetoableChange(String p, Object old) throws PropertyVetoException { 474 PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, null); 475 for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) { 476 vc.vetoableChange(evt); 477 } 478 } 479 480 /** {@inheritDoc} */ 481 @Override 482// @OverridingMethodsMustInvokeSuper 483 public final void deleteBean(@Nonnull LogixNG logixNG, @Nonnull String property) throws PropertyVetoException { 484 for (int i=logixNG.getNumConditionalNGs()-1; i >= 0; i--) { 485 ConditionalNG child = logixNG.getConditionalNG(i); 486 InstanceManager.getDefault(ConditionalNG_Manager.class).deleteBean(child, property); 487 } 488 489 // throws PropertyVetoException if vetoed 490 fireVetoableChange(property, logixNG); 491 if (property.equals("DoDelete")) { // NOI18N 492 deregister(logixNG); 493 logixNG.dispose(); 494 } 495 } 496 497 /** {@inheritDoc} */ 498 @Override 499 public void registerSetupTask(Runnable task) { 500 _setupTasks.add(task); 501 } 502 503 /** {@inheritDoc} */ 504 @Override 505 public void executeModule(Module module, Object parameter) 506 throws IllegalArgumentException { 507 508 if (module == null) { 509 throw new IllegalArgumentException("The parameter \"module\" is null"); 510 } 511 // Get the parameters for the module 512 Collection<Module.Parameter> parameterNames = module.getParameters(); 513 514 // Ensure that there is only one parameter 515 if (parameterNames.size() != 1) { 516 throw new IllegalArgumentException("The module doesn't take exactly one parameter"); 517 } 518 519 // Get the parameter 520 Module.Parameter param = parameterNames.toArray(Module.Parameter[]::new)[0]; 521 if (!param.isInput()) { 522 throw new IllegalArgumentException("The module's parameter is not an input parameter"); 523 } 524 525 // Set the value of the parameter 526 Map<String, Object> parameters = new HashMap<>(); 527 parameters.put(param.getName(), parameter); 528 529 // Execute the module 530 executeModule(module, parameters); 531 } 532 533 /** {@inheritDoc} */ 534 @Override 535 public void executeModule(Module module, Map<String, Object> parameters) 536 throws IllegalArgumentException { 537 DefaultConditionalNG.executeModule(module, parameters); 538 } 539 540 /** 541 * The PropertyChangeListener interface in this class is intended to keep 542 * track of user name changes to individual NamedBeans. It is not completely 543 * implemented yet. In particular, listeners are not added to newly 544 * registered objects. 545 * 546 * @param e the event 547 */ 548 @Override 549 @OverridingMethodsMustInvokeSuper 550 public void propertyChange(PropertyChangeEvent e) { 551 super.propertyChange(e); 552 if (LogixNG.PROPERTY_INLINE.equals(e.getPropertyName())) { 553 // If a LogixNG changes its "inline" state, the number of items 554 // listed in the LogixNG table might change. 555 firePropertyChange("length", null, _beans.size()); 556 } 557 } 558 559 560 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultLogixNGManager.class); 561 562}