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