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 logixNG.printTree(settings, locale, writer, indent, "", lineNumber); 402 writer.println(); 403 } 404 InstanceManager.getDefault(ModuleManager.class).printTree(settings, locale, writer, indent, lineNumber); 405 InstanceManager.getDefault(NamedTableManager.class).printTree(locale, writer, indent); 406 InstanceManager.getDefault(GlobalVariableManager.class).printTree(locale, writer, indent); 407 InstanceManager.getDefault(LogixNG_InitializationManager.class).printTree(locale, writer, indent); 408 } 409 410 411 static volatile DefaultLogixNGManager _instance = null; 412 413 @InvokeOnGuiThread // this method is not thread safe 414 static public DefaultLogixNGManager instance() { 415 if (!ThreadingUtil.isGUIThread()) { 416 LoggingUtil.warnOnce(log, "instance() called on wrong thread"); 417 } 418 419 if (_instance == null) { 420 _instance = new DefaultLogixNGManager(); 421 } 422 return (_instance); 423 } 424 425 /** {@inheritDoc} */ 426 @Override 427 public Class<LogixNG> getNamedBeanClass() { 428 return LogixNG.class; 429 } 430 431 /** {@inheritDoc} */ 432 @Override 433 public Clipboard getClipboard() { 434 return _clipboard; 435 } 436 437 /** {@inheritDoc} */ 438 @Override 439 public void registerManager(Manager<? extends MaleSocket> manager) { 440 _managers.put(manager.getClass().getName(), manager); 441 } 442 443 /** {@inheritDoc} */ 444 @Override 445 public Manager<? extends MaleSocket> getManager(String className) { 446 return _managers.get(className); 447 } 448 449 /** 450 * Inform all registered listeners of a vetoable change.If the propertyName 451 * is "CanDelete" ALL listeners with an interest in the bean will throw an 452 * exception, which is recorded returned back to the invoking method, so 453 * that it can be presented back to the user.However if a listener decides 454 * that the bean can not be deleted then it should throw an exception with 455 * a property name of "DoNotDelete", this is thrown back up to the user and 456 * the delete process should be aborted. 457 * 458 * @param p The programmatic name of the property that is to be changed. 459 * "CanDelete" will inquire with all listeners if the item can 460 * be deleted. "DoDelete" tells the listener to delete the item. 461 * @param old The old value of the property. 462 * @throws java.beans.PropertyVetoException If the recipients wishes the 463 * delete to be aborted (see above) 464 */ 465 @OverridingMethodsMustInvokeSuper 466 public void fireVetoableChange(String p, Object old) throws PropertyVetoException { 467 PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, null); 468 for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) { 469 vc.vetoableChange(evt); 470 } 471 } 472 473 /** {@inheritDoc} */ 474 @Override 475// @OverridingMethodsMustInvokeSuper 476 public final void deleteBean(@Nonnull LogixNG logixNG, @Nonnull String property) throws PropertyVetoException { 477 for (int i=logixNG.getNumConditionalNGs()-1; i >= 0; i--) { 478 ConditionalNG child = logixNG.getConditionalNG(i); 479 InstanceManager.getDefault(ConditionalNG_Manager.class).deleteBean(child, property); 480 } 481 482 // throws PropertyVetoException if vetoed 483 fireVetoableChange(property, logixNG); 484 if (property.equals("DoDelete")) { // NOI18N 485 deregister(logixNG); 486 logixNG.dispose(); 487 } 488 } 489 490 /** {@inheritDoc} */ 491 @Override 492 public void registerSetupTask(Runnable task) { 493 _setupTasks.add(task); 494 } 495 496 /** 497 * The PropertyChangeListener interface in this class is intended to keep 498 * track of user name changes to individual NamedBeans. It is not completely 499 * implemented yet. In particular, listeners are not added to newly 500 * registered objects. 501 * 502 * @param e the event 503 */ 504 @Override 505 @OverridingMethodsMustInvokeSuper 506 public void propertyChange(PropertyChangeEvent e) { 507 super.propertyChange(e); 508 if (LogixNG.PROPERTY_INLINE.equals(e.getPropertyName())) { 509 // If a LogixNG changes its "inline" state, the number of items 510 // listed in the LogixNG table might change. 511 firePropertyChange("length", null, _beans.size()); 512 } 513 } 514 515 516 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultLogixNGManager.class); 517 518}