org.eclipse.swt.snippets.SnippetExplorer.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.swt.snippets.SnippetExplorer.java

Source

/*******************************************************************************
 * Copyright (c) 2019 Paul Pazderski and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Paul Pazderski - initial API and implementation
 *******************************************************************************/
package org.eclipse.swt.snippets;

import java.io.*;
import java.lang.ProcessBuilder.*;
import java.lang.reflect.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;
import java.util.regex.*;
import java.util.regex.Pattern;

import org.eclipse.swt.*;
import org.eclipse.swt.custom.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.layout.*;
import org.eclipse.swt.program.*;
import org.eclipse.swt.widgets.*;

/**
 * A useful application to list, filter and run the available Snippets.
 */
public class SnippetExplorer {

    private static final String USAGE_EXPLANATION = "Welcome to the SnippetExplorer!\n" + "\n"
            + "This tool will help you to explore and test the large collection of SWT example Snippets. "
            + "You can use the text field on top to filter the Snippets by there description or Snippet number. "
            + "To start a Snippet you can either double click its entry, press enter or use the button below. "
            + "It is also possible to start multiple Snippets at once. (exact behavior depends on selected Snippet-Runner)\n"
            + "\n"
            + "It is recommended to start the Snippet Explorer connected to a console since some of the Snippets "
            + "print useful informations to the console or do not open a window at all.\n" + "\n"
            + "The Explorer supports (dependent on your OS and environment) different modes to start Snippets. Those runners are:\n"
            + "\n" + " \u2022 Thread Runner: Snippets are executed as threads of the Explorer.\n"
            + "\t- This runner is only available if the environment supports multiple Displays at the same time. (only Windows at the moment)\n"
            + "\t- Multiple Snippets can be run parallel using this runner.\n"
            + "\t- All running Snippets are closed when the explorer exits.\n"
            + "\t- If to many Snippets are run in parallel SWT may run out of handles.\n"
            + "\t- If a Snippet calls System.exit it will also force the explorer itself and all other running Snippets to exit as well.\n"
            + "\n" + " \u2022 Process Runner: Snippets are executed as separate processes.\n"
            + "\t- This runner is only available if a JRE was found which can be used to start the Snippets.\n"
            + "\t- Multiple Snippets can be run parallel using this runner.\n"
            + "\t- This runner is more likely to fail Snippet launch due to incomplete classpath or other launch problems.\n"
            + "\t- When the explorer exits it try to close all running Snippets but has less control over it as the Thread runner.\n"
            + "\t- Unlike the Thread runner the Process runner is resisted to faulty Snippets. (e.g. Snippets calling System.exit)\n"
            + "\n" + " \u2022 Serial Runner: Snippets are executed one after another instead of the explorer.\n"
            + "\t- This runner is always available.\n" + "\t- Cannot run Snippets parallel.\n"
            + "\t- To run Snippets the explorer gets closed, executes the selected Snippets one after another in the same JVM "
            + "and after the last Snippet has finished restarts the Snippet Explorer.\n"
            + "\t- A Snippet calling System.exit will stop the Snippet chain and the explorer itself can not restart.";

    /** Max length for Snippet description in the main table. */
    private static final int MAX_DESCRIPTION_LENGTH_IN_TABLE = 80;
    /**
     * If the user tries to start more than this number of Snippets at once a
     * warning message is shown.
     */
    private static final int START_MANY_SNIPPETS_WARNING_THREASHOLD = 10;
    /** Message shown in the filter text field if empty. */
    private static final String FILTER_HINT = "type to filter list";
    /**
     * Delay in milliseconds before a changed filter value is applied on the list.
     */
    private static final int FILTER_DELAY_MS = 200;
    /**
     * Time snippets get to stop before tried to be killed forcefully. (currently
     * applied per runner)
     */
    private static final int SHUTDOWN_GRACE_TIME_MS = 5000;
    /** Link to online snippet source. Used if no local source is available. */
    private static final String SNIPPET_SOURCE_LINK_TEMPLATE = "https://git.eclipse.org/c/platform/"
            + "eclipse.platform.swt.git/tree/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/%s.java";

    /**
     * Whether or not SWT support creating of multiple {@link Display} instances on
     * the current system. Required to use the thread runner mode.
     */
    private static boolean multiDisplaySupport;
    /**
     * The command used to invoke the java binary. May be <code>null</code> if not
     * found. Required to use the process runner mode.
     */
    private static String javaCommand;
    /** The list of available Snippets. */
    private static List<Snippet> snippets;

    /** The runner used for thread mode. */
    private final SnippetRunner THREAD_RUNNER = new SnippetRunnerThread();
    /** The runner used for process mode. */
    private final SnippetRunner PROCESS_RUNNER = new SnippetRunnerProcess();

    private Display display;
    private Shell shell;

    /** Helper to perform the delayed list update if filter changed. */
    private ListUpdater listUpdater;
    /** Text field to filter Snippet list. */
    private Text filterField;
    /** The main table listing available Snippets. */
    private Table snippetTable;
    /** Button to run selected Snippets. */
    private Button startSelectedButton;
    /** Snippet runner selection. */
    private Combo runnerCombo;
    /** The tabfolder to show information for selected Snippet. */
    private TabFolder infoTabs;
    /** Element to show Snippet description or general help. */
    private StyledText descriptionView;
    /**
     * Element to show Snippets source code or link to source if not local
     * available.
     */
    private StyledText sourceView;
    /** Element to show Snippet preview if possible. */
    private Label previewImageLabel;

    /**
     * The snippet runner used for next snippet start. If <code>null</code> Snippets
     * are run serial.
     */
    private SnippetRunner snippetRunner;
    /** Used to map {@link #runnerCombo} selection to actual Snippet runner. */
    private List<SnippetRunner> runnerMapping = new ArrayList<>();
    /** The Snippet currently shown in {@link #infoTabs}. May be <code>null</code>. */
    private Snippet currentInfoSnippet = null;
    /** Snippets currently run in serial runner mode. May be <code>null</code>. */
    private List<Snippet> serialSnippets;
    /**
     * The SnippetExplorer location for the next {@link Shell#open()}. Used for
     * restart after serial runner finished.
     */
    private Point nextExplorerLocation = null;

    /**
     * SnippetExplorer main method.
     *
     * @param args does not parse any arguments
     */
    public static void main(String[] args) throws Exception {
        final String os = System.getProperty("os.name");
        multiDisplaySupport = (os != null && os.toLowerCase().contains("windows"));
        if (canRunCommand("java")) {
            javaCommand = "java";
        } else {
            final String javaHome = System.getProperty("java.home");
            if (javaHome != null) {
                final Path java = Paths.get(javaHome, "bin", "java");
                java.normalize();
                if (canRunCommand(java.toString())) {
                    javaCommand = java.toString();
                }
            }
        }

        snippets = loadSnippets();
        snippets.sort((a, b) -> {
            int cmp = Integer.compare(a.snippetNum, b.snippetNum);
            if (cmp == 0) {
                cmp = a.snippetName.compareTo(b.snippetName);
            }
            return cmp;
        });

        new SnippetExplorer().open();
    }

    /**
     * Test if the given command can be executed.
     *
     * @param command command to test
     * @return <code>false</code> if executing the command failed for any reason
     */
    private static boolean canRunCommand(String command) {
        try {
            final Process p = Runtime.getRuntime().exec(command);
            p.waitFor(150, TimeUnit.MILLISECONDS);
            if (p.isAlive()) {
                p.destroy();
                p.waitFor(100, TimeUnit.MILLISECONDS);
                if (p.isAlive()) {
                    p.destroyForcibly();
                }
            }
            return true;
        } catch (Exception ex) {
            return false;
        }
    }

    public SnippetExplorer() {
    }

    /**
     * Initializes and shows the SnippetExplorer. The method doesn't return until
     * the explorer is closed or otherwise disposed.
     */
    public void open() {
        initialize();
        runEventLoop();
    }

    /**
     * Initialize the SnippetExplorer. Can be called again if the current explorer
     * was properly disposed.
     */
    private void initialize() {
        display = Display.getDefault();
        snippetRunner = null;
        shell = new Shell(display);
        if (nextExplorerLocation != null) {
            shell.setLocation(nextExplorerLocation);
        }
        shell.setText("SWT Snippet Explorer");

        createControls(shell);

        final String[] columns = new String[] { "Name", "Description" };
        for (String col : columns) {
            final TableColumn tableCol = new TableColumn(snippetTable, SWT.NONE);
            tableCol.setText(col);
            tableCol.setToolTipText(col);
            tableCol.setResizable(true);
            tableCol.setMoveable(true);
        }
        updateTable(null);

        for (TableColumn col : snippetTable.getColumns()) {
            col.pack();
        }
        final GridData rightSideLayout = (GridData) infoTabs.getLayoutData();
        final Point tableSize = snippetTable.getSize();
        rightSideLayout.widthHint = tableSize.x;
        rightSideLayout.heightHint = tableSize.y;
        shell.pack();
        shell.open();
    }

    /** Initialize the SnippetExplorer controls.
     *
     * @param shell parent shell
     */
    private void createControls(Shell shell) {
        shell.setLayout(new FormLayout());

        if (listUpdater == null) {
            listUpdater = new ListUpdater();
            listUpdater.start();
        }

        final Composite leftContainer = new Composite(shell, SWT.NONE);
        leftContainer.setLayout(new GridLayout());

        final Sash splitter = new Sash(shell, SWT.BORDER | SWT.VERTICAL);
        final int splitterWidth = 3;
        splitter.addListener(SWT.Selection, e -> splitter.setBounds(e.x, e.y, e.width, e.height));

        final Composite rightContainer = new Composite(shell, SWT.NONE);
        rightContainer.setLayout(new GridLayout());

        FormData formData = new FormData();
        formData.left = new FormAttachment(0, 0);
        formData.right = new FormAttachment(splitter, 0);
        formData.top = new FormAttachment(0, 0);
        formData.bottom = new FormAttachment(100, 0);
        leftContainer.setLayoutData(formData);

        formData = new FormData();
        formData.left = new FormAttachment(50, 0);
        formData.right = new FormAttachment(50, splitterWidth);
        formData.top = new FormAttachment(0, 0);
        formData.bottom = new FormAttachment(100, 0);
        splitter.setLayoutData(formData);
        splitter.addListener(SWT.Selection, event -> {
            final FormData splitterFormData = (FormData) splitter.getLayoutData();
            splitterFormData.left = new FormAttachment(0, event.x);
            splitterFormData.right = new FormAttachment(0, event.x + splitterWidth);
            shell.layout();
        });

        formData = new FormData();
        formData.left = new FormAttachment(splitter, 0);
        formData.right = new FormAttachment(100, 0);
        formData.top = new FormAttachment(0, 0);
        formData.bottom = new FormAttachment(100, 0);
        rightContainer.setLayoutData(formData);

        filterField = new Text(leftContainer,
                SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL);
        filterField.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
        filterField.setMessage(FILTER_HINT);
        filterField.addListener(SWT.Modify, event -> {
            listUpdater.updateInMs(FILTER_DELAY_MS);
        });
        snippetTable = new Table(leftContainer, SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION);
        snippetTable.setLinesVisible(true);
        snippetTable.setHeaderVisible(true);
        final GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
        data.heightHint = 500;
        snippetTable.setLayoutData(data);
        snippetTable.addListener(SWT.MouseDoubleClick, event -> {
            final Point clickPoint = new Point(event.x, event.y);
            launchSnippet(snippetTable.getItem(clickPoint));
        });
        snippetTable.addListener(SWT.KeyUp, event -> {
            if (event.keyCode == '\r' || event.keyCode == '\n') {
                launchSnippet(snippetTable.getSelection());
            }
        });

        final Composite buttonRow = new Composite(leftContainer, SWT.NONE);
        buttonRow.setLayout(new GridLayout(3, false));
        buttonRow.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));

        startSelectedButton = new Button(buttonRow, SWT.LEAD);
        startSelectedButton.setText("  Start &selected Snippets");
        snippetTable.addListener(SWT.Selection, event -> {
            startSelectedButton.setEnabled(snippetTable.getSelectionCount() > 0);
            updateInfoTab(snippetTable.getSelection());
        });
        startSelectedButton.setEnabled(snippetTable.getSelectionCount() > 0);
        startSelectedButton.addListener(SWT.Selection, event -> {
            launchSnippet(snippetTable.getSelection());
        });

        final Label runnerLabel = new Label(buttonRow, SWT.NONE);
        runnerLabel.setText("Snippet Runner:");
        runnerLabel.setLayoutData(new GridData(SWT.TRAIL, SWT.CENTER, true, false));

        runnerCombo = new Combo(buttonRow, SWT.TRAIL | SWT.DROP_DOWN | SWT.READ_ONLY);
        runnerMapping.clear();
        if (multiDisplaySupport) {
            runnerCombo.add("Thread");
            runnerMapping.add(THREAD_RUNNER);
        }
        if (javaCommand != null) {
            runnerCombo.add("Process");
            runnerMapping.add(PROCESS_RUNNER);
        }
        runnerCombo.add("Serial");
        runnerMapping.add(null);
        runnerCombo.setData(runnerMapping);
        runnerCombo.addListener(SWT.Modify, event -> {
            if (runnerMapping.size() > runnerCombo.getSelectionIndex()) {
                snippetRunner = runnerMapping.get(runnerCombo.getSelectionIndex());
            } else {
                System.err.println("Unknown runner index " + runnerCombo.getSelectionIndex());
            }
        });
        runnerCombo.select(0);

        infoTabs = new TabFolder(rightContainer, SWT.TOP);
        infoTabs.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

        descriptionView = new StyledText(infoTabs, SWT.MULTI | SWT.WRAP | SWT.READ_ONLY | SWT.V_SCROLL);

        sourceView = new StyledText(infoTabs, SWT.MULTI | SWT.READ_ONLY | SWT.V_SCROLL | SWT.H_SCROLL);
        setMonospaceFont(sourceView);

        final ScrolledComposite previewContainer = new ScrolledComposite(infoTabs, SWT.V_SCROLL | SWT.H_SCROLL);
        previewImageLabel = new Label(previewContainer, SWT.NONE);
        previewContainer.setContent(previewImageLabel);

        final TabItem descriptionTab = new TabItem(infoTabs, SWT.NONE);
        descriptionTab.setText("Description");
        descriptionTab.setControl(descriptionView);
        final TabItem sourceTab = new TabItem(infoTabs, SWT.NONE);
        sourceTab.setText("Source");
        sourceTab.setControl(sourceView);
        final TabItem previewTab = new TabItem(infoTabs, SWT.NONE);
        previewTab.setText("Preview");
        previewTab.setControl(previewContainer);

        updateInfoTab(null, true);
        updateInfoTab(snippetTable.getSelection());
    }

    /**
     * Try to set a monospace font for this control. Other font properties like
     * fontSize remain unchanged.
     *
     * @param control control to modify
     */
    private void setMonospaceFont(Control control) {
        final FontData[] fontData = control.getFont().getFontData();
        Font font = null;
        if (font == null) {
            font = tryCreateFont("Consolas", fontData);
        }
        if (font == null) {
            font = tryCreateFont("DejaVu Sans Mono", fontData);
        }
        if (font == null) {
            font = tryCreateFont("Noto Mono", fontData);
        }
        if (font == null) {
            font = tryCreateFont("Liberation Mono", fontData);
        }
        if (font == null) {
            font = tryCreateFont("Ubuntu Mono", fontData);
        }
        if (font == null) {
            font = tryCreateFont("Courier New", fontData);
        }
        if (font == null) {
            font = tryCreateFont("Courier", fontData);
        }
        if (font == null) {
            font = tryCreateFont("Monospace", fontData);
        }

        if (font != null) {
            control.setFont(font);
        }
    }

    /**
     * Try to create font with given name based on existing {@link FontData}.
     *
     * @param fontName     name of font to create
     * @param existingData existing font data to be used for other font attributes
     * @return the created font or <code>null</code> if failed (e.g. no font
     *         available with given name)
     */
    private Font tryCreateFont(String fontName, FontData[] existingData) {
        for (int i = 0; i < existingData.length; i++) {
            existingData[i].setName(fontName);
        }
        try {
            return new Font(display, existingData);
        } catch (SWTException e) {
            return null;
        }
    }

    /**
     * Update the info tabs for the given items. The behavior may change. At the
     * moment informations are only shown for single items.
     *
     * @param items items to show info for
     */
    private void updateInfoTab(TableItem[] items) {
        // if multiple snippets are selected no info are shown
        if (items.length != 1) {
            updateInfoTab((TableItem) null, false);
        } else {
            updateInfoTab(items[0], false);
        }
    }

    /**
     * Update the info tabs (right side of the explorer) for the given item.
     *
     * @param item  the selected item containing Snippet metadata (may be
     *              <code>null</code>)
     * @param force the tabs are only updated if they not already show info for the
     *              given item. If this is <code>true</code> the tabs are updated
     *              anyway.
     */
    private void updateInfoTab(TableItem item, boolean force) {
        final Snippet snippet = (item != null && item.getData() instanceof Snippet) ? (Snippet) item.getData()
                : null;
        if (!force && currentInfoSnippet == snippet) {
            return;
        }
        if (snippet == null) {
            descriptionView.setText(USAGE_EXPLANATION);
            sourceView.setText("");
            updatePreviewImage(null, "");
        } else {
            descriptionView.setText(snippet.snippetName + "\n\n" + snippet.description);
            if (snippet.source == null) {
                sourceView.setWordWrap(true);
                final String msg = "No source available for " + snippet.snippetName
                        + " but you may find it at:\n\n";
                final String link = String.format(Locale.ROOT, SNIPPET_SOURCE_LINK_TEMPLATE, snippet.snippetName);
                sourceView.setText(msg + link);

                final StyleRange linkStyle = new StyleRange();
                linkStyle.start = msg.length();
                linkStyle.length = link.length();
                linkStyle.underline = true;
                linkStyle.underlineStyle = SWT.UNDERLINE_LINK;
                sourceView.setStyleRange(linkStyle);

                sourceView.addListener(SWT.MouseDown, event -> {
                    int offset = sourceView.getOffsetAtPoint(new Point(event.x, event.y));
                    if (offset != -1) {
                        try {
                            final StyleRange style = sourceView.getStyleRangeAtOffset(offset);
                            if (style != null && style.underline && style.underlineStyle == SWT.UNDERLINE_LINK) {
                                Program.launch(link);
                            }
                        } catch (IllegalArgumentException e) {
                            // no character under event.x, event.y
                        }
                    }
                });
            } else {
                sourceView.setWordWrap(false);
                sourceView.setText(snippet.source);
            }
            try {
                final Image previewImage = getPreviewImage(snippet);
                updatePreviewImage(previewImage, previewImage == null ? "No preview image available." : "");
            } catch (IOException e) {
                updatePreviewImage(null, "Failed to load preview image: " + e);
            }
        }
        currentInfoSnippet = snippet;
    }

    /**
     * Update the control showing the image. If <em>image</em> is <code>null</code>
     * show the <em>text</em> instead.
     *
     * @param image the image to show
     * @param text  the alternative text to show if image is <code>null</code>
     */
    private void updatePreviewImage(Image image, String text) {
        final Image previousImage = previewImageLabel.getImage();
        previewImageLabel.setImage(image);
        if (image == null && text != null) {
            previewImageLabel.setText(text);
        }
        if (previousImage != null) {
            previousImage.dispose();
        }
        previewImageLabel.pack(true);
    }

    /**
     * Get the preview image for the Snippet.
     *
     * @param snippet Snippet's metadata to load preview image for
     * @return the preview image or <code>null</code> if none available
     * @throws IOException if image loading failed
     */
    private Image getPreviewImage(Snippet snippet) throws IOException {
        final Path previewFile = Paths.get("previews", snippet.snippetName + ".png");
        if (Files.exists(previewFile)) {
            try (InputStream imageStream = Files.newInputStream(previewFile)) {
                return new Image(display, imageStream);
            }
        }
        try (InputStream imageStream = SnippetExplorer.class
                .getResourceAsStream("/previews/" + snippet.snippetName + ".png")) {
            if (imageStream != null) {
                return new Image(display, imageStream);
            }
        }
        try (InputStream imageStream = ClassLoader
                .getSystemResourceAsStream("previews/" + snippet.snippetName + ".png")) {
            if (imageStream != null) {
                return new Image(display, imageStream);
            }
        }
        return null;
    }

    /**
     * Load all available Snippets from the preconfigured source path and from the
     * current classppath.
     *
     * @return all found Snippets (never <code>null</code>)
     */
    private static List<Snippet> loadSnippets() {
        // Similar to SnippetLauncher this explorer tries to load Snippet0 to Snippet500
        // even if no sources are available. This array is used to track which snippets
        // are already loaded from source.
        final boolean[] loadedSnippets = new boolean[501];
        final List<Snippet> snippets = new ArrayList<>();

        // load snippets from source directory
        final Path sourceDir = SnippetsConfig.SNIPPETS_SOURCE_DIR.toPath();
        if (Files.exists(sourceDir)) {
            try (DirectoryStream<Path> files = Files.newDirectoryStream(sourceDir, "*.java")) {
                for (Path file : files) {
                    try {
                        final Snippet snippet = snippetFromSource(file);
                        if (snippet == null) {
                            continue;
                        }
                        snippets.add(snippet);
                        if (snippet.snippetNum >= 0) {
                            loadedSnippets[snippet.snippetNum] = true;
                        }
                    } catch (ClassNotFoundException | IOException ex) {
                        System.err.println("Failed to load snippet from " + file + ". Error: " + ex);
                    }
                }
            } catch (IOException ex) {
                System.err.println("Failed to access source directory " + sourceDir + ". Error: " + ex);
            }
        }

        // load snippets from classpath
        for (int i = 0; i < loadedSnippets.length; i++) {
            if (!loadedSnippets[i]) {
                final int snippetNum = i;
                final String snippetName = "Snippet" + snippetNum;
                final Class<?> snippetClass;
                try {
                    snippetClass = Class.forName(SnippetsConfig.SNIPPETS_PACKAGE + "." + snippetName, false,
                            SnippetExplorer.class.getClassLoader());
                } catch (ClassNotFoundException e) {
                    continue;
                }
                final String[] arguments = SnippetsConfig.getSnippetArguments(snippetNum);
                snippets.add(new Snippet(snippetNum, snippetName, snippetClass, null, null, arguments));
            }
        }

        return snippets;
    }

    /**
     * Load Snippet metadata from the Java source file found at the given path.
     *
     * @param sourceFile the source file to load
     * @return the gathered Snippet metadata or <code>null</code> if failed
     * @throws IOException            on errors loading the source file
     * @throws ClassNotFoundException if loading the Snippets corresponding class
     *                                file failed
     */
    private static Snippet snippetFromSource(Path sourceFile) throws IOException, ClassNotFoundException {
        final Pattern snippetNamePattern = Pattern.compile("Snippet([0-9]+)", Pattern.CASE_INSENSITIVE);
        sourceFile = sourceFile.normalize();
        final String filename = sourceFile.getFileName().toString();
        final String snippetName = filename.substring(0, filename.lastIndexOf('.'));
        final Class<?> snippeClass = Class.forName(SnippetsConfig.SNIPPETS_PACKAGE + "." + snippetName, false,
                SnippetExplorer.class.getClassLoader());
        int snippetNum = Integer.MIN_VALUE;
        final Matcher snippetNameMatcher = snippetNamePattern.matcher(snippetName);
        if (snippetNameMatcher.matches()) {
            try {
                snippetNum = Integer.parseInt(snippetNameMatcher.group(1), 10);
            } catch (NumberFormatException e) {
            }
        }

        // do not load snippets without number yet
        if (snippetNum < 0) {
            return null;
        }

        final String src = getSnippetSource(sourceFile);
        final String description = extractSnippetDescription(src);
        final String[] arguments = SnippetsConfig.getSnippetArguments(snippetNum);
        return new Snippet(snippetNum, snippetName, snippeClass, src, description, arguments);
    }

    /**
     * Read the content of the source file. (expect <code>UTF-8</code> encoding)
     *
     * @param sourceFile source file to load
     * @return the files content or <code>null</code> if file does not exist
     * @throws IOException if loading failed
     */
    private static String getSnippetSource(Path sourceFile) throws IOException {
        if (!Files.exists(sourceFile)) {
            return null;
        }
        final String src = new String(Files.readAllBytes(sourceFile), StandardCharsets.UTF_8);
        return src;
    }

    /**
     * Tries to extract a snippet description from the snippet source.
     * <p>
     * If description has multiple lines the delimiter is always in UNIX-style (\n).
     * </p>
     *
     * @param snippetSrc the snippet source code
     * @return the extracted snippet description. If none found returns
     *         <code>null</code> or in some cases an empty string.
     */
    private static String extractSnippetDescription(String snippetSrc) {
        if (snippetSrc == null) {
            return null;
        }
        // Usually the second block comment contains a description of the snippet
        // therefore this method returns the first block comment not containing the
        // usual copyright keywords.
        // Note: currently only real block comments are considered. A bunch of line
        // comments forming a block (like that comment you're reading right now) are
        // ignored.

        final Pattern blockCommentPattern = Pattern.compile("/\\*\\*?(.*?)\\*/", Pattern.DOTALL);
        final Matcher blockCommentMatcher = blockCommentPattern.matcher(snippetSrc);
        while (blockCommentMatcher.find()) {
            String comment = blockCommentMatcher.group(1);
            if (comment.contains("Copyright (c)") || comment.contains("https://www.eclipse.org/legal/epl-2.0/")) {
                continue;
            }
            // normalize line breaks
            comment = comment.replaceAll("\r\n?", "\n");
            // remove '*' at line start and trim lines
            comment = comment.replaceAll("[ \t]*\n[ \\t]*\\*+[ \\t]*", "\n");
            // trim start and end
            comment = comment.trim();
            return comment;
        }
        return null;
    }

    private void updateTable(String filter) {
        if (filter == null) {
            filter = "";
        }
        filter = filter.toLowerCase();
        int itemIndex = 0;
        final int itemCount = snippetTable.getItemCount();
        snippetTable.setRedraw(false);
        snippetTable.deselectAll();
        for (Snippet snippet : snippets) {
            if (filter.isEmpty()
                    || (snippet.description != null && snippet.description.toLowerCase().contains(filter))
                    || String.valueOf(snippet.snippetNum).equals(filter)) {
                final TableItem item = itemIndex < itemCount ? snippetTable.getItem(itemIndex)
                        : new TableItem(snippetTable, SWT.NONE);
                fillTableItem(item, snippet);
                itemIndex++;
            }
        }
        if (itemIndex < itemCount) {
            snippetTable.remove(itemIndex, itemCount - 1);
        }
        snippetTable.setRedraw(true);
    }

    /**
     * Initialize the table item with information from the Snippet.
     *
     * @param item table item to initialize (not <code>null</code>)
     * @param snippet source Snippet (not <code>null</code>)
     */
    private void fillTableItem(TableItem item, Snippet snippet) {
        item.setData(snippet);

        final String shortDescription;
        if (snippet.description == null) {
            shortDescription = "";
        } else {
            int index = snippet.description.indexOf('\n');
            if (index < 0) {
                index = snippet.description.length();
            }
            if (index > MAX_DESCRIPTION_LENGTH_IN_TABLE) {
                shortDescription = snippet.description.substring(0, MAX_DESCRIPTION_LENGTH_IN_TABLE) + "...";
            } else {
                shortDescription = snippet.description.substring(0, index);
            }
        }

        item.setText(new String[] { snippet.snippetName, shortDescription });
    }

    /**
     * Process UI event queue until explorer is closed or otherwise ended.
     */
    private void runEventLoop() {
        // Apart from the usual "dispatch events until closed" pattern the
        // SnippetExplorer supports the special workflow where it close itself, run one
        // or more Snippets one after another and then restarts the explorer itself
        // which is all handled in this method.
        try {
            while (true) {
                serialSnippets = null;
                while (!shell.isDisposed()) {
                    if (!display.readAndDispatch()) {
                        display.sleep();
                    }
                }

                if (serialSnippets == null || serialSnippets.isEmpty()) {
                    break;
                }

                display.dispose();
                int i = 0;
                for (Snippet snippet : serialSnippets) {
                    System.out
                            .println(String.format("(%d/%d) %s", ++i, serialSnippets.size(), snippet.snippetName));
                    runSnippetInCurrentThread(snippet);
                }
                final Display currentDisplay = Display.getCurrent();
                if (currentDisplay != null) {
                    // left over from the snippet run
                    currentDisplay.dispose();
                }
                initialize();
                final int index = runnerMapping.indexOf(null);
                if (index != -1) {
                    runnerCombo.select(index);
                }
            }
        } finally {
            stopSnippets();
        }
    }

    /** Try to stop all running Snippets. */
    private synchronized void stopSnippets() {
        for (SnippetRunner runner : runnerMapping) {
            if (runner != null) {
                runner.stopSnippets();
            }
        }
    }

    /**
     * Launch the given snippet items with the currently selected snippet runner.
     * <p>
     * The items must contain the {@link Snippet} metadata as data object.
     * </p>
     *
     * @param items the Snippets to launch
     * @see #snippetRunner
     */
    private void launchSnippet(TableItem... items) {
        final List<Snippet> validSnippets = new ArrayList<>();
        for (TableItem item : items) {
            if (item != null && item.getData() instanceof Snippet) {
                validSnippets.add((Snippet) item.getData());
            }
        }

        if (validSnippets.size() > START_MANY_SNIPPETS_WARNING_THREASHOLD) {
            final MessageBox warnBox = new MessageBox(shell, SWT.ICON_WARNING | SWT.YES | SWT.NO);
            warnBox.setText("Starting many Snippets");
            warnBox.setMessage("You have selected " + validSnippets.size() + " Snippets to start.\n"
                    + "Do you really want to start so many Snippets at once?");
            if (warnBox.open() != SWT.YES) {
                return;
            }
        }

        if (snippetRunner != null) {
            snippetRunner.launchSnippet(validSnippets.toArray(new Snippet[0]));
        } else {
            nextExplorerLocation = shell.getLocation();
            serialSnippets = validSnippets;
            shell.close();
        }
    }

    /**
     * Launches the given Snippet in the current thread by invoking the Snippets
     * <code>main</code> method.
     *
     * @param snippet the Snippet to run (not <code>null</code>)
     */
    private static void runSnippetInCurrentThread(Snippet snippet) {
        final Method method;
        final String[] arguments = snippet.arguments;
        try {
            method = snippet.snippetClass.getMethod("main", arguments.getClass());
        } catch (NoSuchMethodException ex) {
            System.err.println("Did not find main(String []) for " + snippet.snippetName);
            return;
        }
        try {
            method.invoke(null, new Object[] { arguments });
        } catch (IllegalAccessException | IllegalArgumentException e) {
            System.err.println("Failed to launch " + snippet.snippetName + ". Error: " + e);
        } catch (InvocationTargetException e) {
            System.err.println("Exception in Snippet " + snippet.snippetName + ": " + e.getTargetException());
        }
    }

    /**
     * Show a warning dialog that the Snippet may print with the default printer
     * without further warnings.
     *
     * @param shell       parent shell for the warning dialog
     * @param snippetName the Snippet's name to warn for
     * @return <code>true</code> if the user confirmed Snippet execution
     */
    private static boolean printerWarning(Shell shell, String snippetName) {
        final MessageBox warnBox = new MessageBox(shell, SWT.ICON_WARNING | SWT.OK | SWT.CANCEL);
        warnBox.setText("Printing Snippet");
        warnBox.setMessage(snippetName
                + " may print something on your default printer without further warning or confirmation.");
        return (warnBox.open() == SWT.OK);
    }

    /** Class to store metadata for a Snippet. */
    private static class Snippet {
        /** The Snippet's number. (not all Snippets may have numbers in the future) */
        private int snippetNum;
        /** Snippet's name / main class name. */
        private String snippetName;
        /** Snippet's main class. */
        private Class<?> snippetClass;
        /** Snippet's source code or <code>null</code> if not available. */
        private String source;
        /**
         * Snippet description extracted from its source code. (may be
         * <code>null</code>)
         */
        private String description;
        /**
         * Arguments used when launching the Snippets. Can be configured in
         * {@link SnippetsConfig#getSnippetArguments(int)}.
         */
        private String[] arguments;

        public Snippet(int snippetNum, String snippetName, Class<?> snippetClass, String source, String description,
                String[] arguments) {
            super();
            this.snippetNum = snippetNum;
            this.snippetName = snippetName;
            this.snippetClass = snippetClass;
            this.source = source;
            this.description = description;
            this.arguments = arguments;
        }

        @Override
        public String toString() {
            return "Snippet [snippetNum=" + snippetNum + ", snippetName=" + snippetName + ", snippetClass="
                    + snippetClass + ", source=" + source + ", description=" + description + ", arguments="
                    + Arrays.toString(arguments) + "]";
        }
    }

    /** Interface for a runner capable to launch Snippets. */
    private interface SnippetRunner {
        /**
         * Launch the given Snippets in the runner specific way.
         *
         * @param snippets Snippets to launch. Not <code>null</code>.
         */
        void launchSnippet(Snippet... snippets);

        /**
         * Stop all running Snippets launched with this runner. Some runners may not be
         * able to stop Snippets.
         */
        void stopSnippets();
    }

    /** Run Snippets in separate threads. */
    private class SnippetRunnerThread implements SnippetRunner {

        /** All currently <b>running</b> Snippets launched from this runner. */
        private final List<Thread> launchedSnippets = new ArrayList<>();

        /**
         * Launch Snippets parallel in separate threads. Call returns immediately after
         * all Snippets are started.
         */
        @Override
        public void launchSnippet(Snippet... snippets) {
            for (Snippet snippet : snippets) {
                if (snippet == null) {
                    return;
                }
                final Thread thread = new Thread(() -> {
                    try {
                        synchronized (launchedSnippets) {
                            launchedSnippets.add(Thread.currentThread());
                        }

                        // warn user before printing
                        if (SnippetsConfig.isPrintingSnippet(snippet.snippetNum)) {
                            final Display d = new Display();
                            try {
                                if (!printerWarning(new Shell(d), snippet.snippetName)) {
                                    return;
                                }
                            } finally {
                                d.dispose();
                            }
                        }

                        runSnippetInCurrentThread(snippet);

                    } finally {
                        synchronized (launchedSnippets) {
                            final Display d = Display.getCurrent();
                            if (d != null) {
                                d.dispose();
                            }
                            launchedSnippets.remove(Thread.currentThread());
                        }
                    }
                });
                thread.setDaemon(true);
                thread.setName(snippet.snippetName);
                thread.start();
            }
        }

        /**
         * Stops all running Snippets launched by this runner. If a Snippt refuses to
         * react to this stop signal it will not be force stopped until the
         * SnippetExplorer itself is closed.
         */
        @Override
        public void stopSnippets() {
            final List<Thread> runningSnippets;
            synchronized (launchedSnippets) {
                runningSnippets = new ArrayList<>(launchedSnippets);
            }
            for (Thread t : runningSnippets) {
                t.interrupt();
                final Display d = Display.findDisplay(t);
                if (d != null) {
                    d.asyncExec(() -> Arrays.stream(d.getShells()).filter(s -> !s.isDisposed())
                            .forEach(s -> s.close()));
                }
            }
            final long start = System.currentTimeMillis();
            for (Thread t : runningSnippets) {
                if (System.currentTimeMillis() - SHUTDOWN_GRACE_TIME_MS > start) {
                    break;
                }
                try {
                    t.join(200);
                } catch (InterruptedException e) {
                }
            }
            synchronized (launchedSnippets) {
                if (!launchedSnippets.isEmpty()) {
                    System.err.println("Some Snippets are still running:");
                    for (Thread t : launchedSnippets) {
                        System.err.println("    " + t.getName() + " (ThreadId: " + t.getId() + ")");
                        final Display d = Display.findDisplay(t);
                        if (d != null && !d.isDisposed()) {
                            d.syncExec(() -> d.dispose());
                        }
                    }
                }
            }
        }
    }

    /** Run Snippets in separate processes. */
    private class SnippetRunnerProcess implements SnippetRunner {
        /**
         * All Snippets launched from this runner. Listed Snippets may already
         * terminated.
         */
        private List<Process> launchedSnippets = new ArrayList<>();

        /**
         * Launch Snippets parallel as separate processes using the auto discovered JRE.
         * Call returns immediately after all Snippets are started.
         */
        @Override
        public synchronized void launchSnippet(Snippet... snippets) {
            for (Snippet snippet : snippets) {
                if (snippet == null) {
                    continue;
                }

                // warn user before printing
                if (SnippetsConfig.isPrintingSnippet(snippet.snippetNum)) {
                    if (!printerWarning(shell, snippet.snippetName)) {
                        continue;
                    }
                }

                final List<String> command = new ArrayList<>();
                command.add(javaCommand);
                final String os = System.getProperty("os.name");
                if (os != null && os.toLowerCase().contains("mac")) {
                    command.add("-XstartOnFirstThread");
                }
                final String cp = System.getProperty("java.class.path");
                if (cp != null && !cp.isEmpty()) {
                    command.add("-cp");
                    command.add(cp);
                }
                final String libPath = System.getProperty("java.library.path");
                if (libPath != null && !libPath.isEmpty()) {
                    command.add("-Djava.library.path=" + libPath);
                }
                command.add(SnippetsConfig.SNIPPETS_PACKAGE + "." + snippet.snippetName);
                command.addAll(Arrays.asList(snippet.arguments));
                try {
                    System.out.println("Exec: " + String.join(" ", command));
                    ProcessBuilder processBuilder = new ProcessBuilder(command);
                    processBuilder.redirectOutput(Redirect.INHERIT);
                    processBuilder.redirectError(Redirect.INHERIT);
                    final Process p = processBuilder.start();
                    launchedSnippets.add(p);
                } catch (IOException e) {
                    System.err.println("Failed to launch " + snippet.snippetName + ". Error: " + e);
                }
            }
        }

        /**
         * Stops all running Snippets launched by this runner. If the stop signal was
         * send but the Snippet is still running after a short grace time the runner
         * tries to stop the Snippet forcefully.
         * <p>
         * If all attempts to stop the Snippet fail then the Snippet will run even after
         * the SnippetExplorer was closed.
         * </p>
         */
        @Override
        public synchronized void stopSnippets() {
            for (Process p : launchedSnippets) {
                p.destroy();
            }

            final long start = System.currentTimeMillis();
            while (!launchedSnippets.isEmpty() && System.currentTimeMillis() - SHUTDOWN_GRACE_TIME_MS < start) {
                final Iterator<Process> it = launchedSnippets.iterator();
                while (it.hasNext()) {
                    final Process p = it.next();
                    if (!p.isAlive()) {
                        it.remove();
                    }
                }
                if (!launchedSnippets.isEmpty()) {
                    try {
                        launchedSnippets.get(0).waitFor(100, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            }

            if (!launchedSnippets.isEmpty()) {
                System.err.println(launchedSnippets.size() + " Snippets are still running.");
                for (Process p : launchedSnippets) {
                    p.destroyForcibly();
                }
                final Iterator<Process> it = launchedSnippets.iterator();
                while (it.hasNext()) {
                    final Process p = it.next();
                    if (!p.isAlive()) {
                        it.remove();
                    }
                }
            }
        }
    }

    /**
     * Update thread used to delay the list filtering due to changed filter string.
     */
    private class ListUpdater extends Thread {
        /**
         * The timestamp in milliseconds since epoch when the next update should be
         * executed.
         */
        private long nextListUpdate = 0;

        public ListUpdater() {
            setName("List Updater");
            setDaemon(true);
        }

        /**
         * Reapply the table filter in X milliseconds.
         * <p>
         * If an update is already scheduled only the latest update time will be used.
         * </p>
         *
         * @param ms sleep time before updating the main table
         */
        public synchronized void updateInMs(long ms) {
            if (ms < 0) {
                return;
            }
            final long nextUpdate = System.currentTimeMillis() + ms;
            if (nextListUpdate < nextUpdate) {
                nextListUpdate = nextUpdate;
            }
            notify();
        }

        @Override
        public void run() {
            while (!isInterrupted()) {
                final long nextUpdate;
                synchronized (this) {
                    nextUpdate = nextListUpdate;
                }
                if (nextUpdate - System.currentTimeMillis() <= 0) {
                    if (filterField != null) {
                        display.syncExec(() -> updateTable(filterField.getText()));
                    }
                    synchronized (this) {
                        if (nextUpdate == nextListUpdate) {
                            // no new update was scheduled while updating
                            nextListUpdate = 0;
                        }
                    }
                }
                synchronized (this) {
                    long sleepTime = nextListUpdate;
                    if (sleepTime != 0) {
                        sleepTime -= System.currentTimeMillis();
                        if (sleepTime <= 0) {
                            sleepTime = 1;
                        }
                    }
                    try {
                        wait(sleepTime);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            }
        }
    }
}