/** * */ package logbook.scripting; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeMap; import javax.script.Compilable; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import logbook.constants.AppConstants; import logbook.gui.logic.TableItemCreator; import logbook.internal.LoggerHolder; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.AbstractFileFilter; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.lang3.ArrayUtils; /** * スクリプトローダ * @author Nekopanda */ public class ScriptLoader { private static ScriptLoader instance = null; private static LoggerHolder LOG = new LoggerHolder("script"); public static interface MethodInvoke { public Object invoke(Object arg); } public class Script { private final File scriptFile; private long lastModified; private ScriptEngine engine; private final Class<?> type; private Object listener; public boolean exception = false; public int errorCounter = 0; public Script(File scriptFile, Class<?> type, boolean load) { this.scriptFile = scriptFile; this.lastModified = scriptFile.lastModified(); this.type = type; try { if (load) { this.reload_(); } } catch (ScriptException | IOException e) { this.listener = null; LOG.get().warn("スクリプトファイル " + scriptFile.getPath() + " を読み込み中にエラー", e); } ScriptLoader.this.allScripts.put(scriptFile.getName(), this); } public Script(File scriptFile, Class<?> type) { this(scriptFile, type, true); } public boolean isUpdated() { return (this.lastModified != this.scriptFile.lastModified()); } public void reload() { try { this.lastModified = this.scriptFile.lastModified(); if (!this.scriptFile.exists()) { this.listener = null; return; } this.reload_(); } catch (ScriptException | IOException e) { this.listener = null; LOG.get().warn("スクリプトファイル " + this.scriptFile.getPath() + " を読み込み中にエラー", e); } } private void reload_() throws IOException, ScriptException { try (BufferedReader reader = Files.newBufferedReader(this.scriptFile.toPath(), Charset.forName("UTF-8"))) { this.engine = ScriptLoader.this.manager.getEngineByName("nashorn"); if (this.engine == null) { this.engine = ScriptLoader.this.manager.getEngineByExtension("js"); if (this.engine == null) { throw new ScriptException("javascriptエンジンが見つかりません"); } } // eval //this.engine.eval(reader); ((Compilable) this.engine).compile(reader).eval(); // 実装を取得 this.listener = ((Invocable) this.engine).getInterface(this.type); if (this.listener == null) { throw new ScriptException("スクリプトが " + this.type.getName() + " インターフェースを実装していません"); } } this.errorCounter = 0; } public Object invoke(MethodInvoke invokable) { try { if (this.listener == null) { return null; } this.exception = false; return invokable.invoke(this.listener); } catch (Exception e) { this.exception = true; if (this.errorCounter++ < 20) { LOG.get().warn(this.scriptFile.getPath() + " を実行中にエラー", e); if (this.errorCounter == 20) { LOG.get().warn(this.scriptFile.getPath() + " はこれ以上エラーを記録しません"); } } } return null; } } /** * "prefix_*.js"にマッチするスクリプトの集合 * @author Nekopanda */ public class ScriptCollection { private final String prefix; private final Class<?> type; private Map<String, Script> scripts = new TreeMap<>(); public ScriptCollection(String prefix, Class<?> type) { this.prefix = prefix; this.type = type; this.loadScripts(); } public Script makeScript(File file, Class<?> type) { return new Script(file, type); } public Collection<Script> get() { return this.scripts.values(); } private void loadScripts() { Map<String, Script> oldScripts = this.scripts; this.scripts = new TreeMap<>(); for (File file : this.getScriptFiles()) { Script script = oldScripts.get(file.getPath()); if ((script == null)) { script = this.makeScript(file, this.type); } else if (script.isUpdated()) { script.reload(); } this.scripts.put(file.getPath(), script); } } private File[] getScriptFiles() { final String starts = this.prefix + "_"; if (AppConstants.SCRIPT_DIR.exists() == false) { return new File[0]; } File[] array = FileUtils.listFiles(AppConstants.SCRIPT_DIR, new AbstractFileFilter() { @Override public boolean accept(File file) { String name = file.getName(); return name.endsWith(".js") && name.startsWith(starts); } }, FileFilterUtils.trueFileFilter()).toArray(new File[0]); Arrays.sort(array, new Comparator<File>() { @Override public int compare(File arg0, File arg1) { return arg0.getPath().compareTo(arg1.getPath()); } }); return array; } public boolean isUpdated() { File[] files = this.getScriptFiles(); if (this.scripts.size() != files.length) { return true; } for (File file : files) { Script script = this.scripts.get(file.getPath()); if (script == null) { return true; } if (script.isUpdated()) { return true; } } return false; } public void reload() { this.scripts.clear(); this.loadScripts(); } public void update() { this.loadScripts(); } /** * 各スクリプトで実行 * @param invokable 実行するメソッド */ public void invoke(MethodInvoke invokable) { for (Script script : this.get()) { script.invoke(invokable); } } } /** * 各種テーブルのカラム拡張用スクリプト * @author Nekopanda */ public class TableScript extends Script { private final MethodInvoke headerMethod = new MethodInvoke() { @Override public Object invoke(Object arg) { return ((TableScriptListener) arg).header(); } }; private final String[] header; private final Comparable[] exceptionBody; public TableScript(File scriptFile, Class<?> type) { super(scriptFile, type); this.header = (String[]) this.invoke(this.headerMethod); if (this.header != null) { this.exceptionBody = new Comparable[this.header.length]; for (int i = 0; i < this.exceptionBody.length; ++i) { this.exceptionBody[i] = "例外が発生しました"; } } else { this.exceptionBody = null; } } public String[] header() { return this.header; } public Comparable[] body(MethodInvoke invokable) { if (this.header == null) { return null; } Comparable[] raw = (Comparable[]) this.invoke(invokable); if (this.exception) { return this.exceptionBody; } if ((raw != null) && (raw.length == this.header.length)) { return raw; } // 長さを合わせる return this.resize(raw); } private Comparable[] resize(Comparable[] raw) { Comparable[] ret = new Comparable[this.header.length]; if (raw == null) { return ret; } for (int i = 0; i < ret.length; ++i) { if (i < raw.length) { ret[i] = raw[i]; } } return ret; } } /** * テーブルカラム拡張用スクリプトの集合 * テーブルヘッダは起動中変更できないので、reloadでファイルが増減しないようになっています * @author Nekopanda */ public class TableScriptCollection extends ScriptCollection { public TableScriptCollection(String prefix, Class<?> type) { super(prefix, type); } @Override public Script makeScript(File file, Class<?> type) { return new TableScript(file, type); } @Override public void reload() { // テーブルヘッダは起動中変更できないので、ファイルは増減させない for (Script script : this.get()) { script.reload(); } } @Override public void update() { // テーブルヘッダは起動中変更できないので、ファイルは増減させない for (Script script : this.get()) { if (script.isUpdated()) { script.reload(); } } } @Override public boolean isUpdated() { // テーブルヘッダは起動中変更できないので、ファイルは増減させない for (Script script : this.get()) { if (script.isUpdated()) { return true; } } return false; } public String[] header() { String[] result = null; for (Script script : this.get()) { result = ArrayUtils.addAll(result, ((TableScript) script).header()); } return result; } public Comparable[] body(MethodInvoke invokable) { Comparable[] result = null; for (Script script : this.get()) { result = ArrayUtils.addAll(result, ((TableScript) script).body(invokable)); } return result; } } private final ScriptEngineManager manager = new ScriptEngineManager(); private final Map<String, Script> allScripts = new HashMap<>(); private final Map<String, ScriptCollection> scriptCollections = new TreeMap<>(); private final Map<String, Script> scripts = new TreeMap<>(); static { instance = new ScriptLoader(); } private ScriptLoader() { final File sourceDir = new File("./templates/script"); final Set<String> ignoreList = new HashSet<>(); File ignoreFile = new File(AppConstants.SCRIPT_DIR + "/ignore_update.txt"); if (ignoreFile.exists()) { try { for (String filename : FileUtils.readLines(ignoreFile)) { ignoreList.add(filename); } } catch (IOException e) { LOG.get().warn("除外リストファイル読み込み中にエラー", e); } } try { FileUtils.copyDirectory(new File("./templates/script"), AppConstants.SCRIPT_DIR, new FileFilter() { @Override public boolean accept(File src) { if (ignoreList.contains(src.getName())) { LOG.get().info("除外されているためアップデートされません: " + src.getAbsolutePath()); return false; } File dstFile = new File(AppConstants.SCRIPT_DIR.getAbsolutePath() + src.getAbsolutePath().substring(sourceDir.getAbsolutePath().length())); // 新規ファイルまたは更新されていたらコピー return ((dstFile.exists() == false) || (dstFile.lastModified() < src.lastModified())); } }); // 除外リストファイルを作っておく if (ignoreFile.exists() == false) { ignoreFile.createNewFile(); } } catch (IOException e) { LOG.get().warn("スクリプトをテンプレートからコピー中にエラー", e); } } /** * prefixにマッチするテーブルカラム拡張用スクリプト集合を取得 * @param prefix * @param type * @return */ public static TableScriptCollection getTableScript(String prefix, Class<?> type) { return instance.getTableScript_(prefix, type); } /** * prefixにマッチするスクリプト集合を取得 * @param prefix * @param type * @return */ public static ScriptCollection getScriptCollection(String prefix, Class<?> type) { return instance.getScriptCollection_(prefix, type); } /** * テープル行を作るスクリプトを取得 * @param prefix * @return */ public static Script getTableStyleScript(String prefix) { return instance.getTableStyleScript_(prefix); } private synchronized TableScriptCollection getTableScript_(String prefix, Class<?> type) { ScriptCollection script = this.scriptCollections.get(prefix); if (script == null) { script = new TableScriptCollection(prefix, type); this.scriptCollections.put(prefix, script); } else if (script.isUpdated()) { script.update(); } return (TableScriptCollection) script; } private synchronized ScriptCollection getScriptCollection_(String prefix, Class<?> type) { ScriptCollection script = this.scriptCollections.get(prefix); if (script == null) { script = new ScriptCollection(prefix, type); this.scriptCollections.put(prefix, script); } else if (script.isUpdated()) { script.update(); } return script; } private File getTableStyleScriptFile(String prefix) { return new File(AppConstants.SCRIPT_DIR + "/" + prefix + AppConstants.TABLE_STYLE_SUFFIX + ".js"); } private synchronized Script getTableStyleScript_(String prefix) { Script script = this.scripts.get(prefix); if (script == null) { File scriptFile = this.getTableStyleScriptFile(prefix); script = new Script(scriptFile, TableItemCreator.class, scriptFile.exists()); this.scripts.put(prefix, script); } else if (script.isUpdated()) { script.reload(); } return script; } }