package org.openpnp;
import java.awt.Desktop;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.FileReader;
import java.nio.file.FileSystems;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.swing.AbstractAction;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.openpnp.gui.MainFrame;
import org.openpnp.model.Configuration;
import org.openpnp.util.UiUtils;
import org.pmw.tinylog.Logger;
import com.google.common.io.Files;
public class Scripting {
JMenu menu;
final ScriptEngineManager manager = new ScriptEngineManager();
final String[] extensions;
File scriptsDirectory;
File eventsDirectory;
WatchService watcher;
public Scripting() {
// Collect all the script filename extensions we know how to handle from the list of
// available scripting engines.
List<ScriptEngineFactory> factories = manager.getEngineFactories();
Set<String> extensions = new HashSet<>();
for (ScriptEngineFactory factory : factories) {
for (String ext : factory.getExtensions()) {
extensions.add(ext.toLowerCase());
}
}
this.extensions = extensions.toArray(new String[] {});
this.scriptsDirectory =
new File(Configuration.get().getConfigurationDirectory(), "scripts");
// Create the scripts directory if it doesn't exist and copy the example scripts
// over.
if (!getScriptsDirectory().exists()) {
getScriptsDirectory().mkdirs();
// TODO: It would be better if we just copied all the files from the Examples
// directory in the jar, but this is relatively difficult to do.
// There is some information on how to do it in:
// http://stackoverflow.com/questions/1386809/copy-directory-from-a-jar-file
File examplesDir = new File(getScriptsDirectory(), "Examples");
examplesDir.mkdirs();
String[] exampleScripts =
new String[] {"Call_Java.js", "Hello_World.js", "Print_Scripting_Info.js",
"Reset_Strip_Feeders.js", "Move_Machine.js", "Utility.js"};
for (String name : exampleScripts) {
try {
FileUtils.copyURLToFile(
ClassLoader.getSystemResource("scripts/Examples/" + name),
new File(examplesDir, name));
}
catch (Exception e) {
e.printStackTrace();
}
}
}
this.eventsDirectory = new File(scriptsDirectory, "Events");
if (!eventsDirectory.exists()) {
eventsDirectory.mkdirs();
}
// Add a file watcher so that we can be notified if any scripts change
try {
watcher = FileSystems.getDefault().newWatchService();
watchDirectory(getScriptsDirectory());
new Thread(() -> {
for (;;) {
try {
// wait for an event
WatchKey key = watcher.take();
key.pollEvents();
key.reset();
// rescan
synchronizeMenu(menu, getScriptsDirectory());
}
catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
catch (Exception e) {
e.printStackTrace();
}
}
public void setMenu(JMenu menu) {
this.menu = menu;
// Add a separator and the Refresh Scripts and Open Scripts Directory items
menu.addSeparator();
menu.add(new AbstractAction("Refresh Scripts") {
@Override
public void actionPerformed(ActionEvent e) {
synchronizeMenu(menu, getScriptsDirectory());
}
});
menu.add(new AbstractAction("Open Scripts Directory") {
@Override
public void actionPerformed(ActionEvent e) {
UiUtils.messageBoxOnException(() -> {
if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().open(getScriptsDirectory());
}
});
}
});
// Synchronize the menu
synchronizeMenu(menu, getScriptsDirectory());
}
public File getScriptsDirectory() {
return scriptsDirectory;
}
private void watchDirectory(File directory) {
try {
directory.toPath().register(watcher, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
}
catch (Exception e) {
e.printStackTrace();
}
}
private synchronized void synchronizeMenu(JMenu menu, File directory) {
if (menu == null) {
return;
}
// Remove any menu items that don't have a matching entry in the directory
Set<String> filenames = new HashSet<>(Arrays.asList(directory.list()));
for (JMenuItem item : getScriptMenuItems(menu)) {
if (!filenames.contains(item.getText())) {
menu.remove(item);
}
}
// Add any scripts not already in the menu
Set<String> itemNames = getScriptMenuItems(menu).stream().map(JMenuItem::getText)
.collect(Collectors.toSet());
for (File script : FileUtils.listFiles(directory, extensions, false)) {
if (!script.isFile()) {
continue;
}
if (itemNames.contains(script.getName())) {
continue;
}
JMenuItem item = new JMenuItem(script.getName());
item.addActionListener((e) -> {
UiUtils.messageBoxOnException(() -> execute(script));
});
addSorted(menu, item);
}
// And add any directories not already in the menu
itemNames = getScriptMenuItems(menu).stream().map(JMenuItem::getText)
.collect(Collectors.toSet());
for (File d : directory.listFiles(File::isDirectory)) {
if (d.equals(eventsDirectory)) {
continue;
}
if (!itemNames.contains(d.getName())) {
JMenu m = new JMenu(d.getName());
addSorted(menu, m);
watchDirectory(d);
}
}
// Synchronize all of the sub-menus with their directories
for (JMenuItem item : getScriptMenuItems(menu)) {
if (item instanceof JMenu) {
synchronizeMenu((JMenu) item, new File(directory, item.getText()));
}
}
}
private void addSorted(JMenu menu, JMenuItem item) {
if (menu.getItemCount() == 0) {
menu.add(item);
return;
}
for (int i = 0; i < menu.getItemCount(); i++) {
JMenuItem existingItem = menu.getItem(i);
if (existingItem == null || item.getText().toLowerCase()
.compareTo(existingItem.getText().toLowerCase()) <= 0) {
menu.insert(item, i);
return;
}
}
menu.add(item);
}
private List<JMenuItem> getScriptMenuItems(JMenu menu) {
List<JMenuItem> items = new ArrayList<>();
for (int i = 0; i < menu.getItemCount(); i++) {
// Once we hit the separator we stop
if (menu.getItem(i) == null) {
break;
}
items.add(menu.getItem(i));
}
return items;
}
private void execute(File script) throws Exception {
execute(script, null);
}
private void execute(File script, Map<String, Object> additionalGlobals) throws Exception {
ScriptEngine engine =
manager.getEngineByExtension(Files.getFileExtension(script.getName()));
engine.put("config", Configuration.get());
engine.put("machine", Configuration.get().getMachine());
engine.put("gui", MainFrame.get());
engine.put("scripting", this);
if (additionalGlobals != null) {
for (String name : additionalGlobals.keySet()) {
engine.put(name, additionalGlobals.get(name));
}
}
try (FileReader reader = new FileReader(script)) {
engine.eval(reader);
}
}
public void on(String event, Map<String, Object> globals) throws Exception {
Logger.trace("Scripting.on " + event);
for (File script : FileUtils.listFiles(eventsDirectory, extensions, false)) {
if (!script.isFile()) {
continue;
}
if (FilenameUtils.getBaseName(script.getName()).equals(event)) {
Logger.trace("Scripting.on found " + script.getName());
execute(script, globals);
}
}
}
}