Java tutorial
/* This file is part of Timelord. Copyright 2005-2009 Jordan Reed Timelord is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Timelord is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Timelord. If not, see <http://www.gnu.org/licenses/>. */ package net.chaosserver.timelord.swingui; import java.awt.Component; import java.awt.Dimension; import java.awt.FileDialog; import java.awt.Image; import java.awt.Point; import java.awt.Toolkit; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.net.URL; import java.util.Date; import java.util.Properties; import java.util.ResourceBundle; import java.util.prefs.Preferences; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.UIManager; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import net.chaosserver.timelord.data.TimelordData; import net.chaosserver.timelord.data.TimelordDataException; import net.chaosserver.timelord.data.TimelordDataReaderWriter; import net.chaosserver.timelord.data.TimelordDayView; import net.chaosserver.timelord.data.XmlDataReaderWriter; import net.chaosserver.timelord.data.engine.AutoSaveThread; import net.chaosserver.timelord.swingui.data.TimelordDataReaderWriterUI; import net.chaosserver.timelord.swingui.engine.BringToFrontThread; import net.chaosserver.timelord.util.DateUtil; import net.chaosserver.timelord.util.OsUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * The main class to run the Timelord swing application. * * @author Jordan Reed */ public class Timelord { /** Logger. */ private static Log log = LogFactory.getLog(Timelord.class); /** Resource Bundle. */ protected ResourceBundle resourceBundle = ResourceBundle.getBundle("TimelordResources"); /** Resource Root. */ private static final String RROOT = Timelord.class.getName(); /** Preference key for the annoyance mode. */ protected static final String ANNOYANCE_MODE = "ANNOYANCE_MODE"; /** Preferences value for Jordan annoyance mode. */ public static final String ANNOYANCE_JORDAN = "ANNOYANCE_JORDAN"; /** Preferences value for Doug annoyance mode. */ public static final String ANNOYANCE_DOUG = "ANNOYANCE_DOUG"; /** Preferences value for no annoyance mode. */ public static final String ANNOYANCE_NONE = "ANNOYANCE_NONE"; /** Constant for preference of the X location of the frame when saved. */ private static final String FRAME_X_LOCATION = "FRAME_X_LOCATION"; /** Constant for preference of the Y location of the frame when saved. */ private static final String FRAME_Y_LOCATION = "FRAME_Y_LOCATION"; /** Constant for preference of the width of the frame when saved. */ private static final String FRAME_WIDTH = "FRAME_WIDTH"; /** Constant for preference of the height of the frame when saved. */ private static final String FRAME_HEIGHT = "FRAME_HEIGHT"; /** The default frame height. */ private static final int DEFAULT_FRAME_HEIGHT = 480; /** The default frame width. */ private static final int DEFAULT_FRAME_WIDTH = 640; /** Constant for preference for default time increment. */ public static final String TIME_INCREMENT = "TIME_INCREMENT"; /** Holds the application frame. */ protected JFrame applicationFrame; /** Holds the timelord data object. */ protected TimelordData timelordData; /** Holds the main tabbed pane visual component. */ protected TimelordTabbedPane timelordTabbedPane; /** The engine that brings the main window to the front. */ protected BringToFrontThread bringToFrontThread; /** The engine that saves the data once every few minutes. */ protected AutoSaveThread autoSaveThread; /** The application icon. */ protected Icon applicationIcon; /** Holds the application properties packaged in the file. */ protected Properties appProperties = null; /** * Constructs the new timelord object. */ public Timelord() { // Set the UI to the system look and feel so the application // appears more native. try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { if (log.isWarnEnabled()) { log.warn("Failed to set look and feel", e); } } } /** * Sets the timelord data object this is a visual display for. * * @param timelordData the data object. */ public void setTimelordData(TimelordData timelordData) { this.timelordData = timelordData; } /** * Getter for the timelord data object this is displaying. * * @return the timelord data object */ public TimelordData getTimelordData() { return this.timelordData; } /** * Gets the annoyance mode from the perferences or returns the default. * * @return the annoyance mode */ public String getAnnoyanceMode() { Preferences preferences = Preferences.userNodeForPackage(this.getClass()); return preferences.get(ANNOYANCE_MODE, ANNOYANCE_JORDAN); } /** * Sets the annoyance mode. This should be one of the constants else things * will behave erradically. * * @param annoyanceMode the annoyance mode */ public void setAnnoyanceMode(String annoyanceMode) { Preferences preferences = Preferences.userNodeForPackage(this.getClass()); preferences.put(ANNOYANCE_MODE, annoyanceMode); } /** * Persists out the timelord file and allows the user to choose where * the file should go. * * @param rwClassName the name of the RW class * (e.g. "net.chaosserver.timelord.data.ExcelDataReaderWriter") * @param userSelect allows the user to select where the file should * be persisted. */ public void writeTimeTrackData(String rwClassName, boolean userSelect) { try { Class<?> rwClass = Class.forName(rwClassName); TimelordDataReaderWriter timelordDataRW = (TimelordDataReaderWriter) rwClass.newInstance(); int result = JFileChooser.APPROVE_OPTION; File outputFile = timelordDataRW.getDefaultOutputFile(); if (timelordDataRW instanceof TimelordDataReaderWriterUI) { TimelordDataReaderWriterUI timelordDataReaderWriterUI = (TimelordDataReaderWriterUI) timelordDataRW; timelordDataReaderWriterUI.setParentFrame(applicationFrame); JDialog configDialog = timelordDataReaderWriterUI.getConfigDialog(); configDialog.pack(); configDialog.setLocationRelativeTo(applicationFrame); configDialog.setVisible(true); } if (userSelect) { if (OsUtil.isMac()) { FileDialog fileDialog = new FileDialog(applicationFrame, "Select File", FileDialog.SAVE); fileDialog.setDirectory(outputFile.getParent()); fileDialog.setFile(outputFile.getName()); fileDialog.setVisible(true); if (fileDialog.getFile() != null) { outputFile = new File(fileDialog.getDirectory(), fileDialog.getFile()); } } else { JFileChooser fileChooser = new JFileChooser(outputFile.getParentFile()); fileChooser.setSelectedFile(outputFile); fileChooser.setFileFilter(timelordDataRW.getFileFilter()); result = fileChooser.showSaveDialog(applicationFrame); if (result == JFileChooser.APPROVE_OPTION) { outputFile = fileChooser.getSelectedFile(); } } } if (result == JFileChooser.APPROVE_OPTION) { timelordDataRW.writeTimelordData(getTimelordData(), outputFile); } } catch (Exception e) { JOptionPane.showMessageDialog(applicationFrame, "Error writing to file.\n" + "Do you have the output file open?", "Save Error", JOptionPane.ERROR_MESSAGE, applicationIcon); if (log.isErrorEnabled()) { log.error("Error persisting file", e); } } } /** * Saves the current location of the applicatioFrame. */ protected void saveFrameLocation() { if (applicationFrame != null) { Point frameLocation = applicationFrame.getLocation(); Preferences preferences = Preferences.userNodeForPackage(this.getClass()); preferences.putDouble(FRAME_X_LOCATION, frameLocation.getX()); preferences.putDouble(FRAME_Y_LOCATION, frameLocation.getY()); } } /** * Gets back the point location where the frame was last saved. * * @return location where the frame was last saved */ protected Point loadLastFrameLocation() { Preferences preferences = Preferences.userNodeForPackage(this.getClass()); Point windowLocation = new Point(); windowLocation.setLocation(preferences.getDouble(FRAME_X_LOCATION, 0), preferences.getDouble(FRAME_Y_LOCATION, 0)); return windowLocation; } /** * Returns the dimension of the last saved framesize. * * @return last saved frame size in preferences */ protected Dimension loadLastFrameSize() { Preferences preferences = Preferences.userNodeForPackage(this.getClass()); Dimension windowSize = new Dimension(); windowSize.setSize(preferences.getDouble(FRAME_WIDTH, DEFAULT_FRAME_WIDTH), preferences.getDouble(FRAME_HEIGHT, DEFAULT_FRAME_HEIGHT)); return windowSize; } /** * Saves the size of the current frame into preferences. */ protected void saveFrameSize() { if (applicationFrame != null) { Dimension windowSize = applicationFrame.getSize(); Preferences preferences = Preferences.userNodeForPackage(this.getClass()); preferences.putDouble(FRAME_WIDTH, windowSize.getWidth()); preferences.putDouble(FRAME_HEIGHT, windowSize.getHeight()); } } /** * Gets the common task panel (the one for the current date) from the * container. * * @return the common task panel */ protected CommonTaskPanel getCommonTaskPanel() { return timelordTabbedPane.getCommonTaskPanel(); } /** * Resets the common task panel (the one for the current date). */ protected void buildCommonTaskPanel() { timelordTabbedPane.buildCommonTaskPanel(); } /** * Returns if the system is already running based on the creation of a lock * file that is deleted when the JVM exits and creates a new instance of the * lockfile for this JVM. If the lockfile is detected it presents a dialog * giving an option for the end user to overwrite the lock file. * * @return indicates another instance of timelord is already running */ public boolean isAlreadyRunning() { boolean alreadyRunning = false; File homeDirectory = new File(System.getProperty("user.home")); File lockFile = new File(homeDirectory, "Timelord.lockfile"); if (lockFile.exists()) { String startAnyway = "Start Anyway"; String dontStart = "Cancel Start"; Object[] options = { dontStart, startAnyway }; int result = JOptionPane.showOptionDialog(null, "A lockfile for an instance of timelord " + "has been found. If you are already " + "running timelord, click \"" + dontStart + "\" and use the running program.", "Timelord Already Running", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, dontStart); if (result == 0) { alreadyRunning = true; } } if (!alreadyRunning) { try { lockFile.createNewFile(); lockFile.deleteOnExit(); } catch (IOException e) { throw new RuntimeException(e); } } return alreadyRunning; } /** * Check if there is a newer version of the timelord application * available and if so, prompt the user to download the new version. * * @return if the user wants to download a new version */ public boolean isUpgradeRequested() { boolean result = false; Properties appProperties = getAppProperties(); if (appProperties != null) { String pomUrlString = appProperties.getProperty("pomurl"); String appVersion = appProperties.getProperty("implementation.version"); if (pomUrlString != null) { InputStream pomStream = null; try { URL pomUrl = new URL(pomUrlString); pomStream = pomUrl.openStream(); if (log.isDebugEnabled()) { log.debug("Opened URL [" + pomUrl + "] with result [" + (pomStream != null ? pomStream.available() : "null") + "]"); } DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.parse(pomStream); if (log.isTraceEnabled()) { log.trace("Loaded document: " + doc.getDocumentElement()); } Element pomversionElement = doc.getElementById("pomversion"); String pomVersion = ""; if (pomversionElement != null) { pomVersion = pomversionElement.getNodeValue(); } if (appVersion == null) { appVersion = ""; } if (log.isDebugEnabled()) { log.debug("Testinv version of app [" + appVersion + "] against version of pom [" + pomVersion + "]"); } if (!pomVersion.equals(appVersion)) { } } catch (ParserConfigurationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SAXException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { if (pomStream != null) { try { pomStream.close(); } catch (IOException e) { if (log.isInfoEnabled()) { log.info("failed to close pomstream", e); } } } } } else { if (log.isWarnEnabled()) { log.warn("Got back blank pomurl, so not checking " + "for upgrade."); } } } else { if (log.isWarnEnabled()) { log.warn("Got back blank properties, so not checking " + "for upgrade."); } } return result; } /** * Starts up the timelord application and displays the frame. */ public void start() { applicationFrame = new JFrame("Timelord"); // Get the pretty application icon URL iconUrl = this.getClass().getResource("/net/chaosserver/timelord/TimelordIcon.gif"); if (log.isTraceEnabled()) { log.trace("iconUrl is [" + iconUrl + "]"); } if (iconUrl != null) { Toolkit toolkit = Toolkit.getDefaultToolkit(); Image applicationImage = toolkit.getImage(iconUrl); applicationIcon = new ImageIcon(applicationImage); applicationFrame.setIconImage(applicationImage); } else { if (log.isWarnEnabled()) { log.warn("Cound not find icon url"); } } if (!isAlreadyRunning() && !isUpgradeRequested()) { TimelordMenu menu = new TimelordMenu(this); applicationFrame.setJMenuBar(menu); applicationFrame.addWindowListener(new WindowCloser()); if (OsUtil.isMac()) { try { Class<?> macSwingerClass = Class .forName("net.chaosserver.timelord." + "swingui.macos.MacSwinger"); Constructor<?> macSwingerConstructor = macSwingerClass .getConstructor(new Class[] { Timelord.class }); macSwingerConstructor.newInstance(new Object[] { this }); } catch (Exception e) { // Shouldn't happen, but not a big deal if (log.isWarnEnabled()) { log.warn("Failed to create the MacSwinger", e); } } } TimelordDataReaderWriter timelordDataRW = new XmlDataReaderWriter(); try { TimelordData inputTimelordData = timelordDataRW.readTimelordData(); inputTimelordData.cleanse(); inputTimelordData.resetTaskListeners(); setTimelordData(inputTimelordData); menu.setTimelordData(getTimelordData()); applicationFrame.setSize(loadLastFrameSize()); timelordTabbedPane = new TimelordTabbedPane(getTimelordData()); applicationFrame.getContentPane().add(timelordTabbedPane); applicationFrame.setLocation(loadLastFrameLocation()); applicationFrame.setVisible(true); // If there is no data for Today, let the user set the // start time. TimelordDayView timelordDayView = new TimelordDayView(inputTimelordData, DateUtil.trunc(new Date())); if (timelordDayView.getTotalTimeToday(true) == 0) { menu.timelord.changeStartTime(true); } bringToFrontThread = new BringToFrontThread(applicationFrame, this); bringToFrontThread.start(); autoSaveThread = new AutoSaveThread(getTimelordData()); autoSaveThread.start(); } catch (TimelordDataException e) { String shutdown = "Shutdown"; Object[] options = { shutdown }; JOptionPane.showOptionDialog(null, "There was an unrecoverable error trying to load " + "the file.\nTimelord was probably shutdown " + "in the middle of the last write.\nThere " + "should be a lot of backup files inside the " + "defaut location.\nCopy one of the backups " + "over the most recent, restart, and keep your " + "fingers crossed.\n" + e, "Timelord Data File Corrupted", JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, null, options, shutdown); stop(); } } else { stop(); } } /** * The stop method cleans up the application and exits. */ public void stop() { if (bringToFrontThread != null) { bringToFrontThread.setStop(true); bringToFrontThread.interrupt(); } if (autoSaveThread != null) { autoSaveThread.setStop(true); autoSaveThread.interrupt(); if (log.isTraceEnabled()) { log.trace("Waiting for the AutoSaveThread to terminate."); } try { autoSaveThread.join(); if (log.isTraceEnabled()) { log.trace("AutoSaveThread has terminated."); } } catch (InterruptedException e) { if (log.isWarnEnabled()) { log.warn("AutoSaveThread was interrupted.", e); } } } saveFrameLocation(); saveFrameSize(); System.exit(0); } /** * Sets the todayTab as the proper tab to show to the user. */ public void showTodayTab() { // Since the Today Tab is always tab zero, set this to tab zero. timelordTabbedPane.setSelectedIndex(0); // TODO: If the today tab is not today, notify the user. } /** * Switches to the next tab */ public void showNextTab() { int nextSelectedTab = (timelordTabbedPane.getSelectedIndex() + 1) % timelordTabbedPane.getTabCount(); if (nextSelectedTab < 0) { nextSelectedTab = 0; } timelordTabbedPane.setSelectedIndex(nextSelectedTab); } /** * Switches to the previous tab */ public void showPreviousTab() { int nextSelectedTab = (timelordTabbedPane.getSelectedIndex() - 1) % timelordTabbedPane.getTabCount(); timelordTabbedPane.setSelectedIndex(nextSelectedTab); } /** * Sets the keyboard focus on the find filter of the current tab. */ public void showFindTask() { Component selectedComponent = timelordTabbedPane.getSelectedComponent(); if (selectedComponent instanceof CommonTaskPanel) { ((CommonTaskPanel) selectedComponent).showFindTask(); } else if (selectedComponent instanceof PreviousDayPanel) { CommonTaskPanel commonTaskPanel = ((PreviousDayPanel) selectedComponent).getCommonTaskPanel(); commonTaskPanel.showFindTask(); } } /** * Shows the about dialog that tells about the application. */ public void showAboutDialog() { Package packageInfo = Package.getPackage("net.chaosserver.timelord.swingui"); if (log.isTraceEnabled()) { if (packageInfo != null) { StringBuffer sb = new StringBuffer(); sb.append(packageInfo.getClass().getName()); sb.append(" [name="); sb.append(packageInfo.getName()); sb.append(", specificationTitle="); sb.append(packageInfo.getSpecificationTitle()); sb.append(", specificationVersion="); sb.append(packageInfo.getSpecificationVersion()); sb.append(", specificationVendor="); sb.append(packageInfo.getSpecificationVendor()); sb.append(", implementationTitle="); sb.append(packageInfo.getImplementationTitle()); sb.append(", implementationVersion="); sb.append(packageInfo.getImplementationVersion()); sb.append(", implementationVendor="); sb.append(packageInfo.getImplementationVendor()); sb.append("]"); log.trace(sb.toString()); } } StringBuffer sb = new StringBuffer(); sb.append("Timelord"); if ((packageInfo != null) && (packageInfo.getImplementationVersion() != null)) { sb.append(" ["); sb.append(packageInfo.getImplementationVersion()); sb.append("]"); } else { Properties appProperties = getAppProperties(); if (appProperties != null) { sb.append(" "); sb.append(appProperties.getProperty("implementation.version", "[Unknown Version]")); } else { sb.append(" [Unknown Version]"); } } sb.append("\n"); sb.append(resourceBundle.getString(RROOT + ".about")); JOptionPane.showMessageDialog(applicationFrame, sb.toString(), "About Timelord", JOptionPane.INFORMATION_MESSAGE, applicationIcon); } /** * Gets the instance of the application properties, loading them * from file if required. * * @return the app properties, or null if they could not be loaded. */ protected synchronized Properties getAppProperties() { if (appProperties == null) { InputStream appPropertiesStream = this.getClass() .getResourceAsStream("/net/chaosserver/timelord/Timelord.properties"); if (appPropertiesStream != null) { appProperties = new Properties(); try { appProperties.load(appPropertiesStream); } catch (IOException e) { if (log.isWarnEnabled()) { log.warn("Failed to load resource for app" + "properties [" + "/net/chaosserver/timelord/Timelord.properties" + "]", e); } } } else { if (log.isWarnEnabled()) { log.warn("Failed to load resource for app properties [" + "/net/chaosserver/timelord/Timelord.properties" + "]"); } } } return appProperties; } /** * Present a dialog to allow the user to change the start time for the * day. * * @param useCurrentTime has the dialog default be the current time */ public void changeStartTime(boolean useCurrentTime) { StartTimeDialog startTimeDialog = new StartTimeDialog(applicationFrame, useCurrentTime, getTimelordData()); startTimeDialog.setVisible(true); startTimeDialog.dispose(); } /** * Presents a dialog to allow the changing of the annoy time * for the various annoy modes. */ public void changeAnnoyTime() { AnnoyTimeDialog annoyTimeDialog = new AnnoyTimeDialog(applicationFrame); annoyTimeDialog.setVisible(true); annoyTimeDialog.dispose(); } /** * The main method creates a new instance and starts it. * * @param args command line args */ public static void main(String[] args) { Timelord timelord = new Timelord(); timelord.start(); } /** * Basic adapter that listens for window close events and stops the * application. */ protected class WindowCloser extends WindowAdapter { /** * Stops the application on window closing. * * @param evt the window event */ public void windowClosing(WindowEvent evt) { stop(); } } }