View Javadoc
1   package io.jawk.ext;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * Jawk
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2006 - 2026 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.lang.reflect.InvocationTargetException;
26  import java.lang.reflect.Method;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.LinkedHashMap;
30  import java.util.List;
31  import java.util.Map;
32  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
33  import io.jawk.backend.AVM;
34  import io.jawk.ext.annotations.JawkBeforeStart;
35  import io.jawk.ext.annotations.JawkFunction;
36  import io.jawk.jrt.IllegalAwkArgumentException;
37  import io.jawk.jrt.JRT;
38  import io.jawk.jrt.VariableManager;
39  import io.jawk.util.AwkSettings;
40  
41  /**
42   * Base class of various extensions.
43   * <p>
44   * Provides functionality common to most extensions,
45   * such as VM and JRT variable management, and convenience
46   * methods such as checkNumArgs() and toAwkString().
47   *
48   * @author Danny Daglas
49   */
50  public abstract class AbstractExtension implements JawkExtension {
51  
52  	private JRT jrt;
53  	private VariableManager vm;
54  	private AwkSettings settings;
55  	private Map<String, ExtensionFunction> annotatedFunctions;
56  	private List<Method> beforeStartMethods;
57  
58  	/**
59  	 * Creates an extension that is not bound to a runtime yet.
60  	 * <p>
61  	 * {@link ExtensionRegistry} instantiates a subclass through its no-arg
62  	 * constructor, then the runtime calls
63  	 * {@link #init(VariableManager, JRT, AwkSettings)} to supply the variable
64  	 * manager, the runtime, and the settings, before any of the extension's Awk
65  	 * functions can be called.
66  	 * </p>
67  	 */
68  	protected AbstractExtension() {
69  		// The runtime handles are supplied by init(), not by construction.
70  	}
71  
72  	/** {@inheritDoc} */
73  	@Override
74  	public String getExtensionName() {
75  		return getClass().getSimpleName();
76  	}
77  
78  	/** {@inheritDoc} */
79  	@Override
80  	@SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Extension needs direct access to runtime, VM and settings")
81  	public void init(VariableManager vmParam, JRT runtime, final AwkSettings conf) {
82  		this.vm = vmParam;
83  		this.jrt = runtime;
84  		this.settings = conf;
85  	}
86  
87  	/**
88  	 * Convert a Jawk variable to a Jawk string
89  	 * based on the value of the CONVFMT variable.
90  	 *
91  	 * @param obj The Jawk variable to convert to a Jawk string.
92  	 * @return A string representation of obj after CONVFMT
93  	 *         has been applied.
94  	 */
95  	protected final String toAwkString(Object obj) {
96  		return jrt.toAwkString(obj);
97  	}
98  
99  	/**
100 	 * Verifies that an exact number of arguments
101 	 * has been passed in by checking the length
102 	 * of the argument array.
103 	 *
104 	 * @param arr The arguments to check.
105 	 * @param expectedNum The expected number of arguments.
106 	 */
107 	protected static void checkNumArgs(Object[] arr, int expectedNum) {
108 		// some sanity checks on the arguments
109 		// (made into assertions so that
110 		// production code does not perform
111 		// these checks)
112 
113 		if (arr.length != expectedNum) {
114 			throw new IllegalAwkArgumentException("Expecting " + expectedNum + " arg(s), got " + arr.length);
115 		}
116 	}
117 
118 	/**
119 	 * <p>
120 	 * Getter for the field <code>jrt</code>.
121 	 * </p>
122 	 *
123 	 * @return the Runtime
124 	 */
125 	protected JRT getJrt() {
126 		return jrt;
127 	}
128 
129 	/**
130 	 * <p>
131 	 * Getter for the field <code>vm</code>.
132 	 * </p>
133 	 *
134 	 * @return the Variable Manager
135 	 */
136 	protected VariableManager getVm() {
137 		return vm;
138 	}
139 
140 	/**
141 	 * <p>
142 	 * Getter for the field <code>settings</code>.
143 	 * </p>
144 	 *
145 	 * @return the Settings
146 	 */
147 	protected AwkSettings getSettings() {
148 		return settings;
149 	}
150 
151 	private Map<String, ExtensionFunction> getAnnotatedFunctions() {
152 		if (annotatedFunctions == null) {
153 			annotatedFunctions = Collections.unmodifiableMap(scanAnnotatedFunctions());
154 		}
155 		return annotatedFunctions;
156 	}
157 
158 	private List<Method> getBeforeStartMethods() {
159 		if (beforeStartMethods == null) {
160 			beforeStartMethods = Collections.unmodifiableList(scanBeforeStartMethods());
161 		}
162 		return beforeStartMethods;
163 	}
164 
165 	private Map<String, ExtensionFunction> scanAnnotatedFunctions() {
166 		Map<String, ExtensionFunction> discovered = new LinkedHashMap<String, ExtensionFunction>();
167 		Class<? extends AbstractExtension> type = getClass();
168 		for (Method method : type.getMethods()) {
169 			JawkFunction function = method.getAnnotation(JawkFunction.class);
170 			if (function == null) {
171 				continue;
172 			}
173 			String keyword = function.value();
174 			ExtensionFunction existing = discovered.put(keyword, new ExtensionFunction(keyword, method));
175 			if (existing != null) {
176 				throw new IllegalStateException(
177 						"Duplicate @JawkFunction mapping for keyword '" + keyword + "' in " + type.getName());
178 			}
179 		}
180 		return discovered;
181 	}
182 
183 	private List<Method> scanBeforeStartMethods() {
184 		List<Method> discovered = new ArrayList<Method>();
185 		Class<? extends AbstractExtension> type = getClass();
186 		for (Method method : type.getMethods()) {
187 			if (!method.isAnnotationPresent(JawkBeforeStart.class)) {
188 				continue;
189 			}
190 			if (java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
191 				throw new IllegalStateException(
192 						"@" + JawkBeforeStart.class.getSimpleName()
193 								+ " does not support static methods: " + method.toGenericString());
194 			}
195 			Class<?>[] parameterTypes = method.getParameterTypes();
196 			if (method.getReturnType() != Void.TYPE
197 					|| parameterTypes.length != 2
198 					|| parameterTypes[0] != AVM.class
199 					|| parameterTypes[1] != JRT.class) {
200 				throw new IllegalStateException(
201 						"@" + JawkBeforeStart.class.getSimpleName()
202 								+ " method must declare void method(AVM, JRT): " + method.toGenericString());
203 			}
204 			method.setAccessible(true);
205 			discovered.add(method);
206 		}
207 		return discovered;
208 	}
209 
210 	/** {@inheritDoc} */
211 	@Override
212 	public Map<String, ExtensionFunction> getExtensionFunctions() {
213 		return getAnnotatedFunctions();
214 	}
215 
216 	/** {@inheritDoc} */
217 	@Override
218 	public void beforeStart(AVM avm, JRT runtime) {
219 		for (Method method : getBeforeStartMethods()) {
220 			try {
221 				method.invoke(this, avm, runtime);
222 			} catch (IllegalAccessException ex) {
223 				throw new IllegalStateException(
224 						"Unable to access extension before-start method " + method.toGenericString(),
225 						ex);
226 			} catch (InvocationTargetException ex) {
227 				Throwable cause = ex.getCause();
228 				if (cause instanceof RuntimeException) {
229 					throw (RuntimeException) cause;
230 				}
231 				if (cause instanceof Error) {
232 					throw (Error) cause;
233 				}
234 				throw new IllegalStateException(
235 						"Extension before-start method failed: " + method.toGenericString(),
236 						cause);
237 			}
238 		}
239 	}
240 }