1
2
3
4
5
6
7
8
9
10
11
12
13
14 package eu.fbk.rdfpro.util;
15
16 import java.io.BufferedReader;
17 import java.io.IOException;
18 import java.lang.reflect.Method;
19 import java.lang.reflect.Modifier;
20 import java.nio.charset.Charset;
21 import java.nio.file.Files;
22 import java.nio.file.Path;
23 import java.nio.file.Paths;
24 import java.util.HashMap;
25 import java.util.Map;
26 import java.util.Objects;
27 import java.util.regex.Pattern;
28
29 import javax.script.Invocable;
30 import javax.script.ScriptEngine;
31 import javax.script.ScriptEngineManager;
32 import javax.script.ScriptException;
33
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 import info.aduna.text.ASCIIUtil;
38
39 public final class Scripting {
40
41 private static final Logger LOGGER = LoggerFactory.getLogger(Scripting.class);
42
43 private static final ScriptEngineManager MANAGER = new ScriptEngineManager();
44
45 private static final Pattern LANGUAGE_PATTERN = Pattern.compile("[a-z]+");
46
47 private static final Map<String, String> INCLUDES;
48
49 static {
50 final Map<String, String> includes = new HashMap<>();
51 for (final String property : Environment.getPropertyNames()) {
52 if (property.startsWith("scripting")) {
53 final String[] tokens = property.split("\\.");
54 if (tokens.length == 4 && tokens[2].equals("includes")) {
55 try {
56 final String language = tokens[1];
57 final String location = Environment.getProperty(property);
58 if (!LANGUAGE_PATTERN.matcher(language).matches()) {
59 throw new Error("Invalid language: " + language);
60 }
61 final StringBuilder builder = new StringBuilder();
62 try (BufferedReader reader = new BufferedReader(IO.utf8Reader(IO
63 .read(location)))) {
64 String line;
65 while ((line = reader.readLine()) != null) {
66 builder.append(line).append("\n");
67 }
68 } catch (final IOException ex) {
69 throw new Error("Could not read " + location);
70 }
71 final String newInclude = builder.toString();
72 final String oldInclude = includes.get(language);
73 includes.put(language, oldInclude == null ? newInclude : oldInclude
74 + newInclude);
75 LOGGER.debug("Loaded {}", location);
76 } catch (final Throwable ex) {
77 LOGGER.error("Failed to process include " + property, ex);
78 }
79 }
80 }
81 }
82 INCLUDES = includes;
83 }
84
85 public static boolean isScript(final String spec) {
86 final int index = spec.indexOf(':');
87 if (index > 0) {
88 final String language = spec.substring(0, 1);
89 return LANGUAGE_PATTERN.matcher(language).matches();
90 }
91 return false;
92 }
93
94 public static <T> T compile(final Class<T> interfaceClass, final String spec,
95 final String... parameterNames) {
96
97
98 Objects.requireNonNull(interfaceClass);
99 Objects.requireNonNull(spec);
100 if (!interfaceClass.isInterface()) {
101 throw new IllegalArgumentException("Class " + interfaceClass.getName()
102 + " is not an interface");
103 }
104
105
106 Method method = null;
107 for (final Method m : interfaceClass.getMethods()) {
108 if (Modifier.isAbstract(m.getModifiers())) {
109 if (method != null) {
110 throw new IllegalArgumentException(interfaceClass
111 + " defines multiple methods");
112 }
113 method = m;
114 }
115 }
116
117
118 if (method.getParameterCount() != parameterNames.length) {
119 throw new IllegalArgumentException("Signature of method " + method
120 + " does not match parameters " + String.join(", ", parameterNames));
121 }
122
123
124 final int index = spec.indexOf(':');
125 final String language = index < 0 ? null : spec.substring(0, index);
126 if (language == null || !LANGUAGE_PATTERN.matcher(language).matches()) {
127 throw new IllegalArgumentException("Not a valid script specification: " + spec);
128 }
129 String expression = spec.substring(index + 1);
130
131
132 try {
133 final Path path = Paths.get(expression);
134 if (Files.isReadable(path)) {
135 expression = new String(Files.readAllBytes(path), Charset.forName("UTF-8"));
136 }
137 } catch (final Throwable ex) {
138
139 }
140
141
142 expression = rewrite(expression);
143
144 try {
145
146 LOGGER.debug("Compiling expression:\n{}", expression);
147 return compileHelper(interfaceClass, expression, language);
148
149 } catch (final Throwable ex1) {
150 try {
151
152
153 expression = wrap(method, expression, language, parameterNames);
154 LOGGER.debug("Compiling wrapped expression:\n{}", expression);
155 return compileHelper(interfaceClass, expression, language);
156
157 } catch (final Throwable ex2) {
158
159 final StringBuilder builder = new StringBuilder();
160 builder.append("Cannot extract ").append(interfaceClass).append(" from script");
161 builder.append("\nError when compiled as is:\n").append(ex1.getMessage());
162 builder.append("\nError when wrapped in function:\n").append(ex2.getMessage());
163 builder.append("\nScript spec is:\n").append(spec);
164 throw new IllegalArgumentException(builder.toString());
165 }
166 }
167 }
168
169 private static <T> T compileHelper(final Class<T> interfaceClass, final String expression,
170 final String language) throws ScriptException {
171
172
173 final ScriptEngine engine = MANAGER.getEngineByExtension(language);
174 if (!(engine instanceof Invocable)) {
175 throw new UnsupportedOperationException("Unsupported script language: " + language);
176 }
177
178
179 final String include = INCLUDES.get(language);
180 if (include != null) {
181 engine.eval(include);
182 }
183
184
185 engine.eval(expression);
186
187
188 return interfaceClass.cast(((Invocable) engine).getInterface(interfaceClass));
189 }
190
191 private static String wrap(final Method method, final String expression,
192 final String language, final String... parameterNames) {
193
194 if ("js".equals(language)) {
195 final StringBuilder builder = new StringBuilder();
196 builder.append("function ");
197 builder.append(method.getName());
198 builder.append("(");
199 builder.append(String.join(", ", parameterNames));
200 builder.append(") {\n");
201 builder.append(expression);
202 if (method.getReturnType() != null && !method.getReturnType().equals(Void.class)) {
203 builder.append("\nthrow \"missing return statement in supplied script\";");
204 }
205 builder.append("\n}");
206 return builder.toString();
207
208 } else if ("groovy".equals(language)) {
209 final StringBuilder builder = new StringBuilder();
210 builder.append("def ");
211 builder.append(method.getName());
212 builder.append("(");
213 builder.append(String.join(", ", parameterNames));
214 builder.append(") {\n");
215 builder.append(expression);
216 builder.append("\n}");
217 return builder.toString();
218
219 } else {
220 throw new UnsupportedOperationException(
221 "Wrapping within a function is unsupported for language " + language);
222 }
223 }
224
225 private static String rewrite(final String script) {
226
227 final StringBuilder builder = new StringBuilder();
228 final int length = script.length();
229 int i = 0;
230
231 try {
232 while (i < length) {
233 char c = script.charAt(i);
234 if (c == '<') {
235 final int end = parseURI(script, i);
236 if (end >= 0) {
237 final String uri = script.substring(i, end);
238 builder.append("(new org.openrdf.model.impl.URIImpl(\"").append(uri)
239 .append("\"))");
240 i = end;
241 } else {
242 builder.append(c);
243 ++i;
244 }
245
246 } else if (isPN_CHARS_BASE(c)) {
247 final int end = parseQName(script, i);
248 if (end >= 0) {
249 final String uri = Statements.parseValue(script.substring(i, end),
250 Namespaces.DEFAULT).stringValue();
251 builder.append("(new org.openrdf.model.impl.URIImpl(\"").append(uri)
252 .append("\"))");
253 i = end;
254 } else {
255 do {
256 builder.append(c);
257 c = script.charAt(++i);
258 } while (Character.isLetterOrDigit(c));
259 }
260
261 } else if (c == '\'' || c == '\"') {
262 final char d = c;
263 builder.append(d);
264 do {
265 c = script.charAt(++i);
266 builder.append(c);
267 } while (c != d || script.charAt(i - 1) == '\\');
268 ++i;
269
270 } else {
271 builder.append(c);
272 ++i;
273 }
274 }
275 } catch (final Exception ex) {
276 throw new IllegalArgumentException("Illegal URI escaping near offset " + i, ex);
277 }
278
279 return builder.toString();
280 }
281
282
283
284
285 private static int parseURI(final String string, int i) {
286 final int len = string.length();
287 if (string.charAt(i) != '<') {
288 return -1;
289 }
290 for (++i; i < len; ++i) {
291 final char c = string.charAt(i);
292 if (c == '<' || c == '\"' || c == '{' || c == '}' || c == '|' || c == '^' || c == '`'
293 || c == '\\' || c == ' ') {
294 return -1;
295 }
296 if (c == '>') {
297 return i + 1;
298 }
299 }
300 return -1;
301 }
302
303 private static int parseQName(final String string, int i) {
304 final int len = string.length();
305 char c;
306 if (!isPN_CHARS_BASE(string.charAt(i))) {
307 return -1;
308 }
309 for (; i < len; ++i) {
310 c = string.charAt(i);
311 if (!isPN_CHARS(c) && c != '.') {
312 break;
313 }
314 }
315 if (string.charAt(i - 1) == '.' || string.charAt(i) != ':' || i == len - 1) {
316 return -1;
317 }
318 c = string.charAt(++i);
319 if (!isPN_CHARS_U(c) && c != ':' && c != '%' && !Character.isDigit(c)) {
320 return -1;
321 }
322 for (; i < len; ++i) {
323 c = string.charAt(i);
324 if (!isPN_CHARS(c) && c != '.' && c != ':' && c != '%') {
325 break;
326 }
327 }
328 if (string.charAt(i - 1) == '.') {
329 return -1;
330 }
331 return i;
332 }
333
334 private static boolean isPN_CHARS(final int c) {
335 return isPN_CHARS_U(c) || ASCIIUtil.isNumber(c) || c == 45 || c == 183 || c >= 768
336 && c <= 879 || c >= 8255 && c <= 8256;
337 }
338
339 private static boolean isPN_CHARS_U(final int c) {
340 return isPN_CHARS_BASE(c) || c == 95;
341 }
342
343 private static boolean isPN_CHARS_BASE(final int c) {
344 return ASCIIUtil.isLetter(c) || c >= 192 && c <= 214 || c >= 216 && c <= 246 || c >= 248
345 && c <= 767 || c >= 880 && c <= 893 || c >= 895 && c <= 8191 || c >= 8204
346 && c <= 8205 || c >= 8304 && c <= 8591 || c >= 11264 && c <= 12271 || c >= 12289
347 && c <= 55295 || c >= 63744 && c <= 64975 || c >= 65008 && c <= 65533
348 || c >= 65536 && c <= 983039;
349 }
350
351 }