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}