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