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