Java tutorial
/******************************************************************************* * 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; } } } } } }