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}