org.wremja.launcher.Launcher.java Source code

Java tutorial

Introduction

Here is the source code for org.wremja.launcher.Launcher.java

Source

/* 
 * $Id$
 *
 * This file is part of Wremja.
 * 
 * Copyright (C) 2009 Christoph Kutzinski.
 * 
 * Wremja 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, version 2.1 of the License.
 * 
 * Wremja 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
 * Wremja. If not, see <http://www.gnu.org/licenses/>.
 */
package org.wremja.launcher;

import java.awt.Color;
import java.awt.Desktop;
import java.awt.Frame;
import java.awt.SystemTray;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.net.NetworkInterface;
import java.net.URL;
import java.net.URLConnection;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.LogManager;

import javax.swing.JEditorPane;
import javax.swing.JOptionPane;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.event.HyperlinkEvent.EventType;

import org.jdesktop.swingx.plaf.LookAndFeelAddons;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.xmlpull.mxp1.MXParser;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import com.jgoodies.looks.plastic.Plastic3DLookAndFeel;
import com.jgoodies.looks.plastic.PlasticLookAndFeel;
import com.kemai.util.CollectionUtils;
import com.kemai.util.TextResourceBundle;
import com.kemai.wremja.gui.GuiConstants;
import com.kemai.wremja.gui.MainFrame;
import com.kemai.wremja.gui.model.PresentationModel;
import com.kemai.wremja.gui.model.ProjectActivityStateException;
import com.kemai.wremja.gui.model.io.DataBackup;
import com.kemai.wremja.gui.model.io.SaveDaemon;
import com.kemai.wremja.gui.settings.ApplicationSettings;
import com.kemai.wremja.gui.settings.IUserSettings;
import com.kemai.wremja.gui.settings.UserSettings;
import com.kemai.wremja.logging.BetterFormatter;
import com.kemai.wremja.logging.Logger;
import com.kemai.wremja.model.ActivityRepository;
import com.kemai.wremja.model.io.ProTrackReader;

import de.kutzi.javautils.misc.MavenVersion;

/**
 * Controls the lifecycle of the application.
 * 
 * @author remast
 * @author kutzi
 * @author $Author$ (last change)
 */
public final class Launcher {

    /** The bundle for internationalized texts. */
    private static final TextResourceBundle textBundle = TextResourceBundle.getBundle(Launcher.class);

    /** The logger. */
    private static volatile Logger LOG;

    //------------------------------------------------
    // Command line options
    //------------------------------------------------

    /** Property for command line option minimized (-m). */
    private Boolean minimized;

    private boolean debug = false;

    private boolean updateCheck;

    private MainFrame mainFrame;

    private static boolean firstStart;

    /** The daemon to periodically save to disk. */
    private static SaveDaemon saveDaemon;

    /** The interval in ms in which the data is saved (at least) to the disk. */
    private static final long SAVE_TIMER_INTERVAL = TimeUnit.MINUTES.toMillis(2);

    //------------------------------------------------
    // Application resources
    //------------------------------------------------

    /** The lock file to avoid multiple instances of the application. */
    private static File lockFile;
    private static FileLock lock;

    /** Changing the last-modified date on a locked file doesn't work (at least on Windows).
     * Therefore we need an additional file to lockFile.
     */
    private static File lastTouchFile;

    /** The absolute path name of the log file. */
    private static String logFileName;

    /** Set to true if the previous shutdown was likely a 'clean' one. I.e. the lock file was deleted. */
    private static boolean cleanShutdown = false;

    /** last modified time of the previous run if it was a 'dirty' shutdown. Only set if cleanShutdown == false */
    private static long lastModified;

    private static RandomAccessFile lockRafFile;

    /** Hide constructor. */
    private Launcher() {
    }

    /**
     * Main method that starts the application.
     * @param arguments the command line arguments
     */
    public static void main(final String[] arguments) {
        try {
            final Launcher mainInstance = new Launcher();
            mainInstance.parseCommandLineArguments(arguments);

            mainInstance.initDir();

            mainInstance.initLogger();

            initUncaughtExceptionHandler();

            initLookAndFeel();

            initLockFile();

            @SuppressWarnings("deprecation")
            IUserSettings settings = UserSettings.instance();

            TimeZone tz = TimeZone.getTimeZone(settings.getTimeZone());
            LOG.info("Using timezone: " + tz);
            TimeZone.setDefault(tz);
            DateTimeZone.setDefault(DateTimeZone.forTimeZone(tz));

            final PresentationModel model = initModel(settings);

            mainInstance.initMainFrame(model, settings);

            initTimer(model);

            initShutdownHook(model);

            mainInstance.checkForUpdates();
        } catch (Throwable t) {
            LOG.error(t, t);
            JOptionPane.showMessageDialog(null, textBundle.textFor("Launcher.FatalError.Message", logFileName), //$NON-NLS-1$
                    textBundle.textFor("Launcher.FatalError.Title"), //$NON-NLS-1$
                    JOptionPane.ERROR_MESSAGE);
            System.exit(-1);
        }
    }

    private void initDir() {
        File wremjaDataDir = ApplicationSettings.instance().getApplicationDataDirectory();
        if (!wremjaDataDir.exists()) {
            firstStart = true;
            if (!wremjaDataDir.mkdir()) {
                throw new IllegalStateException("Couldn't create data directory " + wremjaDataDir);
            }
        }
    }

    /**
     * Parses the parameters from the given command line arguments.
     * @param arguments the command line arguments to parse
     */
    private void parseCommandLineArguments(final String[] arguments) {
        if (arguments == null || arguments.length == 0) {
            return;
        }

        for (String argument : arguments) {
            if (argument.startsWith("--minimized")) {
                this.minimized = Boolean.TRUE;
            } else if (argument.startsWith("--debug")) {
                this.debug = true;
            } else if (argument.startsWith("--updateCheck")) {
                this.updateCheck = true;
            }
        }

    }

    /**
     * Initializes the main frame.
     * @param model the model to be displayed
     * @param mainInstance the main instance
     * @return the initialized main frame
     */
    private MainFrame initMainFrame(PresentationModel model, IUserSettings settings) throws Exception {
        LOG.debug("Initializing main frame ...");
        this.mainFrame = new MainFrame(model, settings);

        boolean minimized = false;
        if (this.minimized != null) {
            // command line overrides preferences
            minimized = this.minimized.booleanValue();
        } else {
            if (settings.isRememberWindowSizeLocation()) {
                minimized = settings.isWindowMinimized();
            }
        }

        if (!cleanShutdown && model.isActive()) {
            mainFrame.handleUnfinishedActivityOnStartup(lastModified);
        }

        if (settings.isUseTrayIcon() && SystemTray.isSupported()) {
            mainFrame.setVisible(true);
            mainFrame.setExtendedState(minimized ? Frame.ICONIFIED : Frame.NORMAL);
        } else {
            // tray icon disabled or not supported
            // remembering minimized state doesn't make much sense in that scenario
            // ergo ignore it
            mainFrame.setVisible(true);
        }

        return mainFrame;
    }

    /**
     * Initializes the lock file.
     */
    private static void initLockFile() {
        LOG.debug("Initializing lock file ...");
        if (!tryLock()) {
            JOptionPane.showMessageDialog(null, textBundle.textFor("Launcher.ErrorAlreadyRunning.Message"), //$NON-NLS-1$
                    textBundle.textFor("Launcher.ErrorAlreadyRunning.Title"), //$NON-NLS-1$
                    JOptionPane.ERROR_MESSAGE);
            LOG.info(textBundle.textFor("Launcher.ErrorAlreadyRunning.Message")); //$NON-NLS-1$
            System.exit(0);
        }
    }

    /**
     * Initializes the shutdown hook process.
     * @param model
     */
    private static void initShutdownHook(final PresentationModel model) {
        LOG.debug("Initializing shutdown hook ...");

        // Note: that logging in the shutdown hook doesn't work, as
        // probably the LogManager is already destroyed
        Runtime.getRuntime().addShutdownHook(new Thread("Wremja shutdown ...") {

            @Override
            public void run() {
                // TODO: decouple this from the PresentationModel
                // (always causes problems, because some GUI components which
                // should be notified are already gone!)

                // prevent dirty saves on shutdown
                saveDaemon.requestStop();

                // stop current activity (if necessary)
                if (model.isActive() && model.isStopActivityOnShutdown()) {
                    // remove all observers to prevent side effect, because of
                    // already dead observers
                    model.deleteObservers();
                    try {
                        model.stop();
                    } catch (ProjectActivityStateException e) {
                        // ignore
                    }
                }

                // save model
                try {
                    model.save();
                } catch (Throwable t) {
                    System.err.println(t);
                } finally {
                    // Release file lock
                    releaseLock();
                }
            }
        });
    }

    /**
     * Initializes the model from the stored data file or creates a new one.
     * @param settings 
     * @return the model
     */
    private static PresentationModel initModel(IUserSettings settings) {
        LOG.debug("Initializing model...");

        final PresentationModel model = new PresentationModel(settings, lastTouchFile);
        if (firstStart) {
            //look if there is a Baralga file
            File baralgaDir = new File(System.getProperty("user.home"), ".ProTrack");
            File baralgaData = new File(baralgaDir, "ProTrack.ptd");
            if (baralgaData.isFile()) {
                int result = JOptionPane.showConfirmDialog(null,
                        textBundle.textFor("Launcher.DataLoading.FromBaralga.Text", baralgaData.getAbsolutePath()), //$NON-NLS-1$
                        textBundle.textFor("Launcher.DataLoading.FromBaralga.Title"), //$NON-NLS-1$
                        JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);

                if (result == JOptionPane.YES_OPTION) {
                    try {
                        ActivityRepository data = readData(baralgaData);
                        LOG.info("Importing data from Baralga file " + baralgaData + ".\n"
                                + data.getProjects().size() + " projects and " + data.getActivities().size()
                                + " activities.");

                        // Reading data file was successful.
                        model.setData(data);
                    } catch (IOException e) {
                        LOG.error("Error reading Baralga file", e);
                        JOptionPane.showMessageDialog(null,
                                textBundle.textFor("Launcher.DataLoading.ErrorTextGeneric"), //$NON-NLS-1$
                                textBundle.textFor("Launcher.DataLoading.ErrorTitle"), JOptionPane.ERROR_MESSAGE);
                    }
                }
            }
        }

        final String dataFileLocation = settings.getDataFileLocation();
        final File file = new File(dataFileLocation);

        try {
            if (file.exists()) {
                final ActivityRepository data = readData(file);
                LOG.info("Loaded data from file " + file + ".\n" + data.getProjects().size() + " projects and "
                        + data.getActivities().size() + " activities.");

                // Reading data file was successful.
                model.setData(data);
            } else {
                // look if there are backup files
                final List<File> backupFiles = DataBackup.getBackupFiles(file);

                if (CollectionUtils.isNotEmpty(backupFiles)) {
                    loadFromBackup(model, backupFiles);
                }
            }
        } catch (IOException dataFileIOException) {
            LOG.error("Data file corrupt, trying to read from backup", dataFileIOException);
            // Make a backup copy of the corrupt file
            DataBackup.saveCorruptDataFile(file);

            // Reading data file was not successful so we try the backup files. 
            final List<File> backupFiles = DataBackup.getBackupFiles(file);

            if (CollectionUtils.isNotEmpty(backupFiles)) {
                loadFromBackup(model, backupFiles);
            } else {
                // Data corrupt and no backup file found
                JOptionPane.showMessageDialog(null, textBundle.textFor("Launcher.DataLoading.ErrorTextNoBackup"), //$NON-NLS-1$
                        textBundle.textFor("Launcher.DataLoading.ErrorTitle"), JOptionPane.ERROR_MESSAGE);
            }
        }
        return model;
    }

    private static void loadFromBackup(final PresentationModel model, final List<File> backupFiles) {
        for (File backupFile : backupFiles) {
            try {
                final ActivityRepository data = readData(backupFile);
                model.setData(data);
                LOG.info("Loaded data from backup file " + backupFile + ".\n" + data.getProjects().size()
                        + " projects and " + data.getActivities().size() + " activities.");

                final DateTime backupDate = DataBackup.getDateOfBackup(backupFile);
                String backupDateString = backupFile.getName();
                if (backupDate != null) {
                    backupDateString = DateTimeFormat.mediumDateTime().print(backupDate);
                }

                JOptionPane.showMessageDialog(null,
                        textBundle.textFor("Launcher.DataLoading.ErrorText", backupDateString), //$NON-NLS-1$
                        textBundle.textFor("Launcher.DataLoading.ErrorTitle"), JOptionPane.WARNING_MESSAGE);

                break;
            } catch (IOException backupFileIOException) {
                LOG.error(backupFileIOException, backupFileIOException);
            }
        }
    }

    /**
     * Read ActivityRepository data from the given file.
     * @param file the file to be read
     * @return the data read from file or null if the file is null or doesn't exist
     * @throws IOException on error reading file
     */
    private static ActivityRepository readData(final File file) throws IOException {
        // Check for saved data
        if (file != null && file.exists()) {
            ProTrackReader reader;
            try {
                reader = new ProTrackReader();
                return reader.read(file);
            } catch (Exception e) {
                throw new IOException(e);
            }
        }
        return null;
    }

    /**
     * Initialize the logger of the application.
     * @throws IOException 
     */
    private void initLogger() throws IOException {
        String logDir = ApplicationSettings.instance().getApplicationDataDirectory().getAbsolutePath()
                + File.separator + "log";
        File logDirF = new File(logDir);
        if (!logDirF.isDirectory()) {
            if (!logDirF.mkdirs()) {
                throw new IOException("Couldn't create log directory: " + logDir);
            }
        }

        logFileName = logDir + File.separator + "wremja.log";

        LogManager.getLogManager().reset();

        FileHandler fileHandler = new FileHandler(logFileName + ".%g", 50000, 3, true);
        fileHandler.setFormatter(new BetterFormatter());
        fileHandler.setLevel(Level.INFO);

        ConsoleHandler consoleHandler = new ConsoleHandler() {
            {
                setOutputStream(System.out);
            }
        };
        consoleHandler.setLevel(Level.WARNING);
        consoleHandler.setFormatter(new BetterFormatter());

        java.util.logging.Logger rootLogger = java.util.logging.Logger.getLogger("");
        rootLogger.addHandler(fileHandler);
        rootLogger.addHandler(consoleHandler);
        rootLogger.setLevel(Level.INFO);

        java.util.logging.Logger appLogger = java.util.logging.Logger.getLogger("com.kemai");
        appLogger.setLevel(Level.INFO);

        if (this.debug) {
            consoleHandler.setLevel(Level.FINEST);
            appLogger.setLevel(Level.FINEST);
        }

        LOG = Logger.getLogger(Launcher.class);
    }

    private static void initUncaughtExceptionHandler() {
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {

            private final Logger log = Logger.getLogger("UncaughtExceptionHandler");

            @Override
            public void uncaughtException(Thread t, Throwable e) {
                this.log.error("Caught exception from Thread " + t.getName(), e);
            }
        });
    }

    /**
     * Initialize the look & feel of the application.
     */
    private static void initLookAndFeel() {
        LOG.debug("Initializing look and feel ...");

        // enable antialiasing
        System.setProperty("swing.aatext", "true");

        // use Xrender pipeline on Linux, if available
        System.setProperty("sun.java2d.xrender", "true");

        try {
            // a) Try windows
            UIManager.setLookAndFeel(new Plastic3DLookAndFeel());
            Plastic3DLookAndFeel.setTabStyle(PlasticLookAndFeel.TAB_STYLE_METAL_VALUE);
            Plastic3DLookAndFeel.setHighContrastFocusColorsEnabled(true);
        } catch (UnsupportedLookAndFeelException e) {
            // b) Try system look & feel
            try {
                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            } catch (Exception ex) {
                LOG.error(ex, ex);
            }
        }
        String s = LookAndFeelAddons.getBestMatchAddonClassName();
        try {
            LookAndFeelAddons.setAddon(s);
        } catch (Exception e) {
            LOG.warn(e, e);
        }
    }

    /**
     * Initialize the timer to automatically save the model.
     * @see #SAVE_TIMER_INTERVAL
     * @param model the model to be saved
     */
    private static void initTimer(final PresentationModel model) {
        LOG.debug("Initializing save timer ...");
        saveDaemon = new SaveDaemon(model, SAVE_TIMER_INTERVAL, TimeUnit.MILLISECONDS);
        Thread t = new Thread(saveDaemon, "Wremja save daemon");
        t.setDaemon(true);
        t.start();
    }

    /**
     * Tries to create and lock a lock file at <code>${user.home}/.ProTrack/lock</code>.
     * @return <code>true</code> if the lock could be acquired. <code>false</code> if
     *   the lock is held by another program
     * @throws RuntimeException if an I/O error occurred
     */
    @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RV_RETURN_VALUE_IGNORED_BAD_PRACTICE", justification = "Its irrelevant if lock file already existed or not.")
    private static boolean tryLock() {
        try {
            checkOrCreateDataDir();
            lockFile = new File(UserSettings.getLockFileLocation());
            lastTouchFile = new File(lockFile.getParentFile(), "lastTouch");
            if (!lockFile.exists()) {
                lockFile.createNewFile();
                cleanShutdown = true;
            } else {
                cleanShutdown = false;
                lastModified = lastTouchFile.lastModified();
            }

            if (!lastTouchFile.exists()) {
                lastTouchFile.createNewFile();
            }

            lockRafFile = new RandomAccessFile(lockFile, "rw");
            final FileChannel channel = lockRafFile.getChannel(); //$NON-NLS-1$
            lock = channel.tryLock();

            return lock != null;
        } catch (Exception e) {
            final String error = textBundle.textFor("Launcher.LockFileError.Message"); //$NON-NLS-1$
            LOG.error(error, e);
            throw new RuntimeException(error);
        }
    }

    /**
     * Checks whether the data directory exists and creates it if necessary.
     */
    private static void checkOrCreateDataDir() {
        final File wremjaDir = ApplicationSettings.instance().getApplicationDataDirectory();
        if (!wremjaDir.exists()) {
            final boolean wremjaDirCreated = wremjaDir.mkdir();
            if (!wremjaDirCreated) {
                throw new RuntimeException("Could not create directory at " + wremjaDir.getAbsolutePath() + ".");
            }
        }
    }

    /**
     * Releases the lock file created with {@link #createLock()}.
     */
    private static void releaseLock() {
        if (lock == null) {
            return;
        }

        try {
            lock.release();
        } catch (IOException e) {
            LOG.error(e, e);
        } finally {
            try {
                lock.channel().close();
            } catch (Exception e) {
                // ignore
            }
            try {
                lockRafFile.close();
            } catch (Exception e) {
                // ignore
            }
        }

        final boolean deleteSuccessfull = lockFile.delete();
        if (!deleteSuccessfull) {
            LOG.warn("Could not delete lock file at " + lockFile.getAbsolutePath() + ". Please delete manually.");
        }
    }

    private static final URL UPDATE_URL;
    private static final String DOWNLOAD_URL = "http://kenai.com/projects/wremja/downloads";
    static {
        URL url;
        try {
            //url = new URL("http://oss.sonatype.org/content/repositories/kenai-snapshots/com/kenai/wremja/wremja-installer/maven-metadata.xml");
            url = new URL(
                    "http://oss.sonatype.org/content/repositories/kenai-releases/com/kenai/wremja/wremja-installer/maven-metadata.xml");
        } catch (MalformedURLException e) {
            LOG.error("", e);
            url = null;
        }
        UPDATE_URL = url;
    }

    private void checkForUpdates() {

        if (!this.updateCheck) {
            return;
        }

        SwingWorker<String, Object> checker = new SwingWorker<String, Object>() {

            private String newerVersion;

            @Override
            public String doInBackground() {
                try {
                    TimeUnit.SECONDS.sleep(3);
                    boolean hasInternet = false;
                    Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
                    while (interfaces.hasMoreElements()) {
                        NetworkInterface interf = interfaces.nextElement();
                        if (interf.isUp()) {
                            if (!interf.isLoopback()) {
                                hasInternet = true;
                                break;
                            }
                        }
                    }

                    if (!hasInternet) {
                        //System.out.println("No Internet connection found");
                        return null;
                    } else {
                        //System.out.println("Internet connection found!");
                        MavenVersion currentVersion = MavenVersion.fromString(GuiConstants.WREMJA_VERSION);

                        URLConnection conn = UPDATE_URL.openConnection();

                        XmlPullParser parser = new MXParser();
                        parser.setInput(conn.getInputStream(), null);

                        while (true) {
                            int eventType = parser.next();

                            if (eventType == XmlPullParser.END_DOCUMENT) {
                                break;
                            }

                            if (eventType == XmlPullParser.START_TAG) {
                                if ("versioning".equals(parser.getName())) {
                                    return parseVersioningElement(parser, currentVersion);
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    LOG.warn("", e);
                }
                return null;
            }

            private String parseVersioningElement(XmlPullParser parser, MavenVersion currentVersion)
                    throws XmlPullParserException, IOException {
                while (true) {
                    int eventType = parser.next();

                    // seems like <release> is not necessarily what we want:
                    // http://www.sonatype.com/people/2009/12/maven-dependency-resolution-a-repository-perspective/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+sonatype+%28Sonatype+Blogs%29&utm_content=Google+Reader

                    //               if(eventType == XmlPullParser.START_TAG
                    //                     && "release".equals(parser.getName())) {
                    //                  String versionStr = parser.nextText();
                    //                     if (versionStr != null) {
                    //                        MavenVersion version = MavenVersion.fromString(versionStr.trim());
                    //                        if (version.isGreaterThan(currentVersion)) {
                    //                           this.newerVersion = versionStr.trim();
                    //                           return versionStr;
                    //                        }
                    //                     }
                    //               }

                    if (eventType == XmlPullParser.START_TAG && "versions".equals(parser.getName())) {
                        this.newerVersion = getNewerVersion(parser, currentVersion);
                        return newerVersion;
                    }

                    if (eventType == XmlPullParser.END_TAG && "versioning".equals(parser.getName())) {
                        return null;
                    }
                }
            }

            private String getNewerVersion(XmlPullParser parser, MavenVersion currentVersion)
                    throws XmlPullParserException, IOException {
                while (true) {
                    int eventType = parser.next();

                    if (eventType == XmlPullParser.END_TAG) {
                        if ("versions".equals(parser.getName())) {
                            return null;
                        }
                    }

                    if (eventType == XmlPullParser.START_TAG) {
                        if ("version".equals(parser.getName())) {
                            String versionStr = parser.nextText();
                            if (versionStr != null) {
                                MavenVersion version = MavenVersion.fromString(versionStr.trim());
                                if (version.isGreaterThan(currentVersion)) {
                                    return versionStr;
                                }
                            }
                        }
                    }
                }
            }

            @Override
            protected void done() {
                if (newerVersion != null) {
                    JEditorPane msg = new JEditorPane();

                    Color bg = (Color) UIManager.get("OptionPane.background", Locale.getDefault());
                    msg.setBackground(bg);
                    msg.setEditable(false);
                    msg.setContentType("text/html");
                    msg.setText(
                            "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">"
                                    + "<html>" + "<head>" + "<style>\n"
                                    + "    a {text-decoration: underline; color: blue}\n" + "</style>\n"
                                    + "</head><body>A newer version (" + newerVersion
                                    + ") of Wremja is available.<br>Please download it from" + " <a href=\""
                                    + DOWNLOAD_URL + "\">here</a>.</body></html>!");
                    msg.addHyperlinkListener(new HyperlinkListener() {
                        @Override
                        public void hyperlinkUpdate(HyperlinkEvent e) {
                            if (e.getEventType().equals(EventType.ACTIVATED)) {
                                try {
                                    Desktop.getDesktop().browse(e.getURL().toURI());
                                } catch (Exception e1) {
                                    LOG.error("", e1);
                                }
                            }
                        }
                    });
                    JOptionPane.showMessageDialog(mainFrame, msg);
                }
            }
        };
        checker.execute();
    }
}