001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
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}