001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 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 https://www.gnu.org/licenses/. 027 */ 028 029package edu.wisc.ssec.mcidasv; 030 031import java.awt.Component; 032import java.awt.Dimension; 033import java.awt.Font; 034import java.awt.GraphicsEnvironment; 035import java.rmi.RemoteException; 036 037import java.util.ArrayList; 038import java.util.Collections; 039import java.util.List; 040 041import javax.swing.JComponent; 042import javax.swing.JLabel; 043import javax.swing.JOptionPane; 044import javax.swing.JPanel; 045import javax.swing.JScrollPane; 046import javax.swing.JTextArea; 047 048import edu.wisc.ssec.mcidasv.startupmanager.options.FileOption; 049import visad.VisADException; 050 051import ucar.unidata.idv.IdvConstants; 052 053import ucar.unidata.idv.ArgsManager; 054import ucar.unidata.idv.IntegratedDataViewer; 055import ucar.unidata.util.GuiUtils; 056import ucar.unidata.util.IOUtil; 057import ucar.unidata.util.Msg; 058import ucar.unidata.util.LogUtil; 059import ucar.unidata.util.PatternFileFilter; 060import ucar.unidata.util.StringUtil; 061 062import org.python.core.Py; 063import org.python.core.PyString; 064import org.slf4j.Logger; 065import org.slf4j.LoggerFactory; 066 067import edu.wisc.ssec.mcidasv.startupmanager.StartupManager; 068 069/** 070 * McIDAS-V needs to handle a few command line flags/options that the IDV does 071 * not. Only the ability to force the Aqua look and feel currently exists. 072 * 073 * @author McIDAS-V Developers 074 */ 075public class ArgumentManager extends ArgsManager { 076 077 private static final Logger helpLogger = 078 LoggerFactory.getLogger("mcvstdout"); 079 080 /** 081 * McIDAS-V flag that signifies everything that follows is a Jython 082 * argument. 083 */ 084 public static final String ARG_JYTHONARGS = "-scriptargs"; 085 086 /** Flag used to set the path to mcidasv.log. */ 087 public static final String ARG_LOGPATH = "-logpath"; 088 089 /** Flag that allows users to automatically run an action after startup. */ 090 public static final String ARG_DOACTION = "-doaction"; 091 092 /** Usage message. */ 093 public static final String USAGE_MESSAGE = 094 "Usage: runMcV [OPTIONS] <bundle/script files, e.g., .mcv, .mcvz, .py>"; 095 096 /** 097 * {@literal "__name__"} to use when no Jython/Python script has been 098 * provided at startup. 099 */ 100 public static final String NO_PYTHON_MODULE = "<none>"; 101 102 /** Jython arguments, if any. */ 103 private List<PyString> jythonArguments; 104 105 /** 106 * Jython script to execute, or {@literal "<none>"} if one was not given. 107 */ 108 private String jythonScript; 109 110 /** 111 * Holds the ID of an action to automatically run after starting McV. 112 * Value may be null. 113 */ 114 private String startupAction; 115 116 /** 117 * Given by the "-user" argument. Alternative user path for bundles, 118 * resources, etc. 119 */ 120 String defaultUserDirectory = 121 StartupManager.getInstance().getPlatform().getUserDirectory(); 122 123 /** 124 * Just bubblin' on up the inheritance hierarchy. 125 * 126 * @param idv The IDV instance. 127 * @param args The command line arguments that were given. 128 */ 129 public ArgumentManager(IntegratedDataViewer idv, String[] args) { 130 super(idv, args); 131 jythonArguments = new ArrayList<>(args.length); 132 jythonScript = NO_PYTHON_MODULE; 133 } 134 135 private static List<PyString> extractJythonArgs(int index, String... args) { 136 List<PyString> jythonArgs = new ArrayList<>(args.length); 137 for (int i = index; i < args.length; i++) { 138 jythonArgs.add(Py.newString(args[i])); 139 } 140 return jythonArgs; 141 } 142 143 /** 144 * Currently we're only handling the {@code -forceaqua} flag so we can 145 * mitigate some overlay issues we've been seeing on OS X Leopard. 146 * 147 * @param arg The current argument we're examining. 148 * @param args The actual array of arguments. 149 * @param idx The index of {@code arg} within {@code args}. 150 * 151 * @return The idx of the last value in the args array we look at. i.e., 152 * if the flag arg does not require any further values in the args array 153 * then don't increment idx. If arg requires one more value then 154 * increment idx by one. etc. 155 * 156 * @throws Exception Throw bad things off to something that can handle 'em! 157 */ 158 protected int parseArg(String arg, String[] args, int idx) 159 throws Exception { 160 161 if ("-forceaqua".equals(arg)) { 162 // unfortunately we can't simply set the look and feel here. If I 163 // were to do so, the loadLookAndFeel in the IdvUIManager would 164 // eventually get loaded and then set the look and feel to whatever 165 // the preferences dictate. 166 // instead I use the boolean toggle to signal to McV's 167 // UIManager.loadLookAndFeel that it should simply ignore the user's 168 // preference is and load the Aqua L&F from there. 169 McIDASV.useAquaLookAndFeel = true; 170 } else if (ARG_HELP.equals(arg)) { 171 String msg = USAGE_MESSAGE + "\n" + getUsageMessage(); 172 if (McIDASV.isWindows() && !GraphicsEnvironment.isHeadless()) { 173 userMessage(msg, false); 174 } else { 175 helpLogger.info(System.getProperty("line.separator") + msg); 176 } 177 ((McIDASV)getIdv()).exit(1); 178 } else if (checkArg(arg, "-script", args, idx, 1) || checkArg(arg, "-pyfile", args, idx, 1)) { 179 String scriptArg = args[idx++]; 180 jythonScript = scriptArg; 181 scriptingFiles.add(scriptArg); 182 if (!getIslInteractive()) { 183 setIsOffScreen(true); 184 } 185 } else if ("-welcomewindow".equals(arg)) { 186 // do nothing 187 188 } else if (checkArg(arg, "-autoquit", args, idx, 1)) { 189 // do nothing besides skip the next parameter 190 // (which should be the autoquit delay) 191 idx++; 192 } 193 else if ("-console".equals(arg)) { 194 System.err.println("*** WARNING: console flag is likely to go away soon!"); 195 } else if (ARG_JYTHONARGS.equals(arg)) { 196 if (scriptingFiles.isEmpty()) { 197 System.err.println("*** WARNING: Jython script arguments will be ignored unless you provide a Jython script to execute!"); 198 } else { 199 jythonArguments.addAll(extractJythonArgs(idx, args)); 200 201 // jump to end of args to halt further idv processing. 202 return args.length; 203 } 204 } else if (checkArg(arg, ARG_LOGPATH, args, idx, 1)) { 205 String argValue = args[idx++]; 206 persistentCommandLineArgs.add(ARG_LOGPATH); 207 persistentCommandLineArgs.add(argValue); 208 } else if (checkArg(arg, ARG_BUNDLE, args, idx, 1)) { 209 String argValue = args[idx++]; 210 String[] results = FileOption.parseFormat(argValue); 211 if (FileOption.booleanFromFormat(results[0])) { 212 argXidvFiles.add(results[1]); 213 } 214 } else if (checkArg(arg, ARG_DOACTION, args, idx, 1)) { 215 startupAction = args[idx++]; 216 } else { 217 if (ARG_ISLINTERACTIVE.equals(arg) || ARG_B64ISL.equals(arg) || ARG_ISLFILE.equals(arg) || isIslFile(arg)) { 218 System.err.println("*** WARNING: ISL is being deprecated!"); 219 } else if (arg.startsWith("-D")) { 220 List<String> l = StringUtil.split(arg.substring(2), "="); 221 if (l.size() == 2) { 222 System.setProperty(l.get(0), l.get(1)); 223 } 224 } 225 return super.parseArg(arg, args, idx); 226 } 227 return idx; 228 } 229 230 /** 231 * Runs the action ID stored in {@link #startupAction}. 232 * 233 * Calling this method will result in the contents of {@code startupAction} 234 * being deleted. 235 */ 236 public void runStartupAction() { 237 if ((startupAction != null) && !startupAction.isEmpty()) { 238 getIdv().handleAction("action:"+startupAction); 239 startupAction = null; 240 } 241 } 242 243 /** 244 * Get the {@link JComponent} that displays the given message. 245 * 246 * @param msg Message to display. 247 * @param breakLines Whether or not {@literal "long"} lines should be broken up. 248 * 249 * @return {@code JComponent} that displays {@code msg}. 250 */ 251 private static JComponent getMessageComponent(String msg, boolean breakLines) { 252 if (msg.startsWith("<html>")) { 253 Component[] comps = GuiUtils.getHtmlComponent(msg, null, 500, 400); 254 return (JScrollPane)comps[1]; 255 } 256 257 int msgLength = msg.length(); 258 if (msgLength < 50) { 259 return new JLabel(msg); 260 } 261 262 StringBuilder sb = new StringBuilder(msgLength * 2); 263 if (breakLines) { 264 for (String line : StringUtil.split(msg, "\n")) { 265 line = StringUtil.breakText(line, "\n", 50); 266 sb.append(line).append('\n'); 267 } 268 } else { 269 sb.append(msg).append('\n'); 270 } 271 272 JTextArea textArea = new JTextArea(sb.toString()); 273 textArea.setFont(textArea.getFont().deriveFont(Font.BOLD)); 274 textArea.setBackground(new JPanel().getBackground()); 275 textArea.setEditable(false); 276 JScrollPane textSp = GuiUtils.makeScrollPane(textArea, 400, 200); 277 textSp.setPreferredSize(new Dimension(400, 200)); 278 return textSp; 279 } 280 281 /** 282 * Show a dialog containing a message. 283 * 284 * @param msg Message to display. 285 * @param breakLines If {@code true}, long lines are split. 286 */ 287 public static void userMessage(String msg, boolean breakLines) { 288 msg = Msg.msg(msg); 289 if (LogUtil.showGui()) { 290 LogUtil.consoleMessage(msg); 291 JComponent msgComponent = getMessageComponent(msg, breakLines); 292 GuiUtils.addModalDialogComponent(msgComponent); 293 JOptionPane.showMessageDialog(LogUtil.getCurrentWindow(), msgComponent); 294 GuiUtils.removeModalDialogComponent(msgComponent); 295 } else { 296 System.err.println(msg); 297 } 298 } 299 300 /** 301 * Show a dialog containing an error message. 302 * 303 * @param msg Error message to display. 304 * @param breakLines If {@code true}, long lines are split. 305 */ 306 public static void userErrorMessage(String msg, boolean breakLines) { 307 msg = Msg.msg(msg); 308 if (LogUtil.showGui()) { 309 LogUtil.consoleMessage(msg); 310 JComponent msgComponent = getMessageComponent(msg, breakLines); 311 GuiUtils.addModalDialogComponent(msgComponent); 312 JOptionPane.showMessageDialog(LogUtil.getCurrentWindow(), 313 msgComponent, "Error", JOptionPane.ERROR_MESSAGE); 314 GuiUtils.removeModalDialogComponent(msgComponent); 315 } else { 316 System.err.println(msg); 317 } 318 } 319 320 /** 321 * Print out the command line usage message and exit 322 * 323 * @param err The usage message 324 */ 325 @Override public void usage(String err) { 326 List<String> chunks = StringUtil.split(err, ":"); 327 if (chunks.size() == 2) { 328 err = chunks.get(0) + ": " + chunks.get(1) + '\n'; 329 } 330 String msg = USAGE_MESSAGE; 331 msg = msg + '\n' + getUsageMessage(); 332 userErrorMessage(err + '\n' + msg, false); 333 ((McIDASV)getIdv()).exit(1); 334 } 335 336 /** 337 * Format a line in the {@literal "usage message"} output. The chief 338 * difference between this method and 339 * {@link ArgsManager#msg(String, String)} is that this method prefixes 340 * each line with four {@literal "space"} characters, rather than a single 341 * {@literal "tab"} character. 342 * 343 * @param arg Commandline argument. 344 * @param desc Description of the argument. 345 * 346 * @return Formatted line (suitable for {@link #getUsageMessage()}. 347 */ 348 @Override protected String msg(String arg, String desc) { 349 return " " + arg + ' ' + desc + '\n'; 350 } 351 352 /** 353 * Append some McIDAS-V specific command line options to the default IDV 354 * usage message. 355 * 356 * @return Usage message. 357 */ 358 protected String getUsageMessage() { 359 return msg(ARG_HELP, "(this message)") 360 + msg("-forceaqua", "Forces the Aqua look and feel on OS X") 361 + msg(ARG_PROPERTIES, "<property file>") 362 + msg("-Dpropertyname=value", "(Define the property value)") 363 + msg(ARG_INSTALLPLUGIN, "<plugin jar file or url to install>") 364 + msg(ARG_PLUGIN, "<plugin jar file, directory, url for this run>") 365 + msg(ARG_NOPLUGINS, "Don't load plugins") 366// + msg(ARG_CLEARDEFAULT, "(Clear the default bundle)") 367// + msg(ARG_NODEFAULT, "(Don't read in the default bundle file)") 368// + msg(ARG_DEFAULT, "<.mcv/.mcvz file>") 369 + msg(ARG_BUNDLE, "<bundle file or url>") 370 + msg(ARG_B64BUNDLE, "<base 64 encoded inline bundle>") 371 + msg(ARG_SETFILES, "<datasource pattern> <semi-colon delimited list of files> (Use the list of files for the bundled datasource)") 372 + msg(ARG_ONEINSTANCEPORT, "<port number> (Check if another version of McIDAS-V is running. If so pass command line arguments to it and shutdown)") 373 + msg(ARG_NOONEINSTANCE, "(Don't do the one instance port)") 374// + msg(ARG_NOPREF, "(Don't read in the user preferences)") 375 + msg(ARG_USERPATH, "<user directory to use>") 376 + msg("-tempuserpath", "(Starts McIDAS-V with a randomly generated temporary userpath)") 377 + msg(ARG_LOGPATH, "<path to log file>") 378 + msg(ARG_SITEPATH, "<url path to find site resources>") 379 + msg(ARG_NOGUI, "(Don't show the main window gui)") 380 + msg(ARG_DATA, "<data source> (Load the data source)") 381// + msg(ARG_DISPLAY, "<parameter> <display>") 382// + msg("<scriptfile.isl>", "(Run the IDV script in batch mode)") 383 + msg("-script", "<jython script file to evaluate>") 384 + msg("-pyfile", "<jython script file to evaluate>") 385 + msg(ARG_JYTHONARGS, "All arguments after this flag will be considered Jython arguments.") 386// + msg(ARG_B64ISL, "<base64 encoded inline isl> This will run the isl in interactive mode") 387// + msg(ARG_ISLINTERACTIVE, "run any isl files in interactive mode") 388 + msg(ARG_IMAGE, "<image file name> (create a jpeg image and then exit)") 389// + msg(ARG_MOVIE, "<movie file name> (create a quicktime movie and then exit)") 390// + msg(ARG_IMAGESERVER, "<port number or .properties file> (run McIDAS-V in image generation server mode. Support http requests on the given port)") 391 + msg(ARG_CATALOG, "<url to a chooser catalog>") 392// + msg(ARG_CONNECT, "<collaboration hostname to connect to>") 393// + msg(ARG_SERVER, "(Should McIDAS-V run in collaboration server mode)") 394// + msg(ARG_PORT, "<Port number collaboration server should listen on>") 395 + msg(ARG_CHOOSER, "(show the data chooser on start up) ") 396 + msg(ARG_PRINTJNLP, "(Print out any embedded bundles from jnlp files)") 397 + msg(ARG_CURRENTTIME, "<dttm> (Override current time for background processing)") 398// + msg(ARG_CURRENTTIME, "<dttm> (Override current time for ISL processing)") 399 + msg(ARG_LISTRESOURCES, "<list out the resource types") 400 + msg(ARG_DEBUG, "(Turn on debug print)") 401 + msg(ARG_MSG_DEBUG, "(Turn on language pack debug)") 402 + msg(ARG_MSG_RECORD, "<Language pack file to write missing entries to>") 403 + msg(ARG_TRACE, "(Print out trace messages)") 404 + msg(ARG_NOERRORSINGUI, "(Don't show errors in gui)") 405 + msg(ARG_TRACEONLY, "<trace pattern> (Print out trace messages that match the pattern)") 406 + msg(ARG_DOACTION, "<action id> (Run given action automatically after startup)"); 407// + msg("-console", "[ fix for getting the console functionality in install4j launcher ]"); 408 } 409 410 /** 411 * Determine whether or not the user has provided any arguments for a 412 * Jython script. 413 * 414 * @return {@code true} if the user has provided Jython arguments, 415 * {@code false} otherwise. 416 */ 417 public boolean hasJythonArguments() { 418 return !jythonArguments.isEmpty(); 419 } 420 421 /** 422 * Returns Jython arguments. <b>Note:</b> this does not include the Jython 423 * script that will be executed. 424 * 425 * @return Either a {@link List} of {@link String Strings} containing the 426 * arguments or an empty {@code List} if there were no arguments given. 427 */ 428 public List<PyString> getJythonArguments() { 429 return jythonArguments; 430 } 431 432 /** 433 * Returns the name of the Jython script the user has provided. 434 * 435 * @return Either the path to a Jython file or {@literal "<none>"} if the 436 * user did not provide a script. 437 */ 438 public String getJythonScript() { 439 return jythonScript; 440 } 441 442 /** 443 * Gets called by the IDV to process the set of initial files, e.g., 444 * default bundles, command line bundles, jnlp files, etc. 445 * 446 * <p>Overridden by McIDAS-V to remove bundle file paths that are zero 447 * characters long. This was happening because {@code runMcV.bat} was 448 * always passing {@literal '-bundle ""'} on the command line (for Windows). 449 * 450 * @throws VisADException When something untoward happens 451 * @throws RemoteException When something untoward happens 452 */ 453 @Override protected void processInitialBundles() 454 throws VisADException, RemoteException 455 { 456 for (int i = 0; i < argXidvFiles.size(); i++) { 457 String path = (String)argXidvFiles.get(i); 458 if (path.isEmpty()) { 459 argXidvFiles.remove(i); 460 } 461 } 462 super.processInitialBundles(); 463 } 464 465 /** 466 * @see ArgsManager#getBundleFileFilters() 467 */ 468 @Override public List<PatternFileFilter> getBundleFileFilters() { 469 List<PatternFileFilter> filters = new ArrayList<>(10); 470 Collections.addAll(filters, getXidvFileFilter(), getZidvFileFilter()); 471 return filters; 472 } 473 474 /** 475 * Returns a list of {@link PatternFileFilter}s that can be used to determine 476 * if a file is a bundle. 477 * 478 * <p>If {@code fromOpen} is {@code true}, the 479 * returned list will contain {@code PatternFileFilter}s for bundles as 480 * well as ISL files. If {@code false}, the returned list will only 481 * contain filters for XML and zipped bundles. 482 * 483 * @param fromOpen Whether or not this has been called from an 484 * {@literal "open file"} dialog. 485 * 486 * @return Filters for bundles. 487 */ 488 public List<PatternFileFilter> getBundleFilters(final boolean fromOpen) { 489 List<PatternFileFilter> filters; 490 if (fromOpen) { 491 filters = new ArrayList<>(10); 492 Collections.addAll(filters, getXidvZidvFileFilter(), FILTER_ISL, super.getXidvZidvFileFilter()); 493 } else { 494 filters = new ArrayList<>(getBundleFileFilters()); 495 } 496 return filters; 497 } 498 499 /** 500 * @see ArgsManager#getXidvFileFilter() 501 */ 502 @Override public PatternFileFilter getXidvFileFilter() { 503 return Constants.FILTER_MCV; 504 } 505 506 /** 507 * @see ArgsManager#getZidvFileFilter() 508 */ 509 @Override public PatternFileFilter getZidvFileFilter() { 510 return Constants.FILTER_MCVZ; 511 } 512 513 /** 514 * @see ArgsManager#getXidvZidvFileFilter() 515 */ 516 @Override public PatternFileFilter getXidvZidvFileFilter() { 517 return Constants.FILTER_MCVMCVZ; 518 } 519 520 /* 521 * There's some internal IDV file opening code that relies on this method. 522 * We've gotta override if we want to use .zidv bundles. 523 */ 524 @Override public boolean isZidvFile(final String name) { 525 return isZippedBundle(name); 526 } 527 528 /* same story as isZidvFile! */ 529 @Override public boolean isXidvFile(final String name) { 530 return isXmlBundle(name); 531 } 532 533 /** 534 * Tests to see if {@code name} has a known XML bundle extension. 535 * 536 * @param name Name of the bundle. 537 * 538 * @return Whether or not {@code name} has an XML bundle suffix. 539 */ 540 public static boolean isXmlBundle(final String name) { 541 return IOUtil.hasSuffix(name, Constants.FILTER_MCV.getPreferredSuffix()) 542 || IOUtil.hasSuffix(name, IdvConstants.FILTER_XIDV.getPreferredSuffix()); 543 } 544 545 /** 546 * Tests to see if {@code name} has a known zipped bundle extension. 547 * 548 * @param name Name of the bundle. 549 * 550 * @return Whether or not {@code name} has zipped bundle suffix. 551 */ 552 public static boolean isZippedBundle(final String name) { 553 return IOUtil.hasSuffix(name, Constants.FILTER_MCVZ.getPreferredSuffix()) 554 || IOUtil.hasSuffix(name, IdvConstants.FILTER_ZIDV.getPreferredSuffix()); 555 } 556 557 /** 558 * Tests {@code name} to see if it has a known bundle extension. 559 * 560 * @param name Name of the bundle. 561 * 562 * @return Whether or not {@code name} has a bundle suffix. 563 */ 564 public static boolean isBundle(final String name) { 565 return isXmlBundle(name) || isZippedBundle(name); 566 } 567 568 /** 569 * Clears out the automatic display creation arguments by setting {@link #initParams} and {@link #initDisplays} to 570 * {@link Collections#emptyList()}. 571 */ 572 protected void clearAutomaticDisplayArgs() { 573 initParams = Collections.emptyList(); 574 initDisplays = Collections.emptyList(); 575 } 576}