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}