1 package io.jawk.ext;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 import java.io.IOException;
26 import java.io.ObjectInputStream;
27 import java.io.Serializable;
28 import java.lang.reflect.Array;
29 import java.lang.reflect.InvocationTargetException;
30 import java.lang.reflect.Method;
31 import java.lang.reflect.Parameter;
32 import java.util.ArrayList;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Objects;
36
37 import io.jawk.ext.annotations.JawkAssocArray;
38 import io.jawk.ext.annotations.JawkFunction;
39 import io.jawk.jrt.AssocArray;
40 import io.jawk.jrt.IllegalAwkArgumentException;
41
42
43
44
45 public final class ExtensionFunction implements Serializable {
46
47 private static final long serialVersionUID = 1L;
48
49
50 private final String keyword;
51
52
53 private final Class<? extends AbstractExtension> declaringType;
54
55
56 private final String methodName;
57
58
59 private final Class<?>[] parameterTypes;
60 private transient Method method;
61
62
63 private final boolean[] assocArrayParameters;
64
65
66 private final boolean varArgs;
67
68
69 private final int mandatoryParameterCount;
70
71
72 private final boolean varArgAssocArray;
73
74 ExtensionFunction(String keywordParam, Method methodParam) {
75 this.keyword = validateKeyword(keywordParam, methodParam);
76 this.declaringType = resolveDeclaringType(methodParam);
77 this.methodName = methodParam.getName();
78 this.parameterTypes = methodParam.getParameterTypes();
79 this.method = prepareMethod(methodParam);
80 this.varArgs = methodParam.isVarArgs();
81 this.assocArrayParameters = inspectParameters(methodParam, methodParam.getParameters());
82 this.mandatoryParameterCount = varArgs ? assocArrayParameters.length - 1 : assocArrayParameters.length;
83 this.varArgAssocArray = varArgs && assocArrayParameters[assocArrayParameters.length - 1];
84 }
85
86 private static String validateKeyword(String keyword, Method method) {
87 Objects.requireNonNull(method, "method");
88 if (keyword == null || keyword.trim().isEmpty()) {
89 throw new IllegalStateException(
90 "@" + JawkFunction.class.getSimpleName()
91 + " on " + method + " must declare a non-empty name");
92 }
93 return keyword;
94 }
95
96 private static Class<? extends AbstractExtension> resolveDeclaringType(Method method) {
97 Class<?> declaringClass = method.getDeclaringClass();
98 if (!AbstractExtension.class.isAssignableFrom(declaringClass)) {
99 throw new IllegalStateException(
100 "@" + JawkFunction.class.getSimpleName()
101 + " must be declared on a subclass of " + AbstractExtension.class.getName()
102 + ": " + method);
103 }
104 @SuppressWarnings("unchecked")
105 Class<? extends AbstractExtension> type = (Class<? extends AbstractExtension>) declaringClass;
106 return type;
107 }
108
109 private static Method prepareMethod(Method method) {
110 if (java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
111 throw new IllegalStateException(
112 "@" + JawkFunction.class.getSimpleName()
113 + " does not support static methods: " + method.toGenericString());
114 }
115 method.setAccessible(true);
116 return method;
117 }
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149 private boolean[] inspectParameters(Method methodParam, Parameter[] parameters) {
150 boolean[] assoc = new boolean[parameters.length];
151 for (int idx = 0; idx < parameters.length; idx++) {
152 Parameter parameter = parameters[idx];
153 if (parameter.isAnnotationPresent(JawkAssocArray.class)) {
154 Class<?> parameterType = parameter.getType();
155 if (parameter.isVarArgs()) {
156 parameterType = parameterType.getComponentType();
157 }
158 if (!Map.class.isAssignableFrom(parameterType)
159 || !parameterType.isAssignableFrom(AssocArray.class)) {
160 throw new IllegalStateException(
161 "Parameter " + idx + " of " + methodParam
162 + " annotated with @" + JawkAssocArray.class.getSimpleName()
163 + " must accept " + AssocArray.class.getName()
164 + " instances via " + Map.class.getName());
165 }
166 assoc[idx] = true;
167 }
168 }
169 return assoc;
170 }
171
172
173
174
175
176
177
178
179 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
180 in.defaultReadObject();
181 try {
182 Method resolved = declaringType.getDeclaredMethod(methodName, parameterTypes);
183 this.method = prepareMethod(resolved);
184 } catch (NoSuchMethodException ex) {
185 throw new IllegalStateException(
186 "Unable to rehydrate extension method '" + methodName
187 + "' on type " + declaringType.getName(),
188 ex);
189 }
190 }
191
192
193
194
195
196
197 public String getKeyword() {
198 return keyword;
199 }
200
201
202
203
204
205
206 public Class<? extends AbstractExtension> getDeclaringType() {
207 return declaringType;
208 }
209
210
211
212
213
214
215 public String getExtensionClassName() {
216 return declaringType.getName();
217 }
218
219
220
221
222
223
224 public int getArity() {
225 return mandatoryParameterCount;
226 }
227
228
229
230
231
232
233
234
235
236 public boolean expectsAssocArray(int index) {
237 if (index < 0 || index >= assocArrayParameters.length) {
238 throw new IndexOutOfBoundsException("Parameter index out of range: " + index);
239 }
240 return assocArrayParameters[index];
241 }
242
243
244
245
246
247
248
249
250
251 public int[] collectAssocArrayIndexes(int argCount) {
252 verifyArgCount(argCount);
253 List<Integer> indexes = new ArrayList<Integer>();
254 int upperBound = Math.min(argCount, mandatoryParameterCount);
255 for (int idx = 0; idx < upperBound; idx++) {
256 if (assocArrayParameters[idx]) {
257 indexes.add(Integer.valueOf(idx));
258 }
259 }
260 if (varArgs && varArgAssocArray) {
261 for (int idx = mandatoryParameterCount; idx < argCount; idx++) {
262 indexes.add(Integer.valueOf(idx));
263 }
264 }
265 int[] result = new int[indexes.size()];
266 for (int idx = 0; idx < indexes.size(); idx++) {
267 result[idx] = indexes.get(idx).intValue();
268 }
269 return result;
270 }
271
272
273
274
275
276
277
278
279
280
281 public Object invoke(AbstractExtension target, Object[] args) {
282 Objects.requireNonNull(target, "target");
283 if (!declaringType.isInstance(target)) {
284 throw new IllegalArgumentException(
285 "Extension instance " + target.getClass().getName()
286 + " is not compatible with " + declaringType.getName());
287 }
288 int argCount = args == null ? 0 : args.length;
289 verifyArgCount(argCount);
290 enforceAssocArrayParameters(args);
291 Object[] invocationArgs = prepareArguments(args);
292 try {
293 return method.invoke(target, invocationArgs);
294 } catch (IllegalAccessException ex) {
295 throw new IllegalStateException(
296 "Unable to access extension function method for keyword '" + keyword + "'",
297 ex);
298 } catch (InvocationTargetException ex) {
299 Throwable cause = ex.getCause();
300 if (cause instanceof RuntimeException) {
301 throw (RuntimeException) cause;
302 }
303 if (cause instanceof Error) {
304 throw (Error) cause;
305 }
306 throw new IllegalStateException(
307 "Invocation of extension function '" + keyword + "' failed",
308 cause);
309 }
310 }
311
312 private Object[] prepareArguments(Object[] args) {
313 int argCount = args == null ? 0 : args.length;
314 if (argCount == 0) {
315 return varArgs ?
316 new Object[]
317 { Array.newInstance(parameterTypes[parameterTypes.length - 1].getComponentType(), 0) } : new Object[0];
318 }
319 if (!varArgs) {
320 Object[] invocationArgs = new Object[argCount];
321 System.arraycopy(args, 0, invocationArgs, 0, argCount);
322 return invocationArgs;
323 }
324 Object[] invocationArgs = new Object[mandatoryParameterCount + 1];
325 for (int idx = 0; idx < mandatoryParameterCount; idx++) {
326 invocationArgs[idx] = args[idx];
327 }
328 int varArgCount = argCount - mandatoryParameterCount;
329 Class<?> componentType = parameterTypes[parameterTypes.length - 1].getComponentType();
330 Object varArgArray = Array.newInstance(componentType, varArgCount);
331 for (int idx = 0; idx < varArgCount; idx++) {
332 Array.set(varArgArray, idx, args[mandatoryParameterCount + idx]);
333 }
334 invocationArgs[mandatoryParameterCount] = varArgArray;
335 return invocationArgs;
336 }
337
338
339
340
341
342
343
344
345 public void verifyArgCount(int argCount) {
346 if (!varArgs) {
347 if (argCount != mandatoryParameterCount) {
348 throw new IllegalAwkArgumentException(
349 "Extension function '" + keyword + "' expects " + mandatoryParameterCount
350 + " argument(s), not " + argCount);
351 }
352 return;
353 }
354 if (argCount < mandatoryParameterCount) {
355 throw new IllegalAwkArgumentException(
356 "Extension function '" + keyword + "' expects " + getArity()
357 + " argument(s), not " + argCount);
358 }
359 }
360
361 private void enforceAssocArrayParameters(Object[] args) {
362 if (args == null) {
363 return;
364 }
365 int argCount = args.length;
366 int upperBound = Math.min(argCount, mandatoryParameterCount);
367 for (int idx = 0; idx < upperBound; idx++) {
368 if (!assocArrayParameters[idx]) {
369 continue;
370 }
371 Object argument = args[idx];
372 if (!(argument instanceof Map)) {
373 throw new IllegalAwkArgumentException(
374 "Argument " + idx + " passed to extension function '" + keyword
375 + "' must be an associative array");
376 }
377 }
378 if (varArgs && varArgAssocArray) {
379 for (int idx = mandatoryParameterCount; idx < argCount; idx++) {
380 Object argument = args[idx];
381 if (!(argument instanceof Map)) {
382 throw new IllegalAwkArgumentException(
383 "Argument " + idx + " passed to extension function '" + keyword
384 + "' must be an associative array");
385 }
386 }
387 }
388 }
389 }