001package net.filebot.cli;
002
003import static java.util.Collections.*;
004import static java.util.stream.Collectors.*;
005import static net.filebot.Execute.*;
006import static net.filebot.Logging.*;
007
008import java.io.File;
009import java.util.ArrayList;
010import java.util.EnumSet;
011import java.util.List;
012import java.util.Objects;
013import java.util.Set;
014import java.util.stream.IntStream;
015import java.util.stream.Stream;
016
017import javax.script.Bindings;
018import javax.script.ScriptException;
019
020import net.filebot.ExecuteException;
021import net.filebot.ExitCode;
022import net.filebot.format.ExpressionBindings;
023import net.filebot.format.ExpressionFormat;
024import net.filebot.format.MediaBindingBean;
025
026public class ExecCommand {
027
028        private List<ExpressionFormat> template;
029        private boolean parallel;
030        private boolean distinct;
031
032        private File directory;
033
034        public ExecCommand(List<ExpressionFormat> template, boolean parallel, boolean distinct, File directory) {
035                this.template = template;
036                this.parallel = parallel;
037                this.distinct = distinct;
038                this.directory = directory;
039        }
040
041        public IntStream execute(Stream<MediaBindingBean> group) {
042                if (parallel) {
043                        return executeParallel(group.map(ExpressionBindings::new));
044                } else {
045                        return executeSequence(group.map(ExpressionBindings::new));
046                }
047        }
048
049        private IntStream executeSequence(Stream<Bindings> group) {
050                Stream<List<String>> sequence = group.map(v -> {
051                        return template.stream().map(t -> getArgumentValue(t, v)).filter(Objects::nonNull).collect(toList());
052                });
053
054                // deduplicate commands
055                if (distinct) {
056                        return sequence.distinct().mapToInt(this::execute);
057                } else {
058                        return sequence.mapToInt(this::execute);
059                }
060        }
061
062        private IntStream executeParallel(Stream<Bindings> group) {
063                // collect all bindings and combine them into a single command
064                List<Bindings> bindings = group.collect(toList());
065
066                if (bindings.isEmpty()) {
067                        return IntStream.empty();
068                }
069
070                // collect single command
071                List<String> command = template.stream().flatMap(t -> {
072                        Stream<String> sequence = bindings.stream().map(v -> getArgumentValue(t, v)).filter(Objects::nonNull);
073                        // deduplicate constant argument value (e.g. echo echo -n -n) and account for short-hand {} empty expressions (equivalent to {f} by convention)
074                        if (t.isConstant() && !t.isEmpty()) {
075                                return sequence.limit(1);
076                        }
077                        // deduplicate dynamic argument values (e.g. echo 23 23 23)
078                        if (distinct) {
079                                return sequence.distinct();
080                        }
081                        return sequence;
082                }).collect(toList());
083
084                // execute single command
085                return Stream.of(command).mapToInt(this::execute);
086        }
087
088        private int execute(List<String> command) {
089                if (command.isEmpty()) {
090                        return ExitCode.SUCCESS;
091                }
092
093                try {
094                        // chain internal command
095                        ArgumentProcessor filebot = ArgumentProcessor.parseCommand(command);
096                        if (filebot != null) {
097                                debug.finest(format("Run %s", command));
098                                return filebot.run();
099                        }
100
101                        // chain system command
102                        system(command.get(0), command.subList(1, command.size()), directory, null);
103                        return ExitCode.SUCCESS;
104                } catch (ExecuteException e) {
105                        log.warning(e::getMessage);
106                        return e.getExitCode();
107                } catch (Exception e) {
108                        log.warning(e::getMessage);
109                        return ExitCode.ERROR;
110                }
111        }
112
113        private String getArgumentValue(ExpressionFormat template, Bindings variables) {
114                // make -find -exec echo {} work as if it was {f} to mimic Unix find -exec
115                if (template.isEmpty() && "{}".equals(template.getExpression())) {
116                        return variables.get("f").toString();
117                }
118
119                try {
120                        return template.format(variables);
121                } catch (Exception e) {
122                        debug.warning(cause(template.getExpression(), e));
123                }
124                return null;
125        }
126
127        public static ExecCommand parse(List<String> args, File directory) {
128                if (args == null || args.isEmpty()) {
129                        throw new IllegalArgumentException("args is empty");
130                }
131
132                // execute one command per file or one command with many file arguments
133                Set<Flag> mark = Flag.parse(args.get(args.size() - 1));
134
135                if (!mark.isEmpty()) {
136                        args = args.subList(0, args.size() - 1);
137                }
138
139                List<ExpressionFormat> template = new ArrayList<ExpressionFormat>();
140                for (String argument : args) {
141                        try {
142                                template.add(new ExpressionFormat(argument));
143                        } catch (ScriptException e) {
144                                throw new IllegalArgumentException("Invalid expression: " + argument + ": " + e.getMessage(), e);
145                        }
146                }
147
148                return new ExecCommand(template, mark.contains(Flag.Parallel), !mark.contains(Flag.Duplicate), directory);
149        }
150
151        public static enum Flag {
152
153                Parallel, Duplicate;
154
155                public static Set<Flag> parse(String mark) {
156                        switch (mark) {
157                                case "+":
158                                        return EnumSet.of(Parallel);
159                                case "*":
160                                        return EnumSet.of(Duplicate);
161                                case "+*":
162                                        return EnumSet.of(Parallel, Duplicate);
163                                default:
164                                        return emptySet();
165                        }
166                }
167        }
168
169}