001package net.filebot.format;
002
003import static java.util.stream.Collectors.*;
004import static net.filebot.util.FileUtilities.*;
005
006import java.io.File;
007import java.net.URI;
008import java.util.Collection;
009import java.util.List;
010import java.util.Map;
011import java.util.Objects;
012import java.util.stream.Stream;
013
014import javax.script.SimpleBindings;
015
016import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
017
018import com.sun.jna.Platform;
019
020import groovy.json.JsonGenerator;
021import groovy.lang.Closure;
022import groovy.lang.Script;
023
024import net.filebot.ApplicationFolder;
025import net.filebot.InvalidInputException;
026
027/**
028 * Global functions available in the {@link ExpressionFormat}
029 */
030public class ExpressionFormatFunctions {
031
032        /*
033         * General helpers and utilities
034         */
035
036        private static Object call(Script context, Object object) {
037                // resolve nested closures
038                if (object instanceof Closure) {
039                        try {
040                                return call(context, ((Closure) object).call());
041                        } catch (Exception e) {
042                                return null;
043                        }
044                }
045
046                // resolve empty values
047                if (isEmptyValue(context, object)) {
048                        return null;
049                }
050
051                return object;
052        }
053
054        public static boolean isEmptyValue(Script context, Object object) {
055                // treat null as null
056                if (object == null) {
057                        return true;
058                }
059
060                // custom binding class is never undefined
061                if (object instanceof StringBinding) {
062                        return false;
063                }
064
065                // treat empty string as null
066                if (object instanceof CharSequence) {
067                        CharSequence s = (CharSequence) object;
068                        return s.length() == 0;
069                }
070
071                // treat empty list as null
072                if (object instanceof Collection) {
073                        Collection i = (Collection) object;
074                        return i.isEmpty();
075                }
076
077                return false;
078        }
079
080        public static boolean none(Script context, Object c1, Object... cN) {
081                return stream(context, c1, null, cN).noneMatch(DefaultTypeTransformation::castToBoolean);
082        }
083
084        public static Object any(Script context, Object c1, Object c2, Object... cN) {
085                return stream(context, c1, c2, cN).findFirst().orElse(null);
086        }
087
088        public static List<Object> allOf(Script context, Object c1, Object c2, Object... cN) {
089                return stream(context, c1, c2, cN).collect(toList());
090        }
091
092        public static String concat(Script context, Object c1, Object c2, Object... cN) {
093                return stream(context, c1, c2, cN).map(Objects::toString).collect(joining());
094        }
095
096        public static String component(Script context, Object c, Object... cN) {
097                return replacePathSeparators(concat(context, c, null, cN), "");
098        }
099
100        public static List<Object> milliseconds(Script context, Object c1, Object... cN) {
101                long t = System.currentTimeMillis();
102                List<Object> values = allOf(context, c1, null, cN);
103                values.add(System.currentTimeMillis() - t);
104                return values;
105        }
106
107        private static Stream<Object> stream(Script context, Object c1, Object c2, Object... cN) {
108                return Stream.concat(Stream.of(c1, c2), Stream.of(cN)).map(c -> call(context, c)).filter(Objects::nonNull);
109        }
110
111        /*
112         * Unix Shell / Windows PowerShell utilities
113         */
114
115        public static String quote(Script context, Object c1, Object... cN) {
116                return Platform.isWindows() ? quotePowerShell(context, c1, cN) : quoteBash(context, c1, cN);
117        }
118
119        public static String quoteBash(Script context, Object c1, Object... cN) {
120                return stream(context, c1, null, cN).map(v -> argument(context, v)).map(s -> "'" + s.replace("'", "'\"'\"'") + "'").collect(joining(" "));
121        }
122
123        public static String quotePowerShell(Script context, Object c1, Object... cN) {
124                return stream(context, c1, null, cN).map(v -> argument(context, v)).map(s -> "@'\n" + s + "\n'@").collect(joining(" "));
125        }
126
127        private static String argument(Script context, Object object) {
128                // use JSON String representation for maps and lists
129                if (object instanceof Map || object instanceof Iterable) {
130                        return toJson(context, object);
131                }
132                // use default String representation for anything else
133                return Objects.toString(object, "");
134        }
135
136        public static String toJson(Script context, Object object) {
137                JsonGenerator.Options json = new JsonGenerator.Options();
138                json.disableUnicodeEscaping();
139                json.excludeNulls();
140                json.addConverter(new JsonGenerator.Converter() {
141
142                        @Override
143                        public boolean handles(Class<?> type) {
144                                return Stream.of(Map.class, Iterable.class, CharSequence.class, Number.class, Boolean.class).noneMatch(c -> c.isAssignableFrom(type));
145                        }
146
147                        @Override
148                        public Object convert(Object value, String key) {
149                                return argument(context, call(context, value));
150                        }
151                });
152                return json.build().toJson(object);
153        }
154
155        /*
156         * I/O utilities
157         */
158
159        public static Map<Object, Object> csv(Script context, Object path) throws Exception {
160                return getDataResource(context, path).csv();
161        }
162
163        public static List<String> lines(Script context, Object path) throws Exception {
164                return getDataResource(context, path).lines();
165        }
166
167        public static Object xml(Script context, Object path) throws Exception {
168                return getDataResource(context, path).xml();
169        }
170
171        public static Object json(Script context, Object path) throws Exception {
172                return getDataResource(context, path).json();
173        }
174
175        public static Object html(Script context, Object path) throws Exception {
176                return getDataResource(context, path).html();
177        }
178
179        public static String text(Script context, Object path) throws Exception {
180                return getDataResource(context, path).text();
181        }
182
183        public static Object include(Script context, Object path) throws Exception {
184                DataResource resource = getDataResource(context, path);
185
186                SimpleBindings bindings = new SimpleBindings();
187                bindings.put("__file__", resource.getResource());
188
189                return ExpressionEngine.getExpressionEngine().evaluate(resource.text(), bindings, context);
190        }
191
192        private static DataResource getDataResource(Script context, Object path) throws Exception {
193                String resource = path == null ? null : path.toString();
194
195                if (resource == null || resource.isEmpty()) {
196                        throw new InvalidInputException("Please specify a local file path or remote HTTP URL");
197                }
198
199                // local resource or relative resource
200                File file = new File(resource);
201
202                // absolute local path
203                if (file.isAbsolute()) {
204                        // input file path must not be the file system root
205                        if (file.getParent() == null) {
206                                throw new InvalidInputException("Bad file path: " + file);
207                        }
208                        return DataResource.local(file);
209                }
210
211                // remote resource
212                if (resource.startsWith("https://") || resource.startsWith("http://")) {
213                        return DataResource.remote(new URI(resource));
214                }
215
216                // resolve relative paths against caller script (if possible)
217                Object source = null;
218                try {
219                        source = context.getProperty("__file__");
220                } catch (Exception e) {
221                        // __file__ is undefined for entry point code
222                }
223
224                // resolve relative paths against $HOME by default
225                if (source instanceof File) {
226                        File f = new File(((File) source).getParentFile(), resource);
227                        return DataResource.local(f);
228                } else if (source instanceof URI) {
229                        URI r = ((URI) source).resolve("..").resolve(resource);
230                        return DataResource.remote(r);
231                }
232
233                // resolve relative paths against $HOME by default
234                File f = ApplicationFolder.UserHome.resolve(resource);
235                return DataResource.local(f);
236        }
237
238}