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}