View Javadoc
1   package org.metricshub.jawk.ext;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * Jawk
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2006 - 2025 MetricsHub
8    * ჻჻჻჻჻჻
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
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   * Metadata describing a single annotated extension function.
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 	 * Returns the Awk keyword mapped to this extension function.
139 	 *
140 	 * @return the keyword exposed by the annotated method
141 	 */
142 	public String getKeyword() {
143 		return keyword;
144 	}
145 
146 	/**
147 	 * Returns the extension type that declares the underlying Java method.
148 	 *
149 	 * @return declaring {@link AbstractExtension} subtype
150 	 */
151 	public Class<? extends AbstractExtension> getDeclaringType() {
152 		return declaringType;
153 	}
154 
155 	/**
156 	 * Returns the fully-qualified class name of the declaring extension.
157 	 *
158 	 * @return extension class name
159 	 */
160 	public String getExtensionClassName() {
161 		return declaringType.getName();
162 	}
163 
164 	/**
165 	 * Returns the minimum number of arguments required to invoke the function.
166 	 *
167 	 * @return required argument count before considering varargs
168 	 */
169 	public int getArity() {
170 		return mandatoryParameterCount;
171 	}
172 
173 	/**
174 	 * Indicates whether the parameter at the supplied index must be an associative
175 	 * array.
176 	 *
177 	 * @param index zero-based parameter index
178 	 * @return {@code true} when the parameter must be an associative array
179 	 * @throws IndexOutOfBoundsException when the index exceeds the parameter count
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 	 * Collects the indexes of arguments that must be associative arrays for a call
190 	 * with the supplied argument count. Vararg positions are included when the
191 	 * vararg parameter requires associative arrays.
192 	 *
193 	 * @param argCount number of arguments supplied by the caller
194 	 * @return indexes of arguments that must be associative arrays
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 	 * Invokes the underlying Java method on the supplied target instance.
219 	 *
220 	 * @param target extension instance to receive the call
221 	 * @param args arguments evaluated by the interpreter
222 	 * @return result of the Java invocation
223 	 * @throws IllegalAwkArgumentException when the arguments violate the metadata
224 	 * @throws IllegalStateException when reflection cannot invoke the method
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 	 * Verifies that the provided argument count satisfies the arity constraints
285 	 * encoded in the metadata.
286 	 *
287 	 * @param argCount number of arguments the caller supplied
288 	 * @throws IllegalAwkArgumentException when the count violates the signature
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 }