| /******************************************************************************* |
| * 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 (FontData element : existingData) { |
| element.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; |
| } |
| } |
| } |
| } |
| } |
| } |