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