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