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}