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 long count(Script context, Object c1, Object... cN) {
101                return list(context, c1, cN).stream().filter(v -> !isEmptyValue(context, v)).count();
102        }
103
104        public static String concat(Script context, Object c1, Object c2, Object... cN) {
105                return stream(context, c1, c2, cN).map(Objects::toString).collect(joining());
106        }
107
108        public static String component(Script context, Object c, Object... cN) {
109                return replacePathSeparators(concat(context, c, null, cN), "");
110        }
111
112        public static double abs(Script context, Number number) {
113                return Math.abs(number.doubleValue());
114        }
115
116        public static List<Object> milliseconds(Script context, Object c1, Object... cN) {
117                long t = System.currentTimeMillis();
118                List<Object> values = allOf(context, c1, null, cN);
119                values.add(System.currentTimeMillis() - t);
120                return values;
121        }
122
123        private static Stream<Object> stream(Script context, Object c1, Object c2, Object... cN) {
124                return Stream.concat(Stream.of(c1, c2), Stream.of(cN)).map(c -> call(context, c)).filter(Objects::nonNull);
125        }
126
127        /*
128         * Unix Shell / Windows PowerShell utilities
129         */
130
131        public static String quote(Script context, Object c1, Object... cN) {
132                return Platform.isWindows() ? quotePowerShell(context, c1, cN) : quoteBash(context, c1, cN);
133        }
134
135        public static String quoteBash(Script context, Object c1, Object... cN) {
136                return stream(context, c1, null, cN).map(v -> argument(context, v)).map(s -> "'" + s.replace("'", "'\"'\"'") + "'").collect(joining(" "));
137        }
138
139        public static String quotePowerShell(Script context, Object c1, Object... cN) {
140                return stream(context, c1, null, cN).map(v -> argument(context, v)).map(s -> "@'\n" + s + "\n'@").collect(joining(" "));
141        }
142
143        private static String argument(Script context, Object object) {
144                // use JSON String representation for maps and lists
145                if (object instanceof Map || object instanceof Iterable) {
146                        return toJson(context, object);
147                }
148                // use default String representation for anything else
149                return Objects.toString(object, "");
150        }
151
152        public static String toJson(Script context, Object object) {
153                JsonGenerator.Options json = new JsonGenerator.Options();
154                json.disableUnicodeEscaping();
155                json.excludeNulls();
156                json.addConverter(new JsonGenerator.Converter() {
157
158                        @Override
159                        public boolean handles(Class<?> type) {
160                                return Stream.of(Map.class, Iterable.class, CharSequence.class, Number.class, Boolean.class).noneMatch(c -> c.isAssignableFrom(type));
161                        }
162
163                        @Override
164                        public Object convert(Object value, String key) {
165                                return argument(context, call(context, value));
166                        }
167                });
168                return json.build().toJson(object);
169        }
170
171        /*
172         * I/O utilities
173         */
174
175        public static Map<Object, Object> csv(Script context, Object path) throws Exception {
176                return getDataResource(context, path).csv();
177        }
178
179        public static List<String> lines(Script context, Object path) throws Exception {
180                return getDataResource(context, path).lines();
181        }
182
183        public static Object xml(Script context, Object path) throws Exception {
184                return getDataResource(context, path).xml();
185        }
186
187        public static Object json(Script context, Object path) throws Exception {
188                return getDataResource(context, path).json();
189        }
190
191        public static Object json(Script context, Map<String, String> header, String url, Object postData) throws Exception {
192                // HTTP POST JSON REQUEST
193                return new DataResource.Post(new URI(url), toJson(context, postData), "application/json", header).json();
194        }
195
196        public static Object html(Script context, Object path) throws Exception {
197                return getDataResource(context, path).html();
198        }
199
200        public static String text(Script context, Object path) throws Exception {
201                return getDataResource(context, path).text();
202        }
203
204        public static Object include(Script context, Object path) throws Exception {
205                DataResource resource = getDataResource(context, path);
206
207                SimpleBindings bindings = new SimpleBindings();
208                bindings.put("__file__", resource.getResource());
209
210                return ExpressionEngine.getExpressionEngine().evaluate(resource.text(), bindings, context);
211        }
212
213        private static DataResource getDataResource(Script context, Object path) throws Exception {
214                String resource = path == null ? null : path.toString();
215
216                if (resource == null || resource.isEmpty()) {
217                        throw new InvalidInputException("Please specify a local file path or remote HTTP URL");
218                }
219
220                // local resource or relative resource
221                File file = new File(resource);
222
223                // absolute local path
224                if (file.isAbsolute()) {
225                        // input file path must not be the file system root
226                        if (file.getParent() == null) {
227                                throw new InvalidInputException("Bad file path: " + file);
228                        }
229                        return DataResource.local(file);
230                }
231
232                // remote resource
233                if (resource.startsWith("https://") || resource.startsWith("http://")) {
234                        return DataResource.remote(new URI(resource));
235                }
236
237                // resolve relative paths against caller script (if possible)
238                Object source = null;
239                try {
240                        source = context.getProperty("__file__");
241                } catch (Exception e) {
242                        // __file__ is undefined for entry point code
243                }
244
245                // resolve relative paths against $HOME by default
246                if (source instanceof File) {
247                        File f = new File(((File) source).getParentFile(), resource);
248                        return DataResource.local(f);
249                } else if (source instanceof URI) {
250                        URI r = ((URI) source).resolve("..").resolve(resource);
251                        return DataResource.remote(r);
252                }
253
254                // resolve relative paths against $HOME by default
255                File f = ApplicationFolder.UserHome.resolve(resource);
256                return DataResource.local(f);
257        }
258
259        public static String OpenAI(Script context, Map<String, Object> parameters) throws Exception {
260                if (!Stream.of("system", "user", "url", "model", "key").allMatch(parameters::containsKey)) {
261                        throw new InvalidInputException("Usage: OpenAI(system: '...', user: '...', url: '...', model: '...', key: '...')");
262                }
263
264                Map<String, Object> request = new LinkedHashMap<String, Object>(2);
265                request.put("model", parameters.get("model"));
266
267                request.put("messages", Stream.of("system", "user").map(role -> {
268                        Map<String, Object> m = new LinkedHashMap<String, Object>(2);
269                        m.put("role", role);
270                        m.put("content", LINEBREAK.splitAsStream(parameters.get(role).toString().trim()).map(String::trim).collect(joining("\n")));
271                        return m;
272                }).collect(toList()));
273
274                Map<String, String> header = new LinkedHashMap<String, String>(1);
275                header.put("Authorization", "Bearer " + parameters.get("key"));
276
277                Object response = json(context, header, parameters.get("url") + "/chat/completions", request);
278                return streamJsonObjects(response, "choices").map(m -> getMap(m, "message")).map(m -> getString(m, "content")).findFirst().orElse(null);
279        }
280
281}