001package net.filebot.postprocess; 002 003import static java.util.stream.Collectors.*; 004import static net.filebot.GroovyEngine.*; 005 006import java.io.File; 007import java.util.AbstractList; 008import java.util.List; 009import java.util.Map; 010import java.util.Map.Entry; 011import java.util.stream.IntStream; 012 013import javax.script.CompiledScript; 014import javax.script.ScriptContext; 015import javax.script.ScriptException; 016import javax.script.SimpleScriptContext; 017 018import groovy.lang.Closure; 019 020import net.filebot.RenameAction; 021import net.filebot.format.AssociativeScriptObject; 022import net.filebot.format.MediaBindingBean; 023import net.filebot.similarity.Match; 024import net.filebot.util.FunctionList; 025 026public class Script implements Apply { 027 028 public String id; 029 public String name; 030 public String code; 031 032 // compile on demand 033 private transient CompiledScript script; 034 035 // built-in example script 036 public Script(String name, String code) { 037 this(name, name, code); 038 } 039 040 // user-defined custom script 041 public Script(String id, String name, String code) { 042 this.id = id; 043 this.name = name; 044 this.code = code; 045 } 046 047 public String getIdentifier() { 048 return id; 049 } 050 051 public String getName() { 052 return name; 053 } 054 055 public String getCode() { 056 return code; 057 } 058 059 public synchronized CompiledScript compile() throws ScriptException { 060 // compile on demand 061 if (script == null) { 062 script = ScriptEngine.getScriptEngine().compile(resolveScript(code)); 063 } 064 return script; 065 } 066 067 public ScriptContext createScriptContext(Map<File, Match<File, ?>> map, RenameAction action, Feedback log) { 068 Model model = new Model(map); 069 070 ScriptContext context = new SimpleScriptContext(); 071 context.setAttribute("args", model.getTarget(), ScriptContext.ENGINE_SCOPE); 072 context.setAttribute("model", model, ScriptContext.ENGINE_SCOPE); 073 context.setAttribute("action", action, ScriptContext.ENGINE_SCOPE); 074 context.setAttribute("log", log, ScriptContext.ENGINE_SCOPE); 075 076 context.setWriter(FeedbackWriter.newPrintWriter(line -> log.info(line, name))); 077 context.setErrorWriter(FeedbackWriter.newPrintWriter(line -> log.warning(line, name))); 078 079 return context; 080 } 081 082 @Override 083 public void apply(Map<File, Match<File, ?>> map, RenameAction action, Feedback log) { 084 log.info("Run Script", name); 085 086 try { 087 ScriptContext context = createScriptContext(map, action, log); 088 Object result = compile().eval(context); 089 090 // apply closure to each match 091 if (result instanceof Closure) { 092 ApplyClosure each = new ApplyClosure((Closure) result); 093 each.apply(map, action, log); 094 return; 095 } 096 097 log.trace(result, name); 098 } catch (ScriptException e) { 099 log.warning(sanitizeErrorMessage(e), name); 100 } 101 } 102 103 @Override 104 public String toString() { 105 return "SCRIPT"; 106 } 107 108 public static class Model extends AbstractList<AssociativeScriptObject> { 109 110 private final Entry<File, Match<File, ?>>[] map; 111 private final AssociativeScriptObject[] model; 112 113 public Model(Map<File, Match<File, ?>> map) { 114 this.map = map.entrySet().toArray(new Entry[0]); 115 this.model = new AssociativeScriptObject[this.map.length]; 116 } 117 118 public File getTarget(int i) { 119 return map[i].getKey(); 120 } 121 122 public File getSource(int i) { 123 return map[i].getValue().getValue(); 124 } 125 126 public Object getObject(int i) { 127 return map[i].getValue().getCandidate(); 128 } 129 130 @Override 131 public AssociativeScriptObject get(int i) { 132 if (model[i] == null) { 133 model[i] = new MediaBindingBean(getObject(i), getSource(i)).getSelf(); 134 } 135 return model[i]; 136 } 137 138 @Override 139 public int size() { 140 return model.length; 141 } 142 143 public List<File> getSource() { 144 return new FunctionList<File>(this::getSource, this::size); 145 } 146 147 public List<File> getTarget() { 148 return new FunctionList<File>(this::getTarget, this::size); 149 } 150 151 public List<Object> getObject() { 152 return new FunctionList<Object>(this::getObject, this::size); 153 } 154 155 public List<Object> each(Closure<?> closure) { 156 return IntStream.range(0, size()).mapToObj(i -> { 157 // make standard format bindings available in the closure 158 closure.setDelegate(get(i)); 159 160 switch (closure.getMaximumNumberOfParameters()) { 161 case 0: 162 return closure.call(); 163 case 1: 164 return closure.call(get(i)); 165 case 2: 166 return closure.call(getSource(i), getTarget(i)); 167 case 3: 168 return closure.call(getSource(i), getTarget(i), getObject(i)); 169 default: 170 throw new IllegalArgumentException("Unexpected number of parameters in closure: " + closure.getMaximumNumberOfParameters()); 171 } 172 }).collect(toList()); 173 } 174 175 } 176 177}