1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.script.groovy;
17
18 import java.io.IOException;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.concurrent.ExecutionException;
23
24 import org.apache.logging.log4j.LogManager;
25 import org.apache.logging.log4j.Logger;
26 import org.codelibs.core.lang.StringUtil;
27 import org.codelibs.fess.Constants;
28 import org.codelibs.fess.exception.JobProcessingException;
29 import org.codelibs.fess.opensearch.config.exentity.ScheduledJob;
30 import org.codelibs.fess.script.AbstractScriptEngine;
31 import org.codelibs.fess.util.ComponentUtil;
32 import org.lastaflute.di.core.factory.SingletonLaContainerFactory;
33 import org.lastaflute.job.LaJobRuntime;
34
35 import com.google.common.cache.Cache;
36 import com.google.common.cache.CacheBuilder;
37 import com.google.common.cache.RemovalNotification;
38
39 import groovy.lang.Binding;
40 import groovy.lang.GroovyClassLoader;
41 import groovy.lang.Script;
42 import jakarta.annotation.PostConstruct;
43 import jakarta.annotation.PreDestroy;
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 public class GroovyEngine extends AbstractScriptEngine {
65 private static final Logger logger = LogManager.getLogger(GroovyEngine.class);
66
67
68 protected int scriptCacheSize = 1000;
69
70
71 protected int maxScriptLogLength = 200;
72
73
74 protected boolean scriptAuditLogEnabled;
75
76 private Cache<String, CachedScript> scriptCache;
77
78
79
80
81 public GroovyEngine() {
82 super();
83 buildScriptCache();
84 }
85
86
87
88
89
90 @PostConstruct
91 public void init() {
92 buildScriptCache();
93 scriptAuditLogEnabled = ComponentUtil.available() && ComponentUtil.getFessConfig().isScriptAuditLogEnabled()
94 && ComponentUtil.hasComponent("activityHelper");
95 }
96
97 private void buildScriptCache() {
98 final Cache<String, CachedScript> oldCache = scriptCache;
99 scriptCache = CacheBuilder.newBuilder()
100 .maximumSize(scriptCacheSize)
101 .removalListener((final RemovalNotification<String, CachedScript> notification) -> {
102 notification.getValue().close();
103 })
104 .build();
105 if (oldCache != null) {
106 oldCache.invalidateAll();
107 }
108 }
109
110
111
112
113
114
115 public void setScriptCacheSize(final int scriptCacheSize) {
116 this.scriptCacheSize = scriptCacheSize;
117 }
118
119
120
121
122
123
124 public void setMaxScriptLogLength(final int maxScriptLogLength) {
125 this.maxScriptLogLength = maxScriptLogLength;
126 }
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141 @Override
142 public Object evaluate(final String template, final Map<String, Object> paramMap) {
143 if (StringUtil.isBlank(template)) {
144 if (logger.isDebugEnabled()) {
145 logger.debug("Template is blank, returning null");
146 }
147 return null;
148 }
149
150 final Map<String, Object> safeParamMap = paramMap != null ? paramMap : Collections.emptyMap();
151
152 final Map<String, Object> bindingMap = new HashMap<>(safeParamMap);
153 bindingMap.put("container", SingletonLaContainerFactory.getContainer());
154
155 try {
156 final CachedScript cached = getOrCompile(template);
157 final Script script = cached.scriptClass.getDeclaredConstructor().newInstance();
158 script.setBinding(new Binding(bindingMap));
159
160 if (logger.isDebugEnabled()) {
161 logger.debug("Evaluating Groovy script: template={}", template);
162 }
163
164 final Object result = script.run();
165 logScriptExecution(template, "success");
166 return result;
167 } catch (final JobProcessingException e) {
168 if (logger.isDebugEnabled()) {
169 logger.debug("Script raised JobProcessingException", e);
170 }
171 logScriptExecution(template, "failure:" + e.getClass().getSimpleName());
172 throw e;
173 } catch (final Exception e) {
174 final String truncatedScript =
175 template.length() > maxScriptLogLength ? template.substring(0, maxScriptLogLength) + "..." : template;
176 logger.warn("Failed to evaluate Groovy script: script(length={})={}, parameterKeys={}", template.length(), truncatedScript,
177 safeParamMap.keySet(), e);
178 logScriptExecution(template, "failure:" + e.getClass().getSimpleName());
179 return null;
180 }
181 }
182
183 @SuppressWarnings("unchecked")
184 private CachedScript getOrCompile(final String template) {
185 try {
186 return scriptCache.get(template, () -> {
187 ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();
188 if (parentClassLoader == null) {
189 parentClassLoader = GroovyEngine.class.getClassLoader();
190 }
191 final GroovyClassLoader classLoader = new GroovyClassLoader(parentClassLoader);
192 try {
193 final Class<? extends Script> scriptClass = (Class<? extends Script>) classLoader.parseClass(template);
194 return new CachedScript(scriptClass, classLoader);
195 } catch (final Exception e) {
196 try {
197 classLoader.clearCache();
198 classLoader.close();
199 } catch (final IOException closeEx) {
200 logger.warn("Failed to close GroovyClassLoader after compilation failure", closeEx);
201 }
202 throw e;
203 }
204 });
205 } catch (final ExecutionException e) {
206 throw (RuntimeException) e.getCause();
207 }
208 }
209
210
211
212
213
214 @PreDestroy
215 public void close() {
216 scriptCache.invalidateAll();
217 scriptCache.cleanUp();
218 }
219
220
221
222
223
224
225 @Override
226 protected String getName() {
227 return "groovy";
228 }
229
230
231
232
233
234
235 protected ScheduledJob getCurrentScheduledJob() {
236 try {
237 if (!ComponentUtil.hasComponent("jobHelper")) {
238 return null;
239 }
240 final LaJobRuntime runtime = ComponentUtil.getJobHelper().getJobRuntime();
241 if (runtime != null) {
242 final Object job = runtime.getParameterMap().get(Constants.SCHEDULED_JOB);
243 if (job instanceof ScheduledJob) {
244 return (ScheduledJob) job;
245 }
246 }
247 } catch (final Exception e) {
248 if (logger.isDebugEnabled()) {
249 logger.debug("Failed to get scheduled job from thread local", e);
250 }
251 }
252 return null;
253 }
254
255
256
257
258
259
260
261 protected void logScriptExecution(final String script, final String result) {
262 if (!scriptAuditLogEnabled) {
263 return;
264 }
265 try {
266 String source = "unknown";
267 String user = "system";
268
269 final ScheduledJob job = getCurrentScheduledJob();
270 if (job != null) {
271 source = "scheduler:" + job.getName();
272 if (job.getCreatedBy() != null) {
273 user = job.getCreatedBy();
274 }
275 } else {
276 try {
277 user = ComponentUtil.getSystemHelper().getUsername();
278 } catch (final Exception e) {
279
280 }
281 }
282
283 ComponentUtil.getActivityHelper().scriptExecution(getName(), script, source, user, result);
284 } catch (final Exception e) {
285 if (logger.isDebugEnabled()) {
286 logger.debug("Failed to log script execution", e);
287 }
288 }
289 }
290
291
292
293
294
295 private static class CachedScript {
296 final Class<? extends Script> scriptClass;
297 private final GroovyClassLoader classLoader;
298
299 CachedScript(final Class<? extends Script> scriptClass, final GroovyClassLoader classLoader) {
300 this.scriptClass = scriptClass;
301 this.classLoader = classLoader;
302 }
303
304 void close() {
305 try {
306 classLoader.clearCache();
307 classLoader.close();
308 } catch (final IOException e) {
309 LogManager.getLogger(GroovyEngine.class).warn("Failed to close GroovyClassLoader", e);
310 }
311 }
312 }
313
314 }