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}