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