blob: f977a74674e93b86b701c80825c9ef3c8b701f03 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}
}
}
}
}