001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2018 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see http://www.gnu.org/licenses. 027 */ 028 029package edu.wisc.ssec.mcidasv; 030 031import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList; 032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.cast; 033import static ucar.unidata.xml.XmlUtil.getAttribute; 034 035import java.awt.Insets; 036import java.awt.event.ActionEvent; 037import java.awt.event.ActionListener; 038import java.awt.geom.Rectangle2D; 039 040import java.io.BufferedReader; 041import java.io.File; 042import java.io.FileOutputStream; 043import java.io.FileReader; 044import java.io.IOException; 045import java.io.PrintStream; 046 047import java.lang.reflect.Method; 048import java.net.URL; 049import java.net.URLConnection; 050import java.net.URLStreamHandler; 051import java.rmi.RemoteException; 052 053import java.security.Security; 054import java.util.Collections; 055import java.util.Date; 056import java.util.EnumSet; 057import java.util.HashMap; 058import java.util.Hashtable; 059import java.util.LinkedList; 060import java.util.List; 061import java.util.Map; 062import java.util.Objects; 063import java.util.Properties; 064import java.util.Set; 065 066import javax.swing.Icon; 067import javax.swing.JButton; 068import javax.swing.JCheckBox; 069import javax.swing.JComponent; 070import javax.swing.JDialog; 071import javax.swing.JLabel; 072import javax.swing.JOptionPane; 073import javax.swing.SwingUtilities; 074import javax.swing.ToolTipManager; 075 076import edu.wisc.ssec.mcidas.adde.AddeURL; 077import edu.wisc.ssec.mcidas.adde.AddeURLStreamHandler; 078 079import edu.wisc.ssec.mcidasv.collaboration.CollaborationManager; 080import edu.wisc.ssec.mcidasv.util.McVGuiUtils; 081import edu.wisc.ssec.mcidasv.util.OptionPaneClicker; 082import edu.wisc.ssec.mcidasv.util.SystemState; 083import edu.wisc.ssec.mcidasv.util.WebBrowser; 084import edu.wisc.ssec.mcidasv.util.WelcomeWindow; 085 086import javafx.application.Platform; 087import javafx.embed.swing.JFXPanel; 088import org.joda.time.DateTime; 089import org.python.util.PythonInterpreter; 090import org.w3c.dom.Element; 091 092import ucar.nc2.NetcdfFile; 093import visad.VisADException; 094 095import ucar.unidata.data.DataManager; 096import ucar.unidata.idv.ArgsManager; 097import ucar.unidata.idv.ControlDescriptor; 098import ucar.unidata.idv.IdvObjectStore; 099import ucar.unidata.idv.IdvPersistenceManager; 100import ucar.unidata.idv.IdvPreferenceManager; 101import ucar.unidata.idv.IdvResourceManager; 102import ucar.unidata.idv.IntegratedDataViewer; 103import ucar.unidata.idv.PluginManager; 104import ucar.unidata.idv.VMManager; 105import ucar.unidata.idv.ViewDescriptor; 106import ucar.unidata.idv.ViewManager; 107import ucar.unidata.idv.chooser.IdvChooserManager; 108import ucar.unidata.idv.collab.CollabManager; 109import ucar.unidata.idv.ui.IdvUIManager; 110import ucar.unidata.ui.colortable.ColorTableManager; 111import ucar.unidata.ui.InteractiveShell.ShellHistoryEntry; 112import ucar.unidata.util.FileManager; 113import ucar.unidata.util.GuiUtils; 114import ucar.unidata.util.IOUtil; 115import ucar.unidata.util.LogUtil; 116import ucar.unidata.util.Misc; 117import ucar.unidata.xml.XmlDelegateImpl; 118import ucar.unidata.xml.XmlEncoder; 119import ucar.unidata.xml.XmlUtil; 120 121import org.bushe.swing.event.EventBus; 122import org.bushe.swing.event.annotation.AnnotationProcessor; 123import org.bushe.swing.event.annotation.EventSubscriber; 124 125import org.slf4j.bridge.SLF4JBridgeHandler; 126import org.slf4j.Logger; 127import org.slf4j.LoggerFactory; 128 129import uk.org.lidalia.sysoutslf4j.context.LogLevel; 130import uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J; 131 132import edu.wisc.ssec.mcidasv.data.GpmIosp; 133import edu.wisc.ssec.mcidasv.chooser.McIdasChooserManager; 134import edu.wisc.ssec.mcidasv.control.LambertAEA; 135import edu.wisc.ssec.mcidasv.data.McvDataManager; 136import edu.wisc.ssec.mcidasv.monitors.MonitorManager; 137import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource; 138import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus; 139import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType; 140import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity; 141import edu.wisc.ssec.mcidasv.servermanager.AddePreferences; 142import edu.wisc.ssec.mcidasv.servermanager.EntryStore; 143import edu.wisc.ssec.mcidasv.servermanager.EntryTransforms; 144import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry; 145import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat; 146import edu.wisc.ssec.mcidasv.servermanager.RemoteAddeEntry; 147import edu.wisc.ssec.mcidasv.servermanager.TabbedAddeManager; 148import edu.wisc.ssec.mcidasv.startupmanager.StartupManager; 149import edu.wisc.ssec.mcidasv.ui.LayerAnimationWindow; 150import edu.wisc.ssec.mcidasv.ui.McIdasColorTableManager; 151import edu.wisc.ssec.mcidasv.ui.UIManager; 152import edu.wisc.ssec.mcidasv.util.gui.EventDispatchThreadHangMonitor; 153import edu.wisc.ssec.mcidasv.util.pathwatcher.DirectoryWatchService; 154import edu.wisc.ssec.mcidasv.util.pathwatcher.OnFileChangeListener; 155import edu.wisc.ssec.mcidasv.util.pathwatcher.SimpleDirectoryWatchService; 156 157/** 158 * Class used as the {@literal "gateway"} to a running McIDAS-V session. 159 * 160 * <p>This is where the startup and shutdown processes are handled, as well as 161 * the initialization of the application's various {@literal "managers"}.</p> 162 */ 163@SuppressWarnings("unchecked") 164public class McIDASV extends IntegratedDataViewer { 165 166 /** Logging object. */ 167 private static final Logger logger = 168 LoggerFactory.getLogger(McIDASV.class); 169 170 /** 171 * Initialization start time. This value is set at the beginning of 172 * {@link #main(String[])}, and is used to estimate the duration of the 173 * session's initialization phase. 174 */ 175 private static long startTime; 176 177 /** 178 * Initialization duration. Set at the end of {@link #initDone()}. 179 */ 180 private static long estimate; 181 182 /** 183 * Path to a {@literal "session"} file--it's created upon McIDAS-V 184 * starting and removed when McIDAS-V exits cleanly. This allows us to 185 * perform a primitive check to see if the current session has happened 186 * after a crash. 187 */ 188 private static String SESSION_FILE = getSessionFilePath(); 189 190 /** 191 * Whether or not the previous session was able to exit as it should. 192 * If {@code false}, the previous session likely crashed. 193 */ 194 private static boolean cleanExit = true; 195 196 /** Date the previous session was started. May be {@code null}. */ 197 private static Date previousStart = null; 198 199 /** Set to true only if "-forceaqua" was found in the command line. */ 200 public static boolean useAquaLookAndFeel = false; 201 202 /** Points to the adde image defaults. */ 203 public static final IdvResourceManager.XmlIdvResource RSC_FRAMEDEFAULTS = 204 new IdvResourceManager.XmlIdvResource("idv.resource.framedefaults", 205 "McIDAS-X Frame Defaults"); 206 207 /** Points to the server definitions. */ 208 public static final IdvResourceManager.XmlIdvResource RSC_SERVERS = 209 new IdvResourceManager.XmlIdvResource("idv.resource.servers", 210 "Servers", "servers\\.xml$"); 211 212 /** Used to access McIDAS-V state in a static context. */ 213 private static McIDASV staticMcv; 214 215 /** Accessory in file save dialog */ 216 private JCheckBox overwriteDataCbx = 217 new JCheckBox("Change data paths", false); 218 219 /** Chooser manager */ 220 protected McIdasChooserManager chooserManager; 221 222 /** HTTP based monitor to dump stack traces and shutdown McIDAS-V. */ 223 private McIDASVMonitor mcvMonitor; 224 225 /** 226 * {@link MonitorManager} allows for relatively easy and efficient 227 * monitoring of various resources. 228 */ 229 private final MonitorManager monitorManager = new MonitorManager(); 230 231 /** 232 * Actions passed into {@link #handleAction(String, Hashtable, boolean)}. 233 */ 234 private final List<String> actions = new LinkedList<>(); 235 236 private enum WarningResult { OK, CANCEL, SHOW, HIDE }; 237 238 /** Reference to the ADDE server manager. */ 239 private EntryStore addeEntries; 240 241 /** 242 * GUI wrapper for ADDE server management. Reference is kept due to some 243 * of the trickery used by {@link AddePreferences}. 244 * 245 * <p>Value may be {@code null}.</p> 246 */ 247 private TabbedAddeManager tabbedAddeManager = null; 248 249 /** Directory monitoring service. */ 250 private final DirectoryWatchService watchService; 251 252 /** 253 * Create the McIDASV with the given command line arguments. 254 * This constructor calls {@link IntegratedDataViewer#init()} 255 * 256 * @param args Command line arguments 257 * @exception VisADException from construction of VisAd objects 258 * @exception RemoteException from construction of VisAD objects 259 */ 260 public McIDASV(String[] args) throws IOException, VisADException { 261 super(args); 262 263 AnnotationProcessor.process(this); 264 265 staticMcv = this; 266 267 // Set up our application to respond to the Mac OS X application menu 268 registerForMacOSXEvents(); 269 270 // we're tired of the IDV's default missing image, so reset it 271 GuiUtils.MISSING_IMAGE = 272 "/edu/wisc/ssec/mcidasv/resources/icons/toolbar/mcidasv-round22.png"; 273 274 watchService = new SimpleDirectoryWatchService(); 275 276 this.init(); 277 278 // ensure jython init only happens once per application session 279 String cacheDir = getStore().getJythonCacheDir(); 280 Properties pyProps = new Properties(); 281 if (cacheDir != null) { 282 pyProps.put("python.home", cacheDir); 283 } 284 String[] blank = new String[] { "" }; 285 PythonInterpreter.initialize(System.getProperties(), pyProps, blank); 286 } 287 288 /** 289 * Generic registration with the macOS application menu. 290 * 291 * <p>Checks the platform, then attempts to register with Apple's 292 * {@literal "EAWT"} stuff.</p> 293 * 294 * <p>See {@code OSXAdapter.java} to learn how this is done without 295 * directly referencing any Apple APIs.</p> 296 */ 297 public void registerForMacOSXEvents() { 298 // TODO(jon): remove? 299 if (isMac()) { 300 try { 301 // Generate and register the OSXAdapter, passing it a hash of all the methods we wish to 302 // use as delegates for various com.apple.eawt.ApplicationListener methods 303 Class<?> thisClass = getClass(); 304 Class<?>[] args = (Class[])null; 305 OSXAdapter.setQuitHandler(this, thisClass.getDeclaredMethod("MacOSXQuit", args)); 306 OSXAdapter.setAboutHandler(this, thisClass.getDeclaredMethod("MacOSXAbout", args)); 307 OSXAdapter.setPreferencesHandler(this, thisClass.getDeclaredMethod("MacOSXPreferences", args)); 308 } catch (Exception e) { 309 logger.error("Error while loading the OSXAdapter", e); 310 } 311 } 312 } 313 314 public boolean MacOSXQuit() { 315 // TODO(jon): remove? 316 return quit(); 317 } 318 319 public void MacOSXAbout() { 320 // TODO(jon): remove? 321 getIdvUIManager().about(); 322 } 323 324 public void MacOSXPreferences() { 325 // TODO(jon): remove? 326 showPreferenceManager(); 327 } 328 329 /** 330 * Get the maximum number of threads to be used when rendering in VisAD. 331 * 332 * @return Number of threads for rendering. Default value is the same as 333 * {@link Runtime#availableProcessors()}. 334 */ 335 @Override public int getMaxRenderThreadCount() { 336 StateManager stateManager = (StateManager)getStateManager(); 337 return stateManager.getPropertyOrPreference(PREF_THREADS_RENDER, 338 Runtime.getRuntime().availableProcessors()); 339 } 340 341 /** 342 * Get the maximum number of threads to be used when reading data. 343 * 344 * @return Number of threads for reading data. Default value is {@code 4}. 345 */ 346 @Override public int getMaxDataThreadCount() { 347 StateManager stateManager = (StateManager)getStateManager(); 348 return stateManager.getPropertyOrPreference(PREF_THREADS_DATA, 4); 349 } 350 351 /** 352 * Start up the McIDAS-V monitor server. 353 * 354 * <p>This is an HTTP server on the port defined by the property 355 * {@code idv.monitorport}. Default value is 8788.</p> 356 * 357 * <p>It is only accessible to 127.0.0.1 (localhost).</p> 358 */ 359 @Override protected void startMonitor() { 360 if (mcvMonitor != null) { 361 return; 362 } 363 final String monitorPort = getProperty(PROP_MONITORPORT, ""); 364 if (monitorPort!=null && monitorPort.trim().length()>0 && !"none".equals(monitorPort.trim())) { 365 Misc.run(() -> { 366 try { 367 mcvMonitor = 368 new McIDASVMonitor(McIDASV.this, 369 Integer.parseInt(monitorPort)); 370 mcvMonitor.init(); 371 } catch (Exception exc) { 372 LogUtil.consoleMessage("Unable to start McIDAS-V monitor on port:" + monitorPort); 373 LogUtil.consoleMessage("Error:" + exc); 374 } 375 }); 376 } 377 } 378 379 /** 380 * Initializes a XML encoder with McIDAS-V specific XML delegates. 381 * 382 * @param encoder XML encoder that'll be dealing with persistence. 383 * @param forRead Not used as of yet. 384 */ 385 // TODO: if we ever get up past three or so XML delegates, I vote that we 386 // make our own version of VisADPersistence. 387 @Override protected void initEncoder(XmlEncoder encoder, boolean forRead) { 388 389 encoder.addDelegateForClass(LambertAEA.class, new XmlDelegateImpl() { 390 @Override public Element createElement(XmlEncoder e, Object o) { 391 LambertAEA projection = (LambertAEA)o; 392 Rectangle2D rect = projection.getDefaultMapArea(); 393 List args = Misc.newList(rect); 394 List types = Misc.newList(rect.getClass()); 395 return e.createObjectConstructorElement(o, args, types); 396 } 397 }); 398 399 encoder.addDelegateForClass(ShellHistoryEntry.class, new XmlDelegateImpl() { 400 @Override public Element createElement(XmlEncoder e, Object o) { 401 ShellHistoryEntry entry = (ShellHistoryEntry)o; 402 List args = Misc.newList(entry.getEntryBytes(), entry.getInputMode().toString()); 403 return e.createObjectConstructorElement(o, args); 404 } 405 }); 406 407 // TODO(jon): ultra fashion makeover!! 408 encoder.addDelegateForClass(RemoteAddeEntry.class, new XmlDelegateImpl() { 409 @Override public Element createElement(XmlEncoder e, Object o) { 410 RemoteAddeEntry entry = (RemoteAddeEntry)o; 411 Element element = e.createObjectElement(o.getClass()); 412 element.setAttribute("address", entry.getAddress()); 413 element.setAttribute("group", entry.getGroup()); 414 element.setAttribute("username", entry.getAccount().getUsername()); 415 element.setAttribute("project", entry.getAccount().getProject()); 416 element.setAttribute("source", entry.getEntrySource().toString()); 417 element.setAttribute("type", entry.getEntryType().toString()); 418 element.setAttribute("validity", entry.getEntryValidity().toString()); 419 element.setAttribute("status", entry.getEntryStatus().toString()); 420 element.setAttribute("temporary", Boolean.toString(entry.isEntryTemporary())); 421 element.setAttribute("alias", entry.getEntryAlias()); 422 return element; 423 } 424 425 @Override public Object createObject(XmlEncoder e, Element element) { 426 String address = getAttribute(element, "address"); 427 String group = getAttribute(element, "group"); 428 String username = getAttribute(element, "username"); 429 String project = getAttribute(element, "project"); 430 String source = getAttribute(element, "source"); 431 String type = getAttribute(element, "type"); 432 String validity = getAttribute(element, "validity"); 433 String status = getAttribute(element, "status"); 434 boolean temporary = getAttribute(element, "temporary", false); 435 String alias = getAttribute(element, "alias", ""); 436 437 EntrySource entrySource = EntryTransforms.strToEntrySource(source); 438 EntryType entryType = EntryTransforms.strToEntryType(type); 439 EntryValidity entryValidity = EntryTransforms.strToEntryValidity(validity); 440 EntryStatus entryStatus = EntryTransforms.strToEntryStatus(status); 441 442 return new RemoteAddeEntry.Builder(address, group) 443 .account(username, project) 444 .source(entrySource) 445 .type(entryType) 446 .validity(entryValidity) 447 .temporary(temporary) 448 .alias(alias) 449 .status(entryStatus).build(); 450 } 451 }); 452 453 encoder.addDelegateForClass(LocalAddeEntry.class, new XmlDelegateImpl() { 454 @Override public Element createElement(XmlEncoder e, Object o) { 455 LocalAddeEntry entry = (LocalAddeEntry)o; 456 Element element = e.createObjectElement(o.getClass()); 457 element.setAttribute("group", entry.getGroup()); 458 element.setAttribute("descriptor", entry.getDescriptor()); 459 element.setAttribute("realtime", entry.getRealtimeAsString()); 460 element.setAttribute("format", entry.getFormat().name()); 461 element.setAttribute("start", entry.getStart()); 462 element.setAttribute("end", entry.getEnd()); 463 element.setAttribute("fileMask", entry.getMask()); 464 element.setAttribute("name", entry.getName()); 465 element.setAttribute("status", entry.getEntryStatus().name()); 466 element.setAttribute("temporary", Boolean.toString(entry.isEntryTemporary())); 467 element.setAttribute("alias", entry.getEntryAlias()); 468 return element; 469 } 470 471 @Override public Object createObject(XmlEncoder e, Element element) { 472 String group = getAttribute(element, "group"); 473 String descriptor = getAttribute(element, "descriptor"); 474 String realtime = getAttribute(element, "realtime", ""); 475 AddeFormat format = EntryTransforms.strToAddeFormat(XmlUtil.getAttribute(element, "format")); 476 String start = getAttribute(element, "start", "1"); 477 String end = getAttribute(element, "end", "999999"); 478 String fileMask = getAttribute(element, "fileMask"); 479 String name = getAttribute(element, "name"); 480 String status = getAttribute(element, "status", "ENABLED"); 481 boolean temporary = getAttribute(element, "temporary", false); 482 String alias = getAttribute(element, "alias", ""); 483 484 return new LocalAddeEntry.Builder(name, group, fileMask, format) 485 .range(start, end) 486 .descriptor(descriptor) 487 .realtime(realtime) 488 .status(status) 489 .temporary(temporary) 490 .alias(alias).build(); 491 } 492 }); 493 494 // Move legacy classes to a new location 495 encoder.registerNewClassName( 496 "edu.wisc.ssec.mcidasv.data.Test2ImageDataSource", 497 "edu.wisc.ssec.mcidasv.data.adde.AddeImageParameterDataSource"); 498 encoder.registerNewClassName( 499 "edu.wisc.ssec.mcidasv.data.Test2AddeImageDataSource", 500 "edu.wisc.ssec.mcidasv.data.adde.AddeImageParameterDataSource"); 501 encoder.registerNewClassName( 502 "edu.wisc.ssec.mcidasv.data.AddePointDataSource", 503 "edu.wisc.ssec.mcidasv.data.adde.AddePointDataSource"); 504 encoder.registerNewClassName( 505 "edu.wisc.ssec.mcidasv.data.AddeSoundingAdapter", 506 "edu.wisc.ssec.mcidasv.data.adde.AddeSoundingAdapter"); 507 } 508 509 /** 510 * Returns <i>all</i> of the actions used in this McIDAS-V session. This is 511 * possibly TMI and might be removed... 512 * 513 * @return Actions executed thus far. 514 */ 515 public List<String> getActionHistory() { 516 return actions; 517 } 518 519 /** 520 * Converts {@link ArgsManager#getOriginalArgs()} to a {@link List} and 521 * returns. 522 * 523 * @return The command-line arguments used to start McIDAS-V, as an 524 * {@code ArrayList}. 525 */ 526 public List<String> getCommandLineArgs() { 527 String[] originalArgs = getArgsManager().getOriginalArgs(); 528 List<String> args = arrList(originalArgs.length); 529 Collections.addAll(args, originalArgs); 530 return args; 531 } 532 533 /** 534 * Captures the action passed to {@code handleAction}. The action is logged 535 * and additionally, if the action is a HTML link, we attempt to visit the 536 * link in the user's preferred browser. 537 */ 538 @Override public boolean handleAction(String action, Hashtable properties, 539 boolean checkForAlias) 540 { 541 actions.add(action); 542 543 boolean result = false; 544 DateTime start = DateTime.now(); 545 logger.trace("started: action='{}', checkForAlias={}, properties='{}'", action, checkForAlias, properties); 546 if (IOUtil.isHtmlFile(action)) { 547 WebBrowser.browse(action); 548 result = true; 549 } else { 550 if (action.toLowerCase().contains("showsupportform")) { 551 logger.trace("showing support form 'manually'..."); 552 getIdvUIManager().showSupportForm(); 553 result = true; 554 } else { 555 result = super.handleAction(action, properties, checkForAlias); 556 } 557 } 558 long duration = new DateTime().minus(start.getMillis()).getMillis(); 559 logger.trace("finished: action='{}', duration: {} (ms), checkForAlias={}, properties='{}'", action, duration, checkForAlias, properties); 560 561 return result; 562 } 563 564 /** 565 * This method checks if the given action is one of the following. 566 * <ul> 567 * <li>Jython code: starts with {@literal "jython:"}.</li> 568 * <li>Help link: starts with {@literal "help:"}.</li> 569 * <li>Resource bundle file: ends with {@literal ".rbi"}.</li> 570 * <li>Bundle file: ends with {@literal ".xidv"}.</li> 571 * <li>JNLP file: ends with {@literal ".jnlp"}.</li> 572 * </ul> 573 * 574 * <p>It returns {@code true} if the action is one of these. {@code false} 575 * otherwise.</p> 576 * 577 * @param action The string action 578 * @param properties any properties 579 * 580 * @return {@code true} if the action was {@literal "handled"}; 581 * {@code false} otherwise. 582 */ 583 @Override protected boolean handleFileOrUrlAction(String action, 584 Hashtable properties) { 585 boolean result = false; 586 boolean idvAction = action.startsWith("idv:"); 587 boolean jythonAction = action.startsWith("jython:"); 588 589 if (!idvAction && !jythonAction) { 590 return super.handleFileOrUrlAction(action, properties); 591 } 592 593 Map<String, Object> hashProps; 594 if (properties != null) { 595 hashProps = new HashMap<>(properties); 596 } else { 597 //noinspection CollectionWithoutInitialCapacity 598 hashProps = new HashMap<>(); 599 } 600 601 ucar.unidata.idv.JythonManager jyManager = getJythonManager(); 602 if (idvAction) { 603 action = action.replace("&", "&").substring(4); 604 jyManager.evaluateUntrusted(action, hashProps); 605 result = true; 606 } else if (jythonAction) { 607 action = action.substring(7); 608 jyManager.evaluateAction(action, hashProps); 609 result = true; 610 } else { 611 result = super.handleFileOrUrlAction(action, properties); 612 } 613 return result; 614 } 615 616 /** 617 * Add a new {@link ControlDescriptor} into the {@code controlDescriptor} 618 * list and {@code controlDescriptorMap}. 619 * 620 * <p>This method differs from the IDV's in that McIDAS-V <b>overwrites</b> 621 * existing {@code ControlDescriptor ControlDescriptors} if 622 * {@link ControlDescriptor#getControlId()} matches. 623 * 624 * @param cd The ControlDescriptor to add. 625 * 626 * @throws NullPointerException if {@code cd} is {@code null}. 627 */ 628 @Override protected void addControlDescriptor(ControlDescriptor cd) { 629 cd = Objects.requireNonNull(cd, "Cannot add a null control descriptor to the list of control descriptors."); 630 String id = cd.getControlId(); 631 if (controlDescriptorMap.get(id) == null) { 632 controlDescriptors.add(cd); 633 controlDescriptorMap.put(id, cd); 634 } else { 635 for (int i = 0; i < controlDescriptors.size(); i++) { 636 ControlDescriptor tmp = (ControlDescriptor)controlDescriptors.get(i); 637 if (tmp.getControlId().equals(id)) { 638 controlDescriptors.set(i, cd); 639 controlDescriptorMap.put(id, cd); 640 break; 641 } 642 } 643 } 644 } 645 646 // pop up an incredibly rudimentary window that controls layer viz animation. 647 public void showLayerVisibilityAnimator() { 648 logger.trace("probably should try to do something here."); 649 SwingUtilities.invokeLater(() -> { 650 try { 651 LayerAnimationWindow window = new LayerAnimationWindow(); 652 window.setVisible(true); 653 } catch (Exception e) { 654 logger.error("oh no! something happened!", e); 655 } 656 }); 657 } 658 659 /** 660 * Handles removing all loaded data sources. 661 * 662 * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method 663 * will ignore the user's preferences and remove all data sources. 664 * 665 * @param showWarning Whether or not to display a warning message before 666 * removing <i>all</i> data sources. See the return details for more. 667 * 668 * @return Either {@code true} if the user wants to continue showing the 669 * warning dialog, or {@code false} if they've elected to stop showing the 670 * warning. If {@code showWarning} is {@code false}, this method will 671 * always return {@code false}, as the user isn't interested in seeing the 672 * warning. 673 */ 674 public boolean removeAllData(final boolean showWarning) { 675 boolean reallyRemove = false; 676 boolean continueWarning = true; 677 678 if (getArgsManager().getIsOffScreen()) { 679 super.removeAllDataSources(); 680 return continueWarning; 681 } 682 683 if (showWarning) { 684 Set<WarningResult> result = showWarningDialog( 685 "Confirm Data Removal", 686 "This action will remove all of the data currently loaded in McIDAS-V.<br>Is this what you want to do?", 687 Constants.PREF_CONFIRM_REMOVE_DATA, 688 "Always ask?", 689 "Remove all data", 690 "Do not remove any data"); 691 reallyRemove = result.contains(WarningResult.OK); 692 continueWarning = result.contains(WarningResult.SHOW); 693 } else { 694 // user doesn't want to see warning messages. 695 reallyRemove = true; 696 continueWarning = false; 697 } 698 699 if (reallyRemove) { 700 super.removeAllDataSources(); 701 } 702 703 return continueWarning; 704 } 705 706 /** 707 * Handles removing all loaded layers ({@literal "displays"} in IDV-land). 708 * 709 * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method 710 * will ignore the user's preferences and remove all layers. 711 * 712 * @param showWarning Whether or not to display a warning message before 713 * removing <i>all</i> layers. See the return details for more. 714 * 715 * @return Either {@code true} if the user wants to continue showing the 716 * warning dialog, or {@code false} if they've elected to stop showing the 717 * warning. If {@code showWarning} is {@code false}, this method will 718 * always return {@code false}, as the user isn't interested in seeing the 719 * warning. 720 */ 721 public boolean removeAllLayers(final boolean showWarning) { 722 boolean reallyRemove = false; 723 boolean continueWarning = true; 724 725 if (getArgsManager().getIsOffScreen()) { 726 super.removeAllDisplays(); 727 ((ViewManagerManager)getVMManager()).disableAllLayerVizAnimations(); 728 return continueWarning; 729 } 730 731 if (showWarning) { 732 Set<WarningResult> result = showWarningDialog( 733 "Confirm Layer Removal", 734 "This action will remove every layer currently loaded in McIDAS-V.<br>Is this what you want to do?", 735 Constants.PREF_CONFIRM_REMOVE_LAYERS, 736 "Always ask?", 737 "Remove all layers", 738 "Do not remove any layers"); 739 reallyRemove = result.contains(WarningResult.OK); 740 continueWarning = result.contains(WarningResult.SHOW); 741 } else { 742 // user doesn't want to see warning messages. 743 reallyRemove = true; 744 continueWarning = false; 745 } 746 747 if (reallyRemove) { 748 super.removeAllDisplays(); 749 ((ViewManagerManager)getVMManager()).disableAllLayerVizAnimations(); 750 } 751 752 return continueWarning; 753 } 754 755 /** 756 * Overridden so that McIDAS-V can prompt the user before removing, if 757 * necessary. 758 */ 759 @Override public void removeAllDataSources() { 760 IdvObjectStore store = getStore(); 761 boolean showWarning = 762 store.get(Constants.PREF_CONFIRM_REMOVE_DATA, true); 763 showWarning = removeAllData(showWarning); 764 store.put(Constants.PREF_CONFIRM_REMOVE_DATA, showWarning); 765 } 766 767 /** 768 * Overridden so that McIDAS-V can prompt the user before removing, if 769 * necessary. 770 */ 771 @Override public void removeAllDisplays() { 772 IdvObjectStore store = getStore(); 773 boolean showWarning = 774 store.get(Constants.PREF_CONFIRM_REMOVE_LAYERS, true); 775 showWarning = removeAllLayers(showWarning); 776 store.put(Constants.PREF_CONFIRM_REMOVE_LAYERS, showWarning); 777 } 778 779 /** 780 * Handles removing all loaded layers ({@literal "displays"} in IDV-land) 781 * and data sources. 782 * 783 * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method 784 * will ignore the user's preferences and remove all layers and data. 785 * 786 * @see #removeAllData(boolean) 787 * @see #removeAllLayers(boolean) 788 */ 789 public void removeAllLayersAndData() { 790 boolean reallyRemove = false; 791 boolean continueWarning = true; 792 793 if (getArgsManager().getIsOffScreen()) { 794 removeAllData(false); 795 removeAllLayers(false); 796 } 797 798 IdvObjectStore store = getStore(); 799 boolean showWarning = store.get(Constants.PREF_CONFIRM_REMOVE_BOTH, true); 800 if (showWarning) { 801 Set<WarningResult> result = showWarningDialog( 802 "Confirm Removal", 803 "This action will remove all of your currently loaded layers and data.<br>Is this what you want to do?", 804 Constants.PREF_CONFIRM_REMOVE_BOTH, 805 "Always ask?", 806 "Remove all layers and data", 807 "Do not remove anything"); 808 reallyRemove = result.contains(WarningResult.OK); 809 continueWarning = result.contains(WarningResult.SHOW); 810 } else { 811 // user doesn't want to see warning messages. 812 reallyRemove = true; 813 continueWarning = false; 814 } 815 816 // don't show the individual warning messages as the user has attempted 817 // to remove *both* 818 if (reallyRemove) { 819 removeAllData(false); 820 removeAllLayers(false); 821 } 822 823 store.put(Constants.PREF_CONFIRM_REMOVE_BOTH, continueWarning); 824 } 825 826 /** 827 * Helper method for showing the removal warning dialog. Note that none of 828 * these parameters should be {@code null} or empty. 829 * 830 * @param title Title of the warning dialog. 831 * @param message Contents of the warning. May contain HTML, but you do 832 * not need to provide opening and closing {@literal "html"} tags. 833 * @param prefId ID of the preference that controls whether or not the 834 * dialog should be displayed. 835 * @param prefLabel Brief description of the preference. 836 * @param okLabel Text of button that signals removal. 837 * @param cancelLabel Text of button that signals cancelling removal. 838 * 839 * @return A {@code Set} of {@link WarningResult WarningResults} that 840 * describes what the user opted to do. Should always contain only 841 * <b>two</b> elements. One for whether or not {@literal "ok"} or 842 * {@literal "cancel"} was clicked, and one for whether or not the warning 843 * should continue to be displayed. 844 */ 845 private Set<WarningResult> showWarningDialog(final String title, 846 final String message, final String prefId, final String prefLabel, 847 final String okLabel, final String cancelLabel) 848 { 849 JCheckBox box = new JCheckBox(prefLabel, true); 850 JComponent comp = GuiUtils.vbox( 851 new JLabel("<html>"+message+"</html>"), 852 GuiUtils.inset(box, new Insets(4, 15, 0, 10))); 853 854 Object[] options = { okLabel, cancelLabel }; 855 int result = JOptionPane.showOptionDialog( 856 LogUtil.getCurrentWindow(), // parent 857 comp, // msg 858 title, // title 859 JOptionPane.YES_NO_OPTION, // option type 860 JOptionPane.WARNING_MESSAGE, // message type 861 (Icon)null, // icon? 862 options, // selection values 863 options[1]); // initial? 864 865 WarningResult button = WarningResult.CANCEL; 866 if (result == JOptionPane.YES_OPTION) { 867 button = WarningResult.OK; 868 } 869 870 WarningResult show = WarningResult.HIDE; 871 if (box.isSelected()) { 872 show = WarningResult.SHOW; 873 } 874 875 return EnumSet.of(button, show); 876 } 877 878 public void removeTabData() { 879 } 880 881 public void removeTabLayers() { 882 883 } 884 885 public void removeTabLayersAndData() { 886 } 887 888 /** 889 * Overridden so that McIDAS-V doesn't have to create an entire new 890 * {@link ucar.unidata.idv.ui.IdvWindow} if 891 * {@link VMManager#findViewManager(ViewDescriptor)} can't find an 892 * appropriate ViewManager for {@code viewDescriptor}. 893 * 894 * <p>Not doing the above causes McIDAS-V to get stuck in a window creation 895 * loop.</p> 896 */ 897 @Override public ViewManager getViewManager(ViewDescriptor viewDescriptor, 898 boolean newWindow, String properties) 899 { 900 ViewManager vm = 901 getVMManager().findOrCreateViewManager(viewDescriptor, properties); 902 if (vm == null) { 903 vm = super.getViewManager(viewDescriptor, newWindow, properties); 904 } 905 return vm; 906 } 907 908 /** 909 * Returns a reference to the current McIDAS-V object. Useful for working 910 * inside static methods. <b>Always check for null when using this 911 * method</b>. 912 * 913 * @return Either the current McIDAS-V "god object" or {@code null}. 914 */ 915 public static McIDASV getStaticMcv() { 916 return staticMcv; 917 } 918 919 /** 920 * @see ucar.unidata.idv.IdvBase#setIdv(ucar.unidata.idv.IntegratedDataViewer) 921 */ 922 @Override public void setIdv(IntegratedDataViewer idv) { 923 this.idv = idv; 924 } 925 926 /** 927 * Load the McV properties. All other property files are disregarded. 928 * 929 * @see ucar.unidata.idv.IntegratedDataViewer#initPropertyFiles(java.util.List) 930 */ 931 @Override public void initPropertyFiles(List files) { 932 files.clear(); 933 files.add(Constants.PROPERTIES_FILE); 934 } 935 936 /** 937 * Makes {@link PersistenceManager} save off a default {@literal "layout"} 938 * bundle. 939 */ 940 public void doSaveAsDefaultLayout() { 941 Misc.run(() -> ((PersistenceManager)getPersistenceManager()).doSaveAsDefaultLayout()); 942 } 943 944 /** 945 * Determines whether or not a default layout exists. 946 * 947 * @return {@code true} if there is a default layout, {@code false} 948 * otherwise. 949 */ 950 public boolean hasDefaultLayout() { 951 String path = 952 getResourceManager().getResources(IdvResourceManager.RSC_BUNDLES) 953 .getWritable(); 954 return new File(path).exists(); 955 } 956 957 /** 958 * Called from the menu command to clear the default bundle. Overridden 959 * in McIDAS-V so that we reference the <i>layout</i> rather than the 960 * bundle. 961 */ 962 @Override public void doClearDefaults() { 963 if (GuiUtils.showYesNoDialog(null, 964 "Are you sure you want to delete your default layout?", 965 "Delete confirmation")) { 966 resourceManager.clearDefaultBundles(); 967 } 968 } 969 970 /** 971 * Returns the time it took for McIDAS-V to start up. 972 * 973 * @return These results are created from subtracting the results of two 974 * {@link System#nanoTime()} calls against one another. 975 */ 976 public long getStartupDuration() { 977 return estimate; 978 } 979 980 /** 981 * <p> 982 * Overridden so that the support form becomes non-modal if launched from 983 * an exception dialog. 984 * </p> 985 * 986 * @see IntegratedDataViewer#addErrorButtons(JDialog, List, String, Throwable) 987 */ 988 @Override public void addErrorButtons(final JDialog dialog, 989 List buttonList, final String msg, final Throwable exc) 990 { 991 JButton supportBtn = new JButton("Support Form"); 992 supportBtn.addActionListener(ae -> 993 getIdvUIManager().showSupportForm(msg, 994 LogUtil.getStackTrace(exc), 995 null)); 996 buttonList.add(supportBtn); 997 } 998 999 /** 1000 * This method is useful for storing commandline {@literal "properties"} 1001 * with the user's preferences. 1002 */ 1003 private void overridePreferences() { 1004 StateManager stateManager = (StateManager)getStateManager(); 1005 int renderThreads = getMaxRenderThreadCount(); 1006 stateManager.putPreference(PREF_THREADS_RENDER, renderThreads); 1007 stateManager.putPreference(PREF_THREADS_DATA, getMaxDataThreadCount()); 1008 visad.util.ThreadManager.setGlobalMaxThreads(renderThreads); 1009 } 1010 1011 /** 1012 * Determine if the last {@literal "exit"} was clean--whether or not 1013 * {@code SESSION_FILE} was removed before the McIDAS-V process terminated. 1014 * 1015 * <p>If the exit was not clean, the user is prompted to submit a support 1016 * request.</p> 1017 */ 1018 private void detectAndHandleCrash() { 1019 GuiUtils.setApplicationTitle(""); 1020 if (cleanExit || getArgsManager().getIsOffScreen()) { 1021 return; 1022 } 1023 1024 String msg = "The previous McIDAS-V session did not exit cleanly.<br>"+ 1025 "Do you want to send the log file to the McIDAS Help Desk?"; 1026 if (previousStart != null) { 1027 msg = "The previous McIDAS-V session (start time: %s) did not exit cleanly.<br>"+ 1028 "Do you want to send the log file to the McIDAS Help Desk?"; 1029 msg = String.format(msg, previousStart); 1030 } 1031 1032 boolean continueAsking = getStore().get("mcv.crash.boom.send.report", true); 1033 if (!continueAsking) { 1034 return; 1035 } 1036 1037 Set<WarningResult> result = showWarningDialog( 1038 "Report Crash", 1039 msg, 1040 "mcv.crash.boom.send.report", 1041 "Always ask?", 1042 "Open support form", 1043 "Do not report"); 1044 1045 getStore().put("mcv.crash.boom.send.report", result.contains(WarningResult.SHOW)); 1046 if (!result.contains(WarningResult.OK)) { 1047 return; 1048 } 1049 1050 getIdvUIManager().showSupportForm(); 1051 } 1052 1053 /** 1054 * Called after the IDV has finished setting everything up after starting. 1055 * McIDAS-V is currently only using this method to determine if the last 1056 * {@literal "exit"} was clean--whether or not {@code SESSION_FILE} was 1057 * removed before the McIDAS-V process terminated. 1058 * 1059 * Called after the IDV has finished setting everything up. McIDAS-V uses 1060 * this method to handle: 1061 * 1062 * <ul> 1063 * <li>Clearing out the automatic display creation arguments.</li> 1064 * <li>Presence of certain properties on the commandline.</li> 1065 * <li>Detection and handling of a crashed McIDAS-V session.</li> 1066 * <li>Run action specified by {@code -doaction} flag (if any).</li> 1067 * <li>Allowing tooltips to remain visible for more than 4 seconds.</li> 1068 * </ul> 1069 * 1070 * @see ArgumentManager#clearAutomaticDisplayArgs() 1071 * @see #overridePreferences() 1072 * @see #detectAndHandleCrash() 1073 */ 1074 @Override public void initDone() { 1075 ((ArgumentManager)argsManager).clearAutomaticDisplayArgs(); 1076 1077 overridePreferences(); 1078 1079 detectAndHandleCrash(); 1080 1081 if (addeEntries == null) { 1082 getServerManager(); 1083 } 1084 addeEntries.startLocalServer(); 1085 1086 estimate = System.nanoTime() - startTime; 1087 logger.info("estimated startup duration: {} ms", estimate / 1.0e6); 1088 System.setProperty("mcv.start.duration", Long.toString(estimate)); 1089 1090 // handle the -doAction <action id> startup option. 1091 ((ArgumentManager)getArgsManager()).runStartupAction(); 1092 1093 // disable idiotic tooltip dismissal (seriously, 4 seconds!?) 1094 ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE); 1095 1096 // turn on directory monitoring in the file choosers. 1097 startWatchService(); 1098 EventBus.publish(Constants.EVENT_FILECHOOSER_START, "init finished"); 1099 1100 EventDispatchThreadHangMonitor.initMonitoring(); 1101 } 1102 1103 /** 1104 * @see IntegratedDataViewer#doOpen(String, boolean, boolean) 1105 */ 1106 @Override public void doOpen(final String filename, 1107 final boolean checkUserPreference, final boolean andRemove) 1108 { 1109 doOpenInThread(filename, checkUserPreference, andRemove); 1110 } 1111 1112 /** 1113 * Have the user select a bundle. If andRemove is true then we remove all 1114 * data sources and displays. 1115 * 1116 * Then we open the bundle and start doing unpersistence things. 1117 * 1118 * @param filename The filename to open 1119 * @param checkUserPreference Should we show, if needed, the 1120 * {@literal "open"} dialog 1121 * @param andRemove If true then first remove all data sources and displays 1122 */ 1123 private void doOpenInThread(String filename, boolean checkUserPreference, 1124 boolean andRemove) 1125 { 1126 boolean overwriteData = false; 1127 if (filename == null) { 1128 if (overwriteDataCbx.getToolTipText() == null) { 1129 overwriteDataCbx.setToolTipText("Change the file paths that the data sources use"); 1130 } 1131 1132 filename = FileManager.getReadFile("Open File", 1133 ((ArgumentManager)getArgsManager()).getBundleFilters(true), 1134 GuiUtils.top(overwriteDataCbx)); 1135 1136 if (filename == null) { 1137 return; 1138 } 1139 1140 overwriteData = overwriteDataCbx.isSelected(); 1141 } 1142 1143 if (ArgumentManager.isXmlBundle(filename)) { 1144 getPersistenceManager().decodeXmlFile(filename, 1145 checkUserPreference, overwriteData); 1146 return; 1147 } 1148 handleAction(filename, null); 1149 } 1150 1151 /** 1152 * Factory method to create the McIDAS-V @link JythonManager}. 1153 * 1154 * @return New {@code JythonManager}. 1155 */ 1156 @Override protected JythonManager doMakeJythonManager() { 1157 logger.debug("returning a new JythonManager"); 1158 return new JythonManager(this); 1159 } 1160 1161 /** 1162 * Factory method to create a McIDAS-V {@link CollaborationManager}. 1163 * 1164 * @return New {@code CollaborationManager}. 1165 */ 1166 @Override protected CollabManager doMakeCollabManager() { 1167 return new CollaborationManager(this); 1168 } 1169 1170 /** 1171 * Factory method to create a McIDAS-V {@link McIdasChooserManager}. 1172 * Here we create our own manager so it can do things specific to McIDAS-V. 1173 * 1174 * @return {@code McIdasChooserManager} indicated by the startup properties. 1175 * 1176 * @see ucar.unidata.idv.IdvBase#doMakeIdvChooserManager() 1177 */ 1178 @Override 1179 protected IdvChooserManager doMakeIdvChooserManager() { 1180 chooserManager = (McIdasChooserManager)makeManager( 1181 McIdasChooserManager.class, new Object[] { this }); 1182 chooserManager.init(); 1183 return chooserManager; 1184 } 1185 1186 /** 1187 * Factory method to create the {@link IdvUIManager}. Here we create our 1188 * own UI manager so it can do things specific to McIDAS-V. 1189 * 1190 * @return {@link UIManager} indicated by the startup properties. 1191 * 1192 * @see ucar.unidata.idv.IdvBase#doMakeIdvUIManager() 1193 */ 1194 @Override 1195 protected IdvUIManager doMakeIdvUIManager() { 1196 return new UIManager(this); 1197 } 1198 1199 /** 1200 * Create our own VMManager so that we can make the tabs play nice. 1201 * @see ucar.unidata.idv.IdvBase#doMakeVMManager() 1202 */ 1203 @Override 1204 protected VMManager doMakeVMManager() { 1205 // what an ugly class name :( 1206 return new ViewManagerManager(this); 1207 } 1208 1209 /** 1210 * Make the {@link McIdasPreferenceManager}. 1211 * @see ucar.unidata.idv.IdvBase#doMakePreferenceManager() 1212 */ 1213 @Override 1214 protected IdvPreferenceManager doMakePreferenceManager() { 1215 return new McIdasPreferenceManager(this); 1216 } 1217 1218 /** 1219 * <p>McIDAS-V (alpha 10+) needs to handle both IDV bundles without 1220 * component groups and all bundles from prior McV alphas. You better 1221 * believe we need to extend the persistence manager functionality!</p> 1222 * 1223 * @see ucar.unidata.idv.IdvBase#doMakePersistenceManager() 1224 */ 1225 @Override protected IdvPersistenceManager doMakePersistenceManager() { 1226 return new PersistenceManager(this); 1227 } 1228 1229 /** 1230 * Create, if needed, and return the {@link McIdasChooserManager}. 1231 * 1232 * @return The Chooser manager 1233 */ 1234 public McIdasChooserManager getMcIdasChooserManager() { 1235 return (McIdasChooserManager)getIdvChooserManager(); 1236 } 1237 1238 /** 1239 * Returns the {@link MonitorManager}. 1240 * 1241 * @return McIDAS-V {@literal "monitor manager"}. 1242 */ 1243 public MonitorManager getMonitorManager() { 1244 return monitorManager; 1245 } 1246 1247 /** 1248 * Responds to events generated by the server manager's GUI. Currently 1249 * limited to {@link edu.wisc.ssec.mcidasv.servermanager.TabbedAddeManager.Event#CLOSED TabbedAddeManager.Event#CLOSED}. 1250 * 1251 * @param evt {@code TabbedAddeManager} event to respond to. 1252 */ 1253 @EventSubscriber(eventClass=TabbedAddeManager.Event.class) 1254 public void onServerManagerWindowEvent(TabbedAddeManager.Event evt) { 1255 if (evt == TabbedAddeManager.Event.CLOSED) { 1256 tabbedAddeManager = null; 1257 } 1258 } 1259 1260 /** 1261 * Creates (if needed) the server manager GUI and displays it. 1262 */ 1263 public void showServerManager() { 1264 if (tabbedAddeManager == null) { 1265 tabbedAddeManager = new TabbedAddeManager(getServerManager()); 1266 } 1267 tabbedAddeManager.showManager(); 1268 } 1269 1270 /** 1271 * Creates a new server manager (if needed) and returns it. 1272 * 1273 * @return The McIDAS-V ADDE server manager. 1274 */ 1275 public EntryStore getServerManager() { 1276 if (addeEntries == null) { 1277 addeEntries = new EntryStore(getStore(), getResourceManager()); 1278 } 1279 return addeEntries; 1280 } 1281 1282 public McvDataManager getMcvDataManager() { 1283 return (McvDataManager)getDataManager(); 1284 } 1285 1286 /** 1287 * Get McIDASV. 1288 * @see ucar.unidata.idv.IdvBase#getIdv() 1289 */ 1290 @Override public IntegratedDataViewer getIdv() { 1291 return this; 1292 } 1293 1294 /** 1295 * Creates a McIDAS-V argument manager so that McV can handle some non-IDV 1296 * command line things. 1297 * 1298 * @param args The arguments from the command line. 1299 * 1300 * @see ucar.unidata.idv.IdvBase#doMakeArgsManager(java.lang.String[]) 1301 */ 1302 @Override protected ArgsManager doMakeArgsManager(String[] args) { 1303 return new ArgumentManager(this, args); 1304 } 1305 1306 /** 1307 * Factory method to create the {@link McvDataManager}. 1308 * 1309 * @return The data manager 1310 * 1311 * @see ucar.unidata.idv.IdvBase#doMakeDataManager() 1312 */ 1313 @Override protected DataManager doMakeDataManager() { 1314 return new McvDataManager(this); 1315 } 1316 1317 /** 1318 * Make the McIDAS-V {@link StateManager}. 1319 * @see ucar.unidata.idv.IdvBase#doMakeStateManager() 1320 */ 1321 @Override protected StateManager doMakeStateManager() { 1322 return new StateManager(this); 1323 } 1324 1325 /** 1326 * Make the McIDAS-V {@link ResourceManager}. 1327 * @see ucar.unidata.idv.IdvBase#doMakeResourceManager() 1328 */ 1329 @Override protected IdvResourceManager doMakeResourceManager() { 1330 return new ResourceManager(this); 1331 } 1332 1333 /** 1334 * Make the {@link McIdasColorTableManager}. 1335 * @see ucar.unidata.idv.IdvBase#doMakeColorTableManager() 1336 */ 1337 @Override protected ColorTableManager doMakeColorTableManager() { 1338 return new McIdasColorTableManager(); 1339 } 1340 1341 /** 1342 * Factory method to create the {@link McvPluginManager}. 1343 * 1344 * @return The McV plugin manager. 1345 * 1346 * @see ucar.unidata.idv.IdvBase#doMakePluginManager() 1347 */ 1348 @Override protected PluginManager doMakePluginManager() { 1349 return new McvPluginManager(this); 1350 } 1351 1352// /** 1353// * Make the {@link edu.wisc.ssec.mcidasv.data.McIDASVProjectionManager}. 1354// * @see ucar.unidata.idv.IdvBase#doMakeIdvProjectionManager() 1355// */ 1356// @Override 1357// protected IdvProjectionManager doMakeIdvProjectionManager() { 1358// return new McIDASVProjectionManager(this); 1359// } 1360 1361 /** 1362 * Make a help button for a particular help topic 1363 * 1364 * @param helpId the topic id 1365 * @param toolTip the tooltip 1366 * 1367 * @return the button 1368 */ 1369 @Override public JComponent makeHelpButton(String helpId, String toolTip) { 1370 JButton btn = McVGuiUtils.makeImageButton(Constants.ICON_HELP, 1371 getIdvUIManager(), "showHelp", helpId, "Show help"); 1372 1373 if (toolTip != null) { 1374 btn.setToolTipText(toolTip); 1375 } 1376 return btn; 1377 } 1378 1379 /** 1380 * Return the current {@literal "userpath"}. 1381 * 1382 * @return Path to the user's {@literal "McIDAS-V directory"}. 1383 */ 1384 public String getUserDirectory() { 1385 return StartupManager.getInstance().getPlatform().getUserDirectory(); 1386 } 1387 1388 /** 1389 * Return the path to a file within {@literal "userpath"}. 1390 * 1391 * @param filename File within the userpath. 1392 * 1393 * @return Path to a file within the user's {@literal "McIDAS-V directory"}. 1394 * No path validation is performed, so please be aware that the returned 1395 * path may not exist. 1396 */ 1397 public String getUserFile(String filename) { 1398 return StartupManager.getInstance().getPlatform().getUserFile(filename); 1399 } 1400 1401 /** 1402 * Invokes the main method for a given class. 1403 * 1404 * <p>Note: this is rather limited so far as it doesn't pass in any 1405 * arguments.</p> 1406 * 1407 * @param className Class whose main method is to be invoked. Cannot be 1408 * {@code null}. 1409 */ 1410 public void runPluginMainMethod(final String className) { 1411 final String[] dummyArgs = { "" }; 1412 try { 1413 Class<?> clazz = Misc.findClass(className); 1414 Class[] args = new Class[] { dummyArgs.getClass() }; 1415 Method mainMethod = Misc.findMethod(clazz, "main", args); 1416 if (mainMethod != null) { 1417 mainMethod.invoke(null, new Object[] { dummyArgs }); 1418 } 1419 } catch (Exception e) { 1420 logger.error("problem with plugin class", e); 1421 LogUtil.logException("problem running main method for class: "+className, e); 1422 } 1423 } 1424 1425 /** 1426 * Attempts to determine if a given string is a 1427 * {@literal "loopback address"} (aka localhost). 1428 * 1429 * <p>Strings are <b>trimmed and converted to lowercase</b>, and currently 1430 * checked against: 1431 * <ul> 1432 * <li>{@code 127.0.0.1}</li> 1433 * <li>{@code ::1} (for IPv6)</li> 1434 * <li>Strings starting with {@code localhost}.</li> 1435 * </ul> 1436 * 1437 * @param host {@code String} to check. Should not be {@code null}. 1438 * 1439 * @return {@code true} if {@code host} is a recognized loopback address. 1440 * {@code false} otherwise. 1441 * 1442 * @throws NullPointerException if {@code host} is {@code null}. 1443 */ 1444 public static boolean isLoopback(final String host) { 1445 String cleaned = Objects.requireNonNull(host.trim().toLowerCase()); 1446 return "127.0.0.1".startsWith(cleaned) 1447 || "::1".startsWith(cleaned) 1448 || cleaned.startsWith("localhost"); 1449 } 1450 1451 /** 1452 * Are we on a Mac? Used to build the MRJ handlers, taken from TN2110. 1453 * 1454 * @return {@code true} if this session is running on top of OS X, 1455 * {@code false} otherwise. 1456 * 1457 * @see <a href="http://developer.apple.com/technotes/tn2002/tn2110.html">TN2110</a> 1458 */ 1459 public static boolean isMac() { 1460 String osName = System.getProperty("os.name"); 1461 return osName.contains("OS X"); 1462 } 1463 1464 /** 1465 * Queries the {@code os.name} system property and if the result does not 1466 * start with {@literal "Windows"}, the platform is assumed to be 1467 * {@literal "unix-like"}. 1468 * 1469 * <p>Given the McIDAS-V supported platforms (Windows, {@literal "Unix"}, 1470 * and OS X), the above logic is safe. 1471 * 1472 * @return {@code true} if we're not running on Windows, {@code false} 1473 * otherwise. 1474 * 1475 * @throws RuntimeException if there is no property associated with 1476 * {@code os.name}. 1477 */ 1478 public static boolean isUnixLike() { 1479 String osName = System.getProperty("os.name"); 1480 if (osName == null) { 1481 throw new RuntimeException("no os.name system property!"); 1482 } 1483 1484 if (System.getProperty("os.name").startsWith("Windows")) { 1485 return false; 1486 } 1487 return true; 1488 } 1489 1490 /** 1491 * Queries the {@code os.name} system property and if the result starts 1492 * with {@literal "Windows"}, the platform is assumed to be Windows. Duh. 1493 * 1494 * @return {@code true} if we're running on Windows, {@code false} 1495 * otherwise. 1496 * 1497 * @throws RuntimeException if there is no property associated with 1498 * {@code os.name}. 1499 */ 1500 public static boolean isWindows() { 1501 String osName = System.getProperty("os.name"); 1502 if (osName == null) { 1503 throw new RuntimeException("no os.name system property!"); 1504 } 1505 1506 return osName.startsWith("Windows"); 1507 } 1508 1509 /** 1510 * If McIDAS-V is running on Windows, this method will return a 1511 * {@code String} that looks like {@literal "C:"} or {@literal "D:"}, etc. 1512 * 1513 * <p>If McIDAS-V is not running on Windows, this method will return an 1514 * empty {@code String}. 1515 * 1516 * @return Either the {@literal "drive letter"} of the {@code java.home} 1517 * property or an empty {@code String} if McIDAS-V isn't running on Windows. 1518 * 1519 * @throws RuntimeException if there is no property associated with 1520 * {@code java.home}. 1521 */ 1522 public static String getJavaDriveLetter() { 1523 if (!isWindows()) { 1524 return ""; 1525 } 1526 1527 String home = System.getProperty("java.home"); 1528 if (home == null) { 1529 throw new RuntimeException("no java.home system property!"); 1530 } 1531 1532 return home.substring(0, 2); 1533 } 1534 1535 /** 1536 * Attempts to create a {@literal "session"} file. This method will create 1537 * a {@literal "userpath"} if it does not already exist. 1538 * 1539 * @param path Path of the session file that should get created. 1540 * {@code null} values are not allowed, and sufficient priviledges are 1541 * assumed. 1542 * 1543 * @throws AssertionError if McIDAS-V couldn't write to {@code path}. 1544 * 1545 * @see #SESSION_FILE 1546 * @see #hadCleanExit(String) 1547 * @see #removeSessionFile(String) 1548 */ 1549 private static void createSessionFile(final String path) { 1550 assert path != null : "Cannot create a null path"; 1551 FileOutputStream out = null; 1552 PrintStream p = null; 1553 1554 File dir = new File(StartupManager.getInstance().getPlatform().getUserDirectory()); 1555 if (!dir.exists()) { 1556 dir.mkdir(); 1557 } 1558 1559 try { 1560 out = new FileOutputStream(path); 1561 p = new PrintStream(out); 1562 p.println(new Date().getTime()); 1563 } catch (Exception e) { 1564 throw new AssertionError("Could not write to "+path+". Error message: "+e.getMessage(), e); 1565 } finally { 1566 if (p != null) { 1567 p.close(); 1568 } 1569 if (out != null) { 1570 try { 1571 out.close(); 1572 } catch (IOException e) { 1573 throw new AssertionError("Could not close "+path+". Error message: "+e.getMessage(), e); 1574 } 1575 } 1576 } 1577 } 1578 1579 /** 1580 * Attempts to extract a timestamp from {@code path}. {@code path} is 1581 * expected to <b>only</b> contain a single line consisting of a 1582 * {@link Long} integer. 1583 * 1584 * @param path Path to the file of interest. 1585 * 1586 * @return Either a {@link Date} of the timestamp contained in 1587 * {@code path} or {@code null} if the extraction failed. 1588 */ 1589 private static Date extractDate(final String path) { 1590 assert path != null; 1591 Date savedDate = null; 1592 BufferedReader reader = null; 1593 try { 1594 reader = new BufferedReader(new FileReader(path)); 1595 String line = reader.readLine(); 1596 if (line != null) { 1597 savedDate = new Date(Long.parseLong(line.trim())); 1598 } 1599 } catch (Exception e) { 1600 logger.trace("problem extracting the date!", e); 1601 } finally { 1602 if (reader != null) { 1603 try { 1604 reader.close(); 1605 } catch (IOException e) { 1606 logger.trace("problem closing session file!", e); 1607 } 1608 } 1609 } 1610 return savedDate; 1611 } 1612 1613 /** 1614 * Attempts to remove the file accessible via {@code path}. 1615 * 1616 * @param path Path of the file that'll get removed. This should be 1617 * non-null and point to an existing and writable filename (not a 1618 * directory). 1619 * 1620 * @throws AssertionError if the file at {@code path} could not be 1621 * removed. 1622 * 1623 * @see #SESSION_FILE 1624 * @see #createSessionFile(String) 1625 * @see #hadCleanExit(String) 1626 */ 1627 private static void removeSessionFile(final String path) { 1628 if (path == null) { 1629 return; 1630 } 1631 1632 File f = new File(path); 1633 1634 if (!f.exists() || !f.canWrite() || f.isDirectory()) { 1635 return; 1636 } 1637 1638 if (!f.delete()) { 1639 throw new AssertionError("Could not delete session file"); 1640 } 1641 } 1642 1643 /** 1644 * Tries to determine whether or not the last McIDAS-V session ended 1645 * {@literal "cleanly"}. Currently a simple check for a 1646 * {@literal "session"} file that is created upon starting and removed upon 1647 * ending. 1648 * 1649 * @param path Path to the session file to check. Can't be {@code null}. 1650 * 1651 * @return Either {@code true} if the file pointed at by {@code path} does 1652 * <b><i>NOT</i></b> exist, {@code false} if it does exist. 1653 * 1654 * @see #SESSION_FILE 1655 * @see #createSessionFile(String) 1656 * @see #removeSessionFile(String) 1657 */ 1658 private static boolean hadCleanExit(final String path) { 1659 assert path != null : "Cannot test for a null path"; 1660 return !(new File(path).exists()); 1661 } 1662 1663 /** 1664 * Returns the (<i>current</i>) path to the session file. Note that the 1665 * location of the file may change arbitrarily. 1666 * 1667 * @return {@code String} pointing to the session file. 1668 * 1669 * @see #SESSION_FILE 1670 */ 1671 public static String getSessionFilePath() { 1672 return StartupManager.getInstance().getPlatform().getUserFile("session.tmp"); 1673 } 1674 1675 /** 1676 * Useful for providing the startup manager with values other than the 1677 * defaults... Note that this method attempts to update the value of 1678 * {@link #SESSION_FILE}. 1679 * 1680 * @param args Likely the argument array coming from the main method. 1681 */ 1682 private static void applyArgs(final String[] args) { 1683 assert args != null : "Cannot use a null argument array"; 1684 StartupManager.applyArgs(true, false, args); 1685 SESSION_FILE = getSessionFilePath(); 1686 } 1687 1688 /** 1689 * This returns the set of {@link ControlDescriptor ControlDescriptors} 1690 * that can be shown. The ordering of this list determines the 1691 * "default" controls shown in the Field Selector, so we override 1692 * here for control over the ordering. 1693 * 1694 * @param includeTemplates If true then include the display templates 1695 * @return re-ordered List of shown control descriptors 1696 */ 1697 @Override public List getControlDescriptors(boolean includeTemplates) { 1698 List<ControlDescriptor> l = 1699 cast(super.getControlDescriptors(includeTemplates)); 1700 for (int i = 0; i < l.size(); i++) { 1701 ControlDescriptor cd = l.get(i); 1702 if (cd.getControlId().equals("omni")) { 1703 // move the omni control to the end of the list 1704 // so it will never be "default" in Field Selector 1705 l.remove(i); 1706 l.add(cd); 1707 } 1708 1709 Hashtable<String, String> props = cast(cd.getProperties()); 1710 String v = props.getOrDefault("disabled", "false"); 1711 if (Objects.equals(v, "true")) { 1712 l.remove(i); 1713 } 1714 } 1715 return l; 1716 } 1717 1718 /** 1719 * Show the McIDAS-V {@literal "Welcome Window"} for the first start up. 1720 * 1721 * @param args Commandline arguments, used to handle autoquit stress testing. 1722 */ 1723 private static void handleWelcomeWindow(String... args) { 1724 boolean showWelcome = false; 1725 boolean welcomeAutoQuit = false; 1726 long welcomeAutoQuitDelay = WelcomeWindow.DEFAULT_QUIT_DELAY; 1727 for (int i = 0; i < args.length; i++) { 1728 if ("-welcomewindow".equals(args[i])) { 1729 showWelcome = true; 1730 } else if ("-autoquit".equals(args[i])) { 1731 welcomeAutoQuit = true; 1732 int delayIdx = i + 1; 1733 if (delayIdx < args.length) { 1734 welcomeAutoQuitDelay = Long.parseLong(args[delayIdx]); 1735 } 1736 } 1737 } 1738 1739 // if user elects to quit, System.exit(1) will be called. 1740 // if the user decides to start, the welcome window will be simply be 1741 // closed, allowing McV to continue starting up. 1742 if (showWelcome) { 1743 WelcomeWindow ww; 1744 if (welcomeAutoQuit) { 1745 ww = new WelcomeWindow(true, welcomeAutoQuitDelay); 1746 } else { 1747 ww = new WelcomeWindow(); 1748 } 1749 ww.setVisible(true); 1750 } 1751 } 1752 1753 /** 1754 * Register {@literal "adde"} and {@literal "idvresource"} URL protocols. 1755 * 1756 * <p>This needs to be called pretty early in the McIDAS-V initialization 1757 * process. They're currently being registered immediately after the 1758 * session file is created.</p> 1759 */ 1760 private static void registerProtocolHandlers() { 1761 try { 1762 URL.setURLStreamHandlerFactory(protocol -> { 1763 switch (protocol.toLowerCase()) { 1764 case AddeURL.ADDE_PROTOCOL: 1765 return new AddeURLStreamHandler(); 1766 case PluginManager.PLUGIN_PROTOCOL: 1767 return new IdvResourceStreamHandler(); 1768 default: 1769 return null; 1770 } 1771 }); 1772 } catch (Throwable e) { 1773 logger.error("Could not register protocol handlers!", e); 1774 } 1775 } 1776 1777 /** 1778 * Responsible for handling {@literal "idvresource"} URLs. 1779 * 1780 * <p>Really just a redirect to {@link IOUtil#getURL(String, Class)}.</p> 1781 */ 1782 private static class IdvResourceStreamHandler extends URLStreamHandler { 1783 @Override protected URLConnection openConnection(URL u) 1784 throws IOException 1785 { 1786 return IOUtil.getURL(u.getPath(), McIDASV.class).openConnection(); 1787 } 1788 } 1789 1790 /** 1791 * Configure the logging and create the McIDAS-V object responsible for 1792 * initializing the application session. 1793 * 1794 * @param args Command line arguments. 1795 * 1796 * @throws Exception When something untoward happens. 1797 */ 1798 public static void main(String... args) throws Exception { 1799 // show the welcome window if needed. 1800 // since the welcome window is intended to be a one time thing, 1801 // it probably shouldn't count for the startTime stuff. 1802 handleWelcomeWindow(args); 1803 1804 startTime = System.nanoTime(); 1805 1806 // allow use of the "unlimited strength" crypto policy. 1807 // this becomes the default in 1.8.0_162, but we need to ship with 1808 // 1.8.0_152. 1809 Security.setProperty("crypto.policy", "unlimited"); 1810 1811 // the following two lines are required if we want to embed JavaFX 1812 // widgets into McV (which is Swing). The first line initializes the 1813 // JavaFX runtime, and the second line allows the JavaFX runtime to 1814 // hang around even if there are no JavaFX windows. 1815 JFXPanel dummy = new JFXPanel(); 1816 Platform.setImplicitExit(false); 1817 1818 try { 1819 applyArgs(args); 1820 1821 SysOutOverSLF4J.sendSystemOutAndErrToSLF4J(LogLevel.INFO, LogLevel.WARN); 1822 1823 // Optionally remove existing handlers attached to j.u.l root logger 1824 SLF4JBridgeHandler.removeHandlersForRootLogger(); // (since SLF4J 1.6.5) 1825 1826 // add SLF4JBridgeHandler to j.u.l's root logger, should be done once during 1827 // the initialization phase of your application 1828 SLF4JBridgeHandler.install(); 1829 1830// Properties pythonProps = new Properties(); 1831// logger.trace("calling PythonInterpreter.initialize..."); 1832// PythonInterpreter.initialize(System.getProperties(), pythonProps, new String[] {""}); 1833 1834 LogUtil.configure(); 1835 1836 NetcdfFile.registerIOProvider(GpmIosp.class); 1837 1838 long sysMem = Long.valueOf(SystemState.queryOpSysProps().get("opsys.memory.physical.total")); 1839 logger.info("============================================================================="); 1840 logger.info("Starting McIDAS-V @ {}", new Date()); 1841 logger.info("Versions:"); 1842 logger.info("{}", SystemState.getMcvVersionString()); 1843 logger.info("{}", SystemState.getIdvVersionString()); 1844 logger.info("{}", SystemState.getVisadVersionString()); 1845 logger.info("{}", SystemState.getNcidvVersionString()); 1846 logger.info("{} MB system memory", Math.round(sysMem/1024/1024)); 1847 1848 if (!hadCleanExit(SESSION_FILE)) { 1849 previousStart = extractDate(SESSION_FILE); 1850 } 1851 1852 createSessionFile(SESSION_FILE); 1853 registerProtocolHandlers(); 1854 1855 McIDASV myself = new McIDASV(args); 1856 } catch (IllegalArgumentException e) { 1857 String msg = "McIDAS-V could not initialize itself. "; 1858 String osName = System.getProperty("os.name"); 1859 if (osName.contains("Windows")) { 1860 LogUtil.userErrorMessage(msg+'\n'+e.getMessage()); 1861 } else { 1862 System.err.println(msg+e.getMessage()); 1863 } 1864 } 1865 } 1866 1867 /** 1868 * Attempts a clean shutdown of McIDAS-V. Currently this entails 1869 * suppressing any error dialogs, explicitly killing the 1870 * {@link #addeEntries}, removing {@link #SESSION_FILE}, and disabling 1871 * the directory monitors found in the file choosers. 1872 * 1873 * @param exitCode System exit code to use. 1874 * 1875 * @see IntegratedDataViewer#quit() 1876 */ 1877 @Override protected void exit(int exitCode) { 1878 LogUtil.setShowErrorsInGui(false); 1879 1880 // turn off the directory monitors in the file choosers. 1881 EventBus.publish(Constants.EVENT_FILECHOOSER_STOP, "shutting down"); 1882 stopWatchService(); 1883 1884 if (addeEntries != null) { 1885 addeEntries.saveForShutdown(); 1886 addeEntries.stopLocalServer(); 1887 } 1888 1889 removeSessionFile(SESSION_FILE); 1890 1891 // shut down javafx runtime 1892 Platform.exit(); 1893 1894 logger.info("Exiting McIDAS-V @ {}", new Date()); 1895 1896 System.exit(exitCode); 1897 } 1898 1899 /** 1900 * This method is largely a copy of {@link IntegratedDataViewer#quit()}, 1901 * but allows for some GUI testing. 1902 */ 1903 public boolean autoQuit() { 1904 IdvObjectStore store = getStore(); 1905 1906 boolean showQuitConfirm = store.get(PREF_SHOWQUITCONFIRM, true); 1907 long quitDelay = store.get("mcidasv.autoexit.delay", 3000); 1908 1909 if (showQuitConfirm) { 1910 JCheckBox cbx = new JCheckBox("Always ask", true); 1911 JComponent comp = 1912 GuiUtils.vbox( 1913 new JLabel("<html><b>Do you really want to exit?</b></html>"), 1914 GuiUtils.inset(cbx, new Insets(4, 15, 0, 10))); 1915 1916 JOptionPane pane = new JOptionPane(comp, 1917 JOptionPane.QUESTION_MESSAGE, 1918 JOptionPane.YES_NO_OPTION); 1919 1920 new OptionPaneClicker(pane, "Exit Confirmation", quitDelay, "Yes"); 1921 getStore().put(PREF_SHOWQUITCONFIRM, cbx.isSelected()); 1922 } 1923 1924 if (!getStationModelManager().checkCloseWindow()) { 1925 return false; 1926 } 1927 1928 if (!getJythonManager().saveOnExit()) { 1929 return false; 1930 } 1931 1932 store.saveIfNeeded(); 1933 store.cleanupTmpFiles(); 1934 getPluginManager().handleApplicationExit(); 1935 getJythonManager().handleApplicationExit(); 1936 1937 if (getInteractiveMode()) { 1938 exit(0); 1939 } 1940 return true; 1941 } 1942 1943 /** 1944 * Register the given {@code listener} so that changes to files matching 1945 * {@code glob} in {@code path} can be handled. 1946 * 1947 * @param path Directory to watch. 1948 * @param glob Only respond to files matching this file mask. 1949 * @param listener Listener that can handle file changes. 1950 * 1951 * @throws IOException if there was a problem registering {@code listener}. 1952 */ 1953 public void watchDirectory(final String path, 1954 final String glob, 1955 final OnFileChangeListener listener) 1956 throws IOException 1957 { 1958 watchService.register(listener, path, glob); 1959 } 1960 1961 /** 1962 * Returns McIDAS-V's {@link DirectoryWatchService}. 1963 * 1964 * @return {@code DirectoryWatchService} responsible for handling all of 1965 * McIDAS-V's directory monitoring. 1966 */ 1967 public DirectoryWatchService getWatchService() { 1968 return watchService; 1969 } 1970 1971 /** 1972 * Enable directory monitoring. 1973 */ 1974 public void startWatchService() { 1975 watchService.start(); 1976 } 1977 1978 /** 1979 * Disable directory monitoring. 1980 */ 1981 public void stopWatchService() { 1982 watchService.stop(); 1983 } 1984 1985 /** 1986 * Exposes {@link #exit(int)} to other classes. 1987 * 1988 * @param exitCode System exit code to use. 1989 * 1990 * @see #exit(int) 1991 */ 1992 public void exitMcIDASV(int exitCode) { 1993 exit(exitCode); 1994 } 1995}