net.sourceforge.docfetcher.util.AppUtil.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.docfetcher.util.AppUtil.java

Source

/*******************************************************************************
 * Copyright (c) 2010, 2011 Tran Nam Quang.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Tran Nam Quang - initial API and implementation
 *******************************************************************************/

package net.sourceforge.docfetcher.util;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.awt.AWTException;
import java.awt.Robot;

import net.sourceforge.docfetcher.util.annotations.NotNull;
import net.sourceforge.docfetcher.util.annotations.Nullable;
import net.sourceforge.docfetcher.util.gui.dialog.StackTraceWindow;
import net.sourceforge.docfetcher.gui.KeyCodeTranslator;
import net.sourceforge.docfetcher.enums.SettingsConf;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.aspectj.lang.annotation.SuppressAjWarnings;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;

import com.google.common.base.Charsets;
import com.google.common.io.Files;
import com.sun.jna.platform.win32.Shell32Util;

/**
 * A container for various utility methods. These aren't members of the
 * {@link Util} package because they depend on some enum constants defined in
 * {@link Const} and {@link Messages}. The <code>Const</code> constants must have
 * been set before any method of this class can be called, otherwise an
 * <code>Exception</code> will be thrown. Setting the <code>Msg</code>
 * constants, on the other hand, is optional.
 * 
 * @author Tran Nam Quang
 */
public final class AppUtil {

    public static enum Const {
        /** This is the internal program name, not the name that can be changed by the user! */
        PROGRAM_NAME, PROGRAM_VERSION, PROGRAM_BUILD_DATE, USER_DIR_PATH, IS_PORTABLE, IS_DEVELOPMENT_VERSION,;

        private String value;

        @Nullable
        public String get() {
            return value;
        }

        public void set(@NotNull String value) {
            Util.checkNotNull(value);
            if (this.value != null)
                throw new UnsupportedOperationException("Constant cannot be set twice: " + this.name());
            if (this == IS_PORTABLE)
                this.value = String.valueOf(Boolean.parseBoolean(value));
            else
                this.value = value;
        }

        public void set(boolean b) {
            set(String.valueOf(b));
        }

        private boolean asBoolean() {
            return Boolean.parseBoolean(value);
        }

        /**
         * Automatically initializes all constants. Should only be used for
         * debugging. Calling this method repeatedly or after the constants have
         * already been set manually will have no effect.
         */
        public static void autoInit() {
            if (initialized)
                return;

            PROGRAM_NAME.set("DocFetcher");
            PROGRAM_VERSION.set("Unspecified");
            PROGRAM_BUILD_DATE.set("Unspecified");
            USER_DIR_PATH.set(Util.USER_DIR_PATH);
            IS_PORTABLE.set("true");
            IS_DEVELOPMENT_VERSION.set("true");

            for (Const c : Const.values())
                if (c.value == null)
                    throw new IllegalStateException();

            initialized = true;
        }

        public static void clear() {
            for (Const c : Const.values())
                c.value = null;
            initialized = false;
        }
    }

    public static enum Messages {
        system_error("System Error"), confirm_operation("Confirm Operation"), invalid_operation(
                "Invalid Operation"), program_died_stacktrace_written("This program just died! "
                        + "The stacktrace below has been written to {0}."), program_running_launch_another(
                                "It seems {0} is already running. "
                                        + "Do you want to launch another instance?"), ok("&OK"), cancel("&Cancel"),;

        private static int setCount = 0;
        private String value;

        Messages(String defaultValue) {
            this.value = defaultValue;
        }

        @NotNull
        public String get() {
            return value;
        }

        public void set(@NotNull String value) {
            this.value = Util.checkNotNull(value);
            setCount++;
        }

        /**
         * Returns a string created from a <tt>java.text.MessageFormat</tt>
         * with the given argument(s).
         */
        private String format(Object... args) {
            return MessageFormat.format(value, args);
        }

        public static void checkInitialized() {
            Util.checkThat(values().length == setCount);
        }
    }

    private AppUtil() {
    }

    private static File appDataDir;
    private static Display display;
    private static boolean initialized = false;

    public static void setDisplay(@NotNull Display display) {
        Util.checkNotNull(display);
        Util.checkThat(AppUtil.display == null);
        AppUtil.display = display;

        display.disposeExec(new Runnable() {
            public void run() {
                AppUtil.display = null;
            }
        });
    }

    public static void checkConstInitialized() {
        if (!initialized) {
            for (Const value : Const.values()) {
                if (value.value == null)
                    throw new IllegalStateException("Uninitialized constant: " + value.name());
            }
            initialized = true;
        }
    }

    public static void ensureNoDisplay() {
        if (display != null)
            throw new IllegalStateException("Display has already been initialized.");
    }

    private static void ensureDisplay() {
        if (display == null)
            throw new IllegalStateException("Display has not been initialized.");
    }

    /**
     * Checks whether an instance of this program is already running. The check
     * relies on a lockfile created in the temporary directory. The argument is
     * the program name to be displayed in the confirmation dialog (see below).
     * <p>
     * The boolean return value should be interpreted as
     * "proceed with running this instance?". More specifically:
     * <ul>
     * <li>If there is no other instance, true is returned.
     * <li>If there is another instance, a confirmation dialog is shown, which
     * asks the user whether this instance should be launched. If the user
     * confirms, true is returned, otherwise false.
     * </ul>
     * The filename of the lockfile includes the username and the working
     * directory, which means:
     * <ul>
     * <li>The same instance may be launched multiple times by different users.
     * <li>Multiple instances from different locations can be run simultaneously
     * by the same user.
     * </ul>
     * <p>
     * <b>Note</b>: The message container must be loaded before calling this
     * method, otherwise the confirmation dialog will show untranslated strings.
     */
    // TODO doc: before calling this method, set USER_DIR_PATH, PROGRAM_NAME and all Msg enums
    public static boolean checkSingleInstance() {
        checkConstInitialized();
        ensureNoDisplay();

        /*
         * The lockfile is created in the system's temporary directory to make
         * sure it gets deleted after OS shutdown - neither File.deleteOnExit()
         * nor a JVM shutdown hook can guarantee this.
         * 
         * The name of the lockfile includes the base64-encoded working
         * directory, thus avoiding filename collisions if the same user runs
         * multiple program instances from different locations. We'll also put
         * in a SHA-1 digest of the working directory, just in case the
         * base64-encoded working directory exceeds the system's allowed
         * filename length limit, which we'll assume to be 255 characters.
         * 
         * Note that the included program name is also encoded as base64 since a
         * developer might have (accidentally?) set a program name that contains
         * special characters such as '/'.
         */

        String dirPath = Const.USER_DIR_PATH.value;
        String shaDirPath = DigestUtils.shaHex(dirPath);
        String programName64 = encodeBase64(Const.PROGRAM_NAME.value);
        String username64 = encodeBase64(System.getProperty("user.name"));
        String dirPath64 = encodeBase64(dirPath);

        String lockname = String.format(".lock-%s-%s-%s-%s.", // dot at the end is intentional
                shaDirPath, programName64, username64, dirPath64);
        // Usual filename length limit is 255 characters
        lockname = lockname.substring(0, Math.min(lockname.length(), 250));
        File lockfile = new File(Util.TEMP_DIR, lockname);

        if (lockfile.exists()) {
            if (SettingsConf.Bool.AllowOnlyOneInstance.get()) {
                sendHotkeyToFront();
                return false;
            } else {
                // Show message, ask whether to launch new instance or to abort
                Display display = new Display();
                Shell shell = new Shell(display);
                MessageBox msgBox = new MessageBox(shell,
                        SWT.ICON_QUESTION | SWT.OK | SWT.CANCEL | SWT.PRIMARY_MODAL);
                msgBox.setText(Messages.confirm_operation.value);
                msgBox.setMessage(Messages.program_running_launch_another.format(Const.PROGRAM_NAME.value));
                int ans = msgBox.open();
                display.dispose();
                if (ans != SWT.OK) {
                    sendHotkeyToFront();
                    return false;
                }
                /*
                 * If the user clicks OK, we'll take over the lockfile we found and
                 * delete it on exit. That means: (1) If there's another instance
                 * running, we'll wrongfully "steal" the lockfile from it. (2) If
                 * there's no other instance running (probably because it crashed or
                 * was killed by the user), we'll rightfully take over an orphaned
                 * lockfile. This behavior is okay, assuming the second case is more
                 * likely.
                 */
            }
        } else {
            try {
                lockfile.createNewFile();
            } catch (IOException e) {
            }
        }
        lockfile.deleteOnExit();
        return true;
    }

    public static void sendHotkeyToFront() {
        try {
            int one = SettingsConf.IntArray.HotkeyToFront.get()[0];
            one = KeyCodeTranslator.translateSWTKey(one);
            int two = SettingsConf.IntArray.HotkeyToFront.get()[1];
            two = KeyCodeTranslator.translateSWTKey(two);
            Robot robot = new Robot();
            robot.keyPress(one);
            robot.keyPress(two);
            robot.delay(500);
            robot.keyRelease(two);
            robot.keyRelease(one);
        } catch (AWTException e) {
            AppUtil.showStackTrace(e);
        }
    }

    private static String encodeBase64(String input) {
        String encodedBytes = Base64.encodeBase64URLSafeString(input.getBytes());
        return new String(encodedBytes);
    }

    /**
     * Returns a shell associated with this program. This method will first try
     * to return the currently active shell. If no shell is active, it will try
     * to return the first inactive shell. If there are no shells at all, null
     * is returned.
     * <p>
     * This method should not be called from a non-GUI thread, and it should not
     * be called before the first display is created.
     */
    private static Shell getActiveShell() {
        ensureDisplay();
        Shell shell = display.getActiveShell();
        if (shell != null)
            return shell;
        Shell[] shells = display.getShells();
        if (shells.length != 0)
            return shells[0];
        return null;
    }

    /**
     * Shows the given message in an error message box, with "System Error" as
     * the shell title. If <tt>isSevere</tt> is true, an error icon is shown,
     * otherwise a warning icon.
     * <p>
     * This method can be used before any GUI components are created, because it
     * creates its own display and shell. If there is already a GUI,
     * {@link #showErrorMsg} should be used instead.
     */
    public static void showErrorOnStart(String message, boolean isSevere) {
        int style = SWT.OK | (isSevere ? SWT.ICON_ERROR : SWT.ICON_WARNING);
        showErrorOnStart(message, style);
    }

    public static int showErrorOnStart(String message, int style) {
        checkConstInitialized();
        ensureNoDisplay();

        Display display = new Display();
        Shell shell = new Shell(display);
        MessageBox msgBox = new MessageBox(shell, style);
        msgBox.setText(Messages.system_error.value);
        msgBox.setMessage(message);
        int buttonID = msgBox.open();
        shell.dispose();
        display.dispose();
        return buttonID;
    }

    /**
     * Shows the given message in a confirmation message box and returns the
     * user's answer, either <tt>SWT.OK</tt> or <tt>SWT.CANCEL</tt>. If
     * <tt>isSevere</tt> is true, a warning icon is shown, otherwise a question
     * icon.
     * <p>
     * This method may be called from a non-GUI thread. It should not be called
     * before the first shell is created.
     */
    public static boolean showConfirmation(final String message, final boolean warningNotQuestion) {
        checkConstInitialized();
        ensureDisplay();
        class MyRunnable implements Runnable {
            private boolean answer;

            public void run() {
                int style = SWT.OK | SWT.CANCEL;
                style |= warningNotQuestion ? SWT.ICON_WARNING : SWT.ICON_QUESTION;
                MessageBox msgBox = new MessageBox(getActiveShell(), style);
                msgBox.setText(Messages.confirm_operation.value);
                msgBox.setMessage(message);
                answer = msgBox.open() == SWT.OK;
            }
        }
        MyRunnable myRunnable = new MyRunnable();
        Util.runSwtSafe(display, myRunnable);
        return myRunnable.answer;
    }

    /**
     * Shows the given message in a message box with an information icon. This
     * method may be called from a non-GUI thread. It should not be called
     * before the first shell is created.
     */
    public static void showInfo(final String message) {
        checkConstInitialized();
        ensureDisplay();

        Util.runSwtSafe(display, new Runnable() {
            public void run() {
                MessageBox msgBox = new MessageBox(getActiveShell(), SWT.ICON_INFORMATION | SWT.OK);
                msgBox.setMessage(message);
                msgBox.open();
            }
        });
    }

    /**
     * Shows the given message in an error message box. If
     * <tt>errorNotWarning</tt> is true, an error icon is shown, otherwise a
     * warning icon. If <tt>isUserError</tt> is true, the shell title is set to
     * "Invalid Operation", otherwise "System Error".
     * <p>
     * This method may be called from a non-GUI thread. It should not be called
     * before the first shell is created.
     */
    public static void showError(@NotNull final String message, final boolean errorNotWarning,
            final boolean isUserError) {
        checkConstInitialized();
        ensureDisplay();

        Util.runSwtSafe(display, new Runnable() {
            public void run() {
                int style = SWT.OK;
                style |= errorNotWarning ? SWT.ICON_ERROR : SWT.ICON_WARNING;
                MessageBox msgBox = new MessageBox(getActiveShell(), style);
                msgBox.setText(isUserError ? Messages.invalid_operation.value : Messages.system_error.value);
                msgBox.setMessage(message);
                msgBox.open();
            }
        });
    }

    /**
     * Prints the stacktrace to {@link System.err} and to a stacktrace file. In
     * addition to that, the stacktrace is displayed in an error window. The
     * printouts for the file and the error window are prepended with some
     * useful debug information about the program.
     * <p>
     * This method creates its own display, and should therefore be called
     * either before the application's display has been created, or after the
     * application's display has been disposed. In between,
     * {@link #showStackTrace} should be used instead.
     */
    public static void showStackTraceInOwnDisplay(Throwable throwable) {
        checkConstInitialized();
        ensureNoDisplay();

        Display display = new Display();
        showStackTrace(display, throwable);
        display.dispose();
    }

    /**
     * Prints the stacktrace to {@link System.err} and to a stacktrace file. In
     * addition to that, the stacktrace is displayed in an error window. The
     * printouts for the file and the error window are prepended with some
     * useful debug information about the program.
     * <p>
     * It is safe to call this method from a non-GUI thread. The method should
     * not be called before the first display has been created. In the latter case
     * {@link #showStackTraceOnStart} should be used instead.
     */
    public static void showStackTrace(Throwable throwable) {
        checkConstInitialized();
        ensureDisplay();

        showStackTrace(Display.getDefault(), throwable);
    }

    @SuppressAjWarnings
    private static void showStackTrace(final Display display, final Throwable throwable) {
        // Print stacktrace to System.err
        throwable.printStackTrace();

        // Prepend useful program info to the stacktrace
        StringBuilder sb = new StringBuilder();
        sb.append("program.name=" + Const.PROGRAM_NAME.value + Util.LS);
        sb.append("program.version=" + Const.PROGRAM_VERSION.value + Util.LS);
        sb.append("program.build=" + Const.PROGRAM_BUILD_DATE.value + Util.LS);
        sb.append("program.portable=" + Const.IS_PORTABLE.asBoolean() + Util.LS);
        String[] keys = { "java.runtime.name", "java.runtime.version", "java.version", "sun.arch.data.model",
                "os.arch", "os.name", "os.version", "user.language" };
        for (String key : keys)
            sb.append(key + "=" + System.getProperty(key) + Util.LS);

        // Get stacktrace as string
        StringWriter writer = new StringWriter();
        throwable.printStackTrace(new PrintWriter(writer));
        sb.append(writer.toString());
        final String trace = sb.toString();

        // Write stacktrace to file
        String timestamp = new SimpleDateFormat("yyyyMMdd-HHmm").format(new Date());
        String traceFilename = "stacktrace_" + timestamp + ".txt";
        final File traceFile = new File(getAppDataDir(), traceFilename);
        try {
            Files.write(trace, traceFile, Charsets.UTF_8);
        } catch (IOException e) {
            e.printStackTrace(); // We'll give up here
        }

        // Show stacktrace in error window
        Util.runSwtSafe(display, new Runnable() {
            public void run() {
                StackTraceWindow window = new StackTraceWindow(display);
                window.setTitle(throwable.getClass().getSimpleName());
                String path = Util.getSystemAbsPath(traceFile);
                String link = String.format("<a href=\"%s\">%s</a>", path, path);
                String msg = Messages.program_died_stacktrace_written.format(link);
                window.setText(msg);
                Image icon = display.getSystemImage(SWT.ICON_WARNING);
                window.setTitleImage(icon);

                /*
                 * It appears that when you paste a stracktrace with Windows
                 * newlines into the text field of a SourceForge.net bug report,
                 * the newlines will end up being duplicated. The workaround is
                 * to use Linux newlines in the stacktrace window.
                 */
                window.setStackTrace(Util.ensureLinuxLineSep(trace));

                window.open();
            }
        });
    }

    /**
     * Returns a directory where the program may store data. The directory is
     * created if necessary. The rules for choosing the directory are as
     * follows:
     * <p>
     * <ul>
     * <li>If the {@code portable} flag was set, the current working directory
     * is returned.
     * <li>If the {@code is development version} flag was set, the "bin"
     * directory under the current working directory is returned.
     * <li>Otherwise, the returned directory is platform-dependent: On Windows,
     * the application data folder + program name is returned, on Linux the home
     * folder + dot + lowercase program name.
     * </ul>
     */
    public static File getAppDataDir() {
        checkConstInitialized();

        if (appDataDir != null)
            return appDataDir; // Return cached value

        String appDataDirOverride = System.getenv("DOCFETCHER_HOME");
        if (appDataDirOverride != null) {
            File appDataDir = Util.getCanonicalFile(appDataDirOverride);
            if (!appDataDir.exists())
                appDataDir.mkdirs(); // may fail
            if (appDataDir.isDirectory()) {
                AppUtil.appDataDir = appDataDir; // Store value in cache
                return appDataDir;
            }
        }

        String programName = Const.PROGRAM_NAME.value;
        File appDataDir = null;
        if (Const.IS_DEVELOPMENT_VERSION.asBoolean()) {
            // The development flag has higher priority than the portable flag
            // TODO post-release-1.1: Remove this part of the if-clause
            appDataDir = new File("bin");
        } else if (Const.IS_PORTABLE.asBoolean()) {
            appDataDir = new File(Const.USER_DIR_PATH.value);
        } else if (Util.IS_WINDOWS) {
            // Windows 7/Vista: C:\Users\<UserName>\AppData\<ProgramName>
            // Windows XP/2000: C:\Documents and Settings\<UserName>\Application Data\<ProgramName>
            String winAppData = System.getenv("APPDATA");
            if (winAppData == null)
                /*
                 * Bug #2812637: The previous System.getenv("APPDATA") call
                 * returns null if DocFetcher is started as an alternative user
                 * via the executable's "Run as..." context menu entry. If this
                 * happens, we'll have to fall back to this JNA-based
                 * workaround.
                 */
                winAppData = Shell32Util.getFolderPath(0x001a); // CSIDL_APPDATA = 0x001a
            if (winAppData == null)
                throw new IllegalStateException("Cannot find application data folder.");
            appDataDir = new File(winAppData, programName);
        } else if (Util.IS_LINUX || Util.IS_MAC_OS_X) {
            // Linux: /home/<UserName>/.<LowerCaseProgramName>
            // Mac OS X: /Users/<UserName>/.<LowerCaseProgramName>
            appDataDir = new File(Util.USER_HOME_PATH, "." + programName.toLowerCase());
        } else {
            throw new IllegalStateException();
        }

        appDataDir.mkdirs();
        AppUtil.appDataDir = appDataDir; // Store value in cache
        return appDataDir;
    }

    public static boolean isPortable() {
        checkConstInitialized();
        return Const.IS_PORTABLE.asBoolean();
    }

    @NotNull
    public static String getImageDir() {
        checkConstInitialized();
        if (Const.IS_DEVELOPMENT_VERSION.asBoolean())
            return "dist/img";
        if (Util.IS_MAC_OS_X && !Const.IS_PORTABLE.asBoolean())
            return "../Resources/img";
        return "img";
    }

}