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