001    package org.LiveGraph;
002    
003    import java.io.File;
004    import java.util.HashMap;
005    import java.util.Map;
006    
007    import javax.swing.JOptionPane;
008    
009    import org.LiveGraph.dataCache.DataCache;
010    import org.LiveGraph.dataCache.UpdateInvoker;
011    import org.LiveGraph.gui.DataFileSettingsWindow;
012    import org.LiveGraph.gui.GraphSettingsWindow;
013    import org.LiveGraph.gui.MainWindow;
014    import org.LiveGraph.gui.PlotWindow;
015    import org.LiveGraph.gui.SeriesSettingsWindow;
016    import org.LiveGraph.plot.GraphExporter;
017    import org.LiveGraph.plot.Plotter;
018    import org.LiveGraph.settings.DataFileSettings;
019    import org.LiveGraph.settings.DataSeriesSettings;
020    import org.LiveGraph.settings.ErrorWhileSettingHasChangedProcessingException;
021    import org.LiveGraph.settings.GraphSettings;
022    
023    import com.softnetConsult.utils.exceptions.ThrowableTools;
024    
025    
026    /**
027     * This is the main executable class of the LiveGraph plotter application.
028     * An instance of this class represents the application itself. The tasks of this
029     * class is to interpret the command line parameters, to set-up and to start-up
030     * the GUI and the back-end of the application, and to provide some
031     * functions which are used by different modules of the application to communicate
032     * with each other and to access global data, such as settings.
033     * 
034     * <p style="font-size:smaller;">This product includes software developed by the
035     *    <strong>LiveGraph</strong> project and its contributors.<br />
036     *    (<a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>)<br />
037     *    Copyright (c) 2007 G. Paperin.<br />
038     *    All rights reserved.
039     * </p>
040     * <p style="font-size:smaller;">File: LiveGraph.java</p>
041     * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or
042     *    without modification, are permitted provided that the following terms and conditions are met:
043     * </p>
044     * <p style="font-size:smaller;">1. Redistributions of source code must retain the above
045     *    acknowledgement of the LiveGraph project and its web-site, the above copyright notice,
046     *    this list of conditions and the following disclaimer.<br />
047     *    2. Redistributions in binary form must reproduce the above acknowledgement of the
048     *    LiveGraph project and its web-site, the above copyright notice, this list of conditions
049     *    and the following disclaimer in the documentation and/or other materials provided with
050     *    the distribution.<br />
051     *    3. All advertising materials mentioning features or use of this software or any derived
052     *    software must display the following acknowledgement:<br />
053     *    <em>This product includes software developed by the LiveGraph project and its
054     *    contributors.<br />(http://www.live-graph.org)</em><br />
055     *    4. All advertising materials distributed in form of HTML pages or any other technology
056     *    permitting active hyper-links that mention features or use of this software or any
057     *    derived software must display the acknowledgment specified in condition 3 of this
058     *    agreement, and in addition, include a visible and working hyper-link to the LiveGraph
059     *    homepage (http://www.live-graph.org).
060     * </p>
061     * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY
062     *    OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
063     *    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT SHALL
064     *    THE AUTHORS, CONTRIBUTORS OR COPYRIGHT  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
065     *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING  FROM, OUT OF OR
066     *    IN CONNECTION WITH THE SOFTWARE OR THE USE OR  OTHER DEALINGS IN THE SOFTWARE.
067     * </p> 
068     *  
069     * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>)
070     * @version {@value org.LiveGraph.LiveGraph#version}
071     */
072    public class LiveGraph {
073    
074    /**
075     * LiveGraph software version.
076     */
077    public static final String version = "1.1.1";
078    
079    // Static stuff {
080    
081    /**
082     * Singleton application instance.
083     */
084    private static LiveGraph app = null;
085    
086    /**
087     * Singleton application access method.
088     * 
089     * @return The singleton application object.
090     */
091    public static LiveGraph application() {
092            if (null == LiveGraph.app)
093                    LiveGraph.app = new LiveGraph();
094            return LiveGraph.app;
095    }
096    
097    /**
098     * Program entry point.
099     * Creates an application instance and calls the {@link #exec(String[])} method.
100     * 
101     * @param args Command line parameters.
102     */
103    public static void main(String [] args) {       
104            LiveGraph.application().exec(args);
105    }
106    
107    // } end of static stuff.
108    
109    
110    /**
111     * Application's data update invoker.
112     */
113    private UpdateInvoker updateInvoker = null;
114    
115    
116    /**
117     * Main GUI window of the application. 
118     */
119    private MainWindow mainWindow = null;
120    
121    /**
122     * GUI window for data file settings.
123     */
124    private DataFileSettingsWindow dataFileSettingsWindow = null;
125    
126    /**
127     * GUI window for graph settings.
128     */
129    private GraphSettingsWindow graphSettingsWindow = null;
130    
131    /**
132     * GUI window for data series settings.
133     */
134    private SeriesSettingsWindow seriesSettingsWindow = null;
135    
136    /**
137     * GUI window for the actual graph plot.
138     */
139    private PlotWindow plotWindow = null;
140    
141    
142    /**
143     * Holds the data file settings for the application.
144     */
145    private DataFileSettings dataFileSettings = null;
146    
147    /**
148     * Holds the graph settings for the application.
149     */
150    private GraphSettings graphSettings = null;
151    
152    /**
153     * Holds the data series settings for the application.
154     */
155    private DataSeriesSettings seriesSettings = null;
156    
157    /**
158     * Holds the graph exporter.
159     */
160    private GraphExporter graphExporter = null;
161    
162    
163    /**
164     * Main program method.
165     * It parses the command line parameters, sets up the GUI and the back-end components
166     * of the application and configures the their communication. It then loads the default
167     * settings and passes the execution control to the main Swing GUI loop. 
168     * 
169     * @param args Command line arguments.
170     */
171    public void exec(String [] args) {
172            
173            // Setup exception handling:
174            installUncaughtExceptionHandler();
175            
176            // Buffer for names of the settings files to load automatically:
177            Map<String, String> startupFiles = new HashMap<String, String>();
178            
179            // Parse the command line arguments and combine the results with default settings:
180            String argsParseErrorMsg = getInitialSettingsFiles(args, startupFiles);
181            
182            // Create settings holder objects:
183            dataFileSettings = new DataFileSettings();
184            graphSettings = new GraphSettings();
185            seriesSettings = new DataSeriesSettings();
186            
187            // Set-up communication between different settings objects:
188            graphSettings.addObserver(seriesSettings);
189            
190            // Create the data cache and the data update invoker,
191            // set-up their communication with other objects,
192            // and create the data update invocation thread:
193            DataCache dataCache = new DataCache();
194            updateInvoker = new UpdateInvoker();
195            updateInvoker.setDataCache(dataCache);
196            dataFileSettings.addObserver(updateInvoker);
197            Thread fileUpdateInvokerThread = new Thread(updateInvoker, "Update invoker thread");
198            
199            // Create the graph plotter and set-up its communication with other objects:
200            Plotter mainPlotter = new Plotter(dataCache);
201            dataCache.addObserver(mainPlotter);
202            graphSettings.addObserver(mainPlotter);
203            seriesSettings.addObserver(mainPlotter);
204            
205            // Create the graphExporter:
206            this.graphExporter = new GraphExporter(mainPlotter);
207            
208            // Create the main application window:
209            mainWindow = new MainWindow();
210            mainWindow.setVisible(true);
211            
212            // Create the data file settings window and set-up its communication with other objects:
213            dataFileSettingsWindow = new DataFileSettingsWindow();
214            setDisplayDataFileSettingsWindow(true);
215            updateInvoker.addObserver(dataFileSettingsWindow);
216            dataCache.addObserver(dataFileSettingsWindow);
217            dataFileSettings.addObserver(dataFileSettingsWindow);
218            
219            // Create the graph settings window and set-up its communication with other objects:
220            graphSettingsWindow = new GraphSettingsWindow();
221            setDisplayGraphSettingsWindow(true);
222            dataCache.addObserver(graphSettingsWindow);
223            graphSettings.addObserver(graphSettingsWindow);
224            
225            // Create the data series settings window and set-up its communication with other objects:
226            seriesSettingsWindow = new SeriesSettingsWindow();
227            setDisplaySeriesSettingsWindow(true);
228            dataCache.addObserver(seriesSettingsWindow);
229            seriesSettings.addObserver(seriesSettingsWindow);
230            graphSettings.addObserver(seriesSettingsWindow);
231            
232            // Create the graph plot window and set-up its communication with other objects:
233            plotWindow = new PlotWindow(mainPlotter);
234            setDisplayPlotWindow(true);
235            plotWindow.addSeriesHighlightListeners(seriesSettingsWindow);
236            dataCache.addObserver(plotWindow);
237            seriesSettings.addObserver(plotWindow);
238            graphSettings.addObserver(plotWindow);
239            
240            // Start the data update invocation thread:
241            fileUpdateInvokerThread.start();
242            
243            // Display any possible error messages about the command line arguments:
244            if (null != argsParseErrorMsg)
245                    logErrorLn(argsParseErrorMsg);
246            
247            // Load default or command-line specified graph settings:
248            if (startupFiles.containsKey("GraphSettings")) {
249                    String fn = startupFiles.get("GraphSettings");
250                    try {
251                            if (graphSettings.load(fn))
252                                    logSuccessLn("Initial graph settings loaded from \"" + fn + "\".");
253                            else
254                                    logErrorLn("Error while loading graph settings from \"" + fn + "\".");
255                    } catch (ErrorWhileSettingHasChangedProcessingException e) {
256                            logErrorLn("There was a problem while loading initial graph settings: \n"
257                                             + "    " + (null != e.getCause() ? e.getCause().getMessage() : e.getMessage()) + ".");                 
258                    }
259            }
260            
261            // Load default or command-line specified data series settings:
262            if (startupFiles.containsKey("DataSeriesSettings")) {
263                    String fn = startupFiles.get("DataSeriesSettings");
264                    try {
265                            if (seriesSettings.load(fn))
266                                    logSuccessLn("Initial data series settings loaded from \"" + fn + "\".");
267                            else
268                                    logErrorLn("Error while loading data series settings from \"" + fn + "\".");
269                    } catch (ErrorWhileSettingHasChangedProcessingException e) {
270                            logErrorLn("There was a problem while loading initial data series settings: \n"
271                                             + "    " + (null != e.getCause() ? e.getCause().getMessage() : e.getMessage()) + ".");
272                    }
273            }
274            
275            // Load default or command-line specified data file settings:
276            if (startupFiles.containsKey("DataFileSettings")) {
277                    String fn = startupFiles.get("DataFileSettings");
278                    try {
279                            if (dataFileSettings.load(fn))
280                                    logSuccessLn("Initial data file settings loaded from \"" + fn + "\".");
281                            else
282                                    logErrorLn("Error while loading data file settings from \"" + fn + "\".");
283                    } catch (ErrorWhileSettingHasChangedProcessingException e) {
284                            logErrorLn("There was a problem while loading initial data file settings: \n"
285                                             + "    " + (null != e.getCause() ? e.getCause().getMessage() : e.getMessage()) + ".");
286                            
287                    }
288            }
289            
290            if (!runsCorrectJavaVersion()) {
291                    JOptionPane.showMessageDialog(null, "The Java runtime environment you are using may not "
292                                                                                      + "support all program features.\n\n"
293                                                                                      + "LiveGraph is targeted for Java version 1.6 or later, "
294                                                                                      + "however, it may run on earlier Java versions with a "
295                                                                                      + "reduced feature set.\nNote that various error messages "
296                                                                                      + "may be displayed when accessing the unsupported features.\n\n"
297                                                                                      + "Your current Java version is " + getJavaSpecificationVersion(),
298                                                                              "Incompatible Java version", JOptionPane.WARNING_MESSAGE);
299            }
300            
301    } // public void exec(String [] args)
302    
303    /**
304     * Determines the current Java specification version. 
305     * @return The current Java specification version or {@code "unknown"} if it could not be obtained.
306     */
307    public String getJavaSpecificationVersion() {
308            try {
309                    return System.getProperty("java.specification.version");
310            } catch (Throwable e) {
311                    return "unknown";
312            }
313    }
314    
315    /**
316     * Determines whether the currect Java version is appropriate. This is done based on the system
317     * property {@code java.specification.version}. Java version {@code 1.6} or higher is considered ok.
318     * @return Whether the currect Java version is appropriate.
319     */
320    public boolean runsCorrectJavaVersion() {
321            
322            String specVer = getJavaSpecificationVersion(); 
323                    
324            if (specVer.equalsIgnoreCase("unknown"))
325                    return false;
326            
327            int p = specVer.indexOf(".");
328            if (0 > p)
329                    return false;
330            
331            int mainVer = Integer.parseInt(specVer.substring(0, p));
332            if (1 > mainVer)
333                    return false;
334            if (1 < mainVer)
335                    return true;
336            
337            if (specVer.length() - 1 <= p)
338                    return false;
339            
340            int subVer =  Integer.parseInt(specVer.substring(p + 1, p + 2));
341            if (6 > subVer)
342                    return false;
343            
344            return true;
345    }
346    
347    /**
348     * Parses the command line arguments for file names for initial settings and combines the results
349     * with default settings file names.
350     * 
351     * @param args Command line arguments.
352     * @param startupFiles A table to hold the results.
353     * @return {@code null} if no error occured or the error message if there was an error (e.g. incorrect
354     * command line arguments).
355     */
356    private String getInitialSettingsFiles(String [] args, Map<String, String> startupFiles) {
357    
358            String errMsg = getInitialSettingsFromCommandLine(args, startupFiles);
359            setDefaultInitialSettingsFiles(startupFiles);
360            return errMsg;
361    } // getInitialSettingsFiles
362    
363    /**
364     * Parses the command line arguments for file names for initial settings.
365     * 
366     * @param args Command line arguments.
367     * @param startupFiles A table to hold the results.
368     * @return {@code null} if no error occured or the error message if there was an error (e.g. incorrect
369     * command line arguments).
370     */
371    private String getInitialSettingsFromCommandLine(String [] args, Map<String, String> startupFiles) {
372            
373            if (0 != args.length && 2 != args.length && 4 != args.length && 6 != args.length) {
374                    return "Command line parameters are invalid and were ignored. \n"
375                             + "    Legal command line arguments are as follows: \n"
376                             + "    > java edu.monash.LiveGraph.LiveGraph [-dfs \"{data file settings file}\"] \n"
377                             + "                                          [-gs \"{graph settings file}\"] \n"
378                             + "                                          [-dss \"{data series settings file}\"] \n"
379                             + "    This means the program expects either 0, 2, 4 or 6 command line arguments. \n"
380                             + "    However, " + args.length + " argument" + (1==args.length?" was":"s were") + " passed.";
381            }
382            
383            String errMsg = "";
384            String s = null;
385            
386            if (2 <= args.length) {              
387                    s = tryParseCommandLineArgument(args[0], args[1], startupFiles);
388                    if (null != s && 0 < errMsg.length())
389                            s += "\n";
390                    if (null != s)
391                            errMsg += s;
392            }
393            
394            if (4 <= args.length) {
395                    s = tryParseCommandLineArgument(args[2], args[3], startupFiles);
396                    if (null != s && 0 < errMsg.length())
397                            s += "\n";
398                    if (null != s)
399                            errMsg += s;
400            }
401            
402            if (6 <= args.length) {
403                    s = tryParseCommandLineArgument(args[4], args[5], startupFiles);
404                    if (null != s && 0 < errMsg.length())
405                            s += "\n";
406                    if (null != s)
407                            errMsg += s;
408            }
409            
410            if (0 == errMsg.length())
411                    return null;
412            
413            return errMsg;
414    } // getInitialSettingsFromCommandLine
415    
416    /**
417     * Processes two consecutive command line parameters.
418     * 
419     * @param flag Flag specifier command line parameter.
420     * @param filename Filename specifier command line parameter.
421     * @param startupFiles A table to hold the results.
422     * @return {@code null} if no error occured or the error message if there was an error (e.g. incorrect
423     * command line arguments).
424     */
425    private String tryParseCommandLineArgument(String flag, String filename, Map<String, String> startupFiles) {
426            
427            if (flag.equalsIgnoreCase("-dfs")) {
428                    File f = new File(filename);
429                    if (!f.exists())
430                            return "Could not load the data file settings from the file specified on the command line "
431                                 + "because the file does not exist. \n"
432                                 + "    (" + filename + ")";
433                    startupFiles.put("DataFileSettings", f.getAbsolutePath());
434                    return null;
435            }
436            
437            if (flag.equalsIgnoreCase("-gs")) {
438                    File f = new File(filename);
439                    if (!f.exists())
440                            return "Could not load the graph settings from the file specified on the command line "
441                                 + "because the file does not exist. \n"
442                                 + "    (" + filename + ")";
443                    startupFiles.put("GraphSettings", f.getAbsolutePath());
444                    return null;
445            }
446            
447            if (flag.equalsIgnoreCase("-dss")) {
448                    File f = new File(filename);
449                    if (!f.exists())
450                            return "Could not load the graph settings from the file specified on the command line "
451                                 + "because the file does not exist. \n"
452                                 + "    (" + filename + ")";
453                    startupFiles.put("DataSeriesSettings", f.getAbsolutePath());
454                    return null;
455            }
456            
457            return "Invalid command line flag \"" + flag + "\" will be ignored. \n"
458                     + "    The subsequent command line argument \"" + filename + "\" will also be ignored.";
459    } // tryParseCommandLineArgument
460    
461    
462    /**
463     * If some settings were not specified to be loaded from a file via the command line,
464     * this method is used to specify default files for loading the settings, provided the
465     * defualt files exist.
466     * 
467     * @param startupFiles A table to hold the results.
468     */
469    private void setDefaultInitialSettingsFiles(Map<String, String> startupFiles) {
470            
471            if (! startupFiles.containsKey("DataFileSettings")) {
472                    File f = new File("session" + DataFileSettings.preferredFileExtension); 
473                    if (f.exists())
474                            startupFiles.put("DataFileSettings", f.getAbsolutePath());
475            }
476            
477            if (! startupFiles.containsKey("GraphSettings")) {
478                    File f = new File("session" + GraphSettings.preferredFileExtension); 
479                    if (f.exists())
480                            startupFiles.put("GraphSettings", f.getAbsolutePath());
481            }
482            
483            if (! startupFiles.containsKey("DataSeriesSettings")) {
484                    File f = new File("session" + DataSeriesSettings.preferredFileExtension); 
485                    if (f.exists())
486                            startupFiles.put("DataSeriesSettings", f.getAbsolutePath());
487            }
488    } // setDefaultInitialSettingsFiles
489    
490    /**
491     * Prints an info message to the main window message area.
492     * 
493     * @param o The message.
494     */
495    public void logInfoLn(Object o) {
496            mainWindow.logInfoLn(o.toString());
497    }
498    
499    /**
500     * Prints an error message to the main window message area.
501     * 
502     * @param o The message.
503     */
504    public void logErrorLn(Object o) {
505            mainWindow.logErrorLn(o.toString());
506    }
507    
508    /**
509     * Prints an success message to the main window message area.
510     * 
511     * @param o The message.
512     */
513    public void logSuccessLn(Object o) {
514            mainWindow.logSuccessLn(o.toString());
515    }
516    
517    /**
518     * Displays or hides the data file settings window.
519     * 
520     * @param state Whether to display ({@code true}) or to hide ({@code false}).
521     */
522    public void setDisplayDataFileSettingsWindow(boolean state) {   
523            dataFileSettingsWindow.setVisible(state);
524            mainWindow.fileSettingsDisplayStateChanged(state);
525    }
526    
527    /**
528     * Displays or hides the graph settings window.
529     * 
530     * @param state Whether to display ({@code true}) or to hide ({@code false}).
531     */
532    public void setDisplayGraphSettingsWindow(boolean state) {      
533            graphSettingsWindow.setVisible(state);
534            mainWindow.graphSettingsDisplayStateChanged(state);
535    }
536    
537    /**
538     * Displays or hides the data series settings window.
539     * 
540     * @param state Whether to display ({@code true}) or to hide ({@code false}).
541     */
542    public void setDisplaySeriesSettingsWindow(boolean state) {     
543            seriesSettingsWindow.setVisible(state);
544            mainWindow.seriesSettingsDisplayStateChanged(state);
545    }
546    
547    /**
548     * Displays or hides plot window.
549     * 
550     * @param state Whether to display ({@code true}) or to hide ({@code false}).
551     */
552    public void setDisplayPlotWindow(boolean state) {       
553            plotWindow.setVisible(state);
554            mainWindow.plotDisplayStateChanged(state);
555    }
556    
557    /**
558     * This method is called by the main window when it is closed. This method
559     * initiates the disposing of all windows and the data update invocation
560     * thread in order to correctly close the application and save all settings
561     * to default files. 
562     */
563    public void disposeGUIAndExit() {
564            mainWindow.dispose();
565            dataFileSettingsWindow.dispose();
566            graphSettingsWindow.dispose();
567            seriesSettingsWindow.dispose();
568            plotWindow.dispose();
569            graphExporter.disposeInternalGUI();
570            updateInvoker.setMustQuit(true);
571            
572            dataFileSettings.save("session" + DataFileSettings.preferredFileExtension);
573            graphSettings.save("session" + GraphSettings.preferredFileExtension);
574            seriesSettings.save("session" + DataSeriesSettings.preferredFileExtension);
575    }
576    
577    /**
578     * Gets the application's global data file settings.
579     * 
580     * @return Global data file settings.
581     */
582    public DataFileSettings getDataFileSettings() {
583            return dataFileSettings;
584    }
585    
586    /**
587     * Gets the application's global graph settings.
588     * 
589     * @return Global graph settings.
590     */
591    public GraphSettings getGraphSettings() {
592            return graphSettings;
593    }
594    
595    
596    /**
597     * Gets the application's global data series settings.
598     * 
599     * @return Global data series settings.
600     */
601    public DataSeriesSettings getDataSeriesSettings() {
602            return seriesSettings;
603    }
604    
605    public GraphExporter getGraphExporter() {
606            return graphExporter;
607    }
608    
609    /**
610     * Causes the next data update.
611     */
612    public void initiateDataUpdate() {
613            updateInvoker.update();
614    }
615    
616    private void installUncaughtExceptionHandler() {
617            
618            try {
619                    UncaughtExceptionHandler handler = new UncaughtExceptionHandler();
620                    Thread.setDefaultUncaughtExceptionHandler(handler);
621                    
622            } catch (SecurityException e) {
623                    e.printStackTrace();
624            }
625    }
626    
627    private class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
628            public void uncaughtException(Thread t, Throwable e) {
629                    try {
630                            String err = ThrowableTools.stackTraceToString(e);
631                            if (null == mainWindow) {
632                                    String h = "Error in thread \"" + t.getName() + "\"";
633                                    JOptionPane.showMessageDialog(null, err, h, JOptionPane.ERROR_MESSAGE);
634                            } else {
635                                    logErrorLn(err);
636                            }
637                    } finally {
638                            e.printStackTrace();
639                    }
640                    //throw new Error(e);
641        }
642    } // private class UncaughtExceptionHandler
643    
644    } // public class LiveGraph