1 /*
2 * Copyright 2012-2025 CodeLibs Project and the Others.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 * either express or implied. See the License for the specific language
14 * governing permissions and limitations under the License.
15 */
16 package org.codelibs.fess.job;
17
18 import java.io.File;
19 import java.io.FileOutputStream;
20 import java.io.FilenameFilter;
21 import java.io.IOException;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.Properties;
26 import java.util.regex.Pattern;
27
28 import org.apache.commons.io.FileUtils;
29 import org.apache.logging.log4j.LogManager;
30 import org.apache.logging.log4j.Logger;
31 import org.codelibs.core.lang.StringUtil;
32 import org.codelibs.core.timer.TimeoutManager;
33 import org.codelibs.core.timer.TimeoutTask;
34 import org.codelibs.fess.Constants;
35 import org.codelibs.fess.opensearch.config.exentity.ScheduledJob;
36 import org.codelibs.fess.util.ComponentUtil;
37 import org.lastaflute.di.exception.IORuntimeException;
38 import org.lastaflute.job.LaJobRuntime;
39
40 /**
41 * Abstract base class for executable jobs in the Fess search engine.
42 * This class provides common functionality for job execution including process management,
43 * logging configuration, JVM options, and timeout handling.
44 *
45 * <p>Subclasses must implement the abstract methods to define specific job behavior
46 * and execution type identification.</p>
47 *
48 * @version 1.0
49 */
50 public abstract class ExecJob {
51
52 /** Logger instance for this class */
53 private static final Logger logger = LogManager.getLogger(ExecJob.class);
54
55 /** The job executor responsible for running this job */
56 protected JobExecutor jobExecutor;
57
58 /** Unique session identifier for this job execution */
59 protected String sessionId;
60
61 /** Flag indicating whether to use local Fesen instance */
62 protected boolean useLocalFesen = true;
63
64 /** Path to the log file for this job execution */
65 protected String logFilePath;
66
67 /** Log level for this job execution */
68 protected String logLevel;
69
70 /** Suffix to append to log file names */
71 protected String logSuffix = StringUtil.EMPTY;
72
73 /** List of JVM options to apply when executing the job */
74 protected List<String> jvmOptions = new ArrayList<>();
75
76 /** Lasta environment configuration */
77 protected String lastaEnv;
78
79 /** Timeout in seconds for job execution (-1 means no timeout) */
80 protected int timeout = -1; // sec
81
82 /** Flag indicating whether the process has timed out */
83 protected boolean processTimeout = false;
84
85 /**
86 * Default constructor for ExecJob.
87 * Initializes default values for job configuration.
88 */
89 protected ExecJob() {
90 // Default constructor
91 }
92
93 /**
94 * Executes the job and returns the result as a string.
95 * This method contains the main business logic for the specific job implementation.
96 *
97 * @return the execution result message or summary
98 */
99 public abstract String execute();
100
101 /**
102 * Returns the execution type identifier for this job.
103 * This type is used for classpath construction, configuration, and logging purposes.
104 *
105 * @return the execution type (e.g., "crawler", "suggest", etc.)
106 */
107 protected abstract String getExecuteType();
108
109 /**
110 * Executes the job with the specified job executor.
111 * This method sets the job executor and then calls the abstract execute method.
112 *
113 * @param jobExecutor the job executor to use for execution
114 * @return the execution result message or summary
115 */
116 public String execute(final JobExecutor jobExecutor) {
117 jobExecutor(jobExecutor);
118 return execute();
119 }
120
121 /**
122 * Sets the job executor for this job.
123 *
124 * @param jobExecutor the job executor to set
125 * @return this ExecJob instance for method chaining
126 */
127 public ExecJob jobExecutor(final JobExecutor jobExecutor) {
128 this.jobExecutor = jobExecutor;
129 return this;
130 }
131
132 /**
133 * Sets the session ID for this job execution.
134 *
135 * @param sessionId the unique session identifier
136 * @return this ExecJob instance for method chaining
137 */
138 public ExecJob sessionId(final String sessionId) {
139 this.sessionId = sessionId;
140 return this;
141 }
142
143 /**
144 * Sets the log file path for this job execution.
145 *
146 * @param logFilePath the path to the log file
147 * @return this ExecJob instance for method chaining
148 */
149 public ExecJob logFilePath(final String logFilePath) {
150 this.logFilePath = logFilePath;
151 return this;
152 }
153
154 /**
155 * Sets the log level for this job execution.
156 *
157 * @param logLevel the log level (e.g., DEBUG, INFO, WARN, ERROR)
158 * @return this ExecJob instance for method chaining
159 */
160 public ExecJob logLevel(final String logLevel) {
161 this.logLevel = logLevel;
162 return this;
163 }
164
165 /**
166 * Sets the log suffix for this job execution.
167 * The suffix is trimmed and whitespace is replaced with underscores.
168 *
169 * @param logSuffix the suffix to append to log file names
170 * @return this ExecJob instance for method chaining
171 */
172 public ExecJob logSuffix(final String logSuffix) {
173 this.logSuffix = logSuffix.trim().replaceAll("\\s", "_");
174 return this;
175 }
176
177 /**
178 * Sets the timeout for this job execution.
179 *
180 * @param timeout the timeout in seconds (-1 for no timeout)
181 * @return this ExecJob instance for method chaining
182 */
183 public ExecJob timeout(final int timeout) {
184 this.timeout = timeout;
185 return this;
186 }
187
188 /**
189 * Sets whether to use local Fesen instance.
190 *
191 * @param useLocalFesen true to use local Fesen, false otherwise
192 * @return this ExecJob instance for method chaining
193 */
194 public ExecJob useLocalFesen(final boolean useLocalFesen) {
195 this.useLocalFesen = useLocalFesen;
196 return this;
197 }
198
199 /**
200 * Enables remote debugging for this job execution.
201 * Adds JVM options for remote debugging on localhost:8000.
202 *
203 * @return this ExecJob instance for method chaining
204 */
205 public ExecJob remoteDebug() {
206 return jvmOptions("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=localhost:8000");
207 }
208
209 /**
210 * Enables garbage collection logging for this job execution.
211 * Configures JVM options for comprehensive GC logging with rotation.
212 *
213 * @return this ExecJob instance for method chaining
214 */
215 public ExecJob gcLogging() {
216 final StringBuilder buf = new StringBuilder(100);
217 buf.append("-Xlog:gc*,gc+age=trace,safepoint:file=");
218 if (logFilePath != null) {
219 buf.append(logFilePath);
220 } else {
221 buf.append(ComponentUtil.getSystemHelper().getLogFilePath());
222 }
223 buf.append(File.separator);
224 buf.append("gc-").append(getExecuteType()).append(".log");
225 buf.append(":utctime,pid,tags:filecount=5,filesize=64m");
226 return jvmOptions(buf.toString());
227 }
228
229 /**
230 * Adds JVM options for this job execution.
231 *
232 * @param options the JVM options to add
233 * @return this ExecJob instance for method chaining
234 */
235 public ExecJob jvmOptions(final String... options) {
236 Collections.addAll(jvmOptions, options);
237 return this;
238 }
239
240 /**
241 * Sets the Lasta environment configuration.
242 *
243 * @param env the Lasta environment string
244 * @return this ExecJob instance for method chaining
245 */
246 public ExecJob lastaEnv(final String env) {
247 lastaEnv = env;
248 return this;
249 }
250
251 /**
252 * Adds a system property to the command list.
253 * If the property exists in the system, it uses that value with optional append value.
254 * Otherwise, it uses the default value if provided.
255 *
256 * @param cmdList the command list to add the property to
257 * @param name the property name
258 * @param defaultValue the default value to use if property doesn't exist
259 * @param appendValue the value to append to the property value
260 */
261 protected void addSystemProperty(final List<String> cmdList, final String name, final String defaultValue, final String appendValue) {
262 final String value = System.getProperty(name);
263 if (value != null) {
264 final StringBuilder buf = new StringBuilder();
265 buf.append("-D").append(name).append("=").append(value);
266 if (appendValue != null) {
267 buf.append(appendValue);
268 }
269 cmdList.add(buf.toString());
270 } else if (defaultValue != null) {
271 cmdList.add("-D" + name + "=" + defaultValue);
272 }
273 }
274
275 /**
276 * Adds all Fess configuration properties to the command list.
277 * Properties starting with the Fess config prefix are included.
278 *
279 * @param cmdList the command list to add properties to
280 */
281 protected void addFessConfigProperties(final List<String> cmdList) {
282 System.getProperties()
283 .keySet()
284 .stream()
285 .filter(k -> k != null && k.toString().startsWith(Constants.FESS_CONFIG_PREFIX))
286 .forEach(k -> addSystemProperty(cmdList, k.toString(), null, null));
287 }
288
289 /**
290 * Adds all Fess system properties to the command list.
291 * Properties starting with the Fess system property prefix are included.
292 *
293 * @param cmdList the command list to add properties to
294 */
295 protected void addFessSystemProperties(final List<String> cmdList) {
296 System.getProperties()
297 .keySet()
298 .stream()
299 .filter(k -> k != null && k.toString().startsWith(Constants.SYSTEM_PROP_PREFIX))
300 .forEach(k -> addSystemProperty(cmdList, k.toString(), null, null));
301 }
302
303 /**
304 * Adds custom system properties that match the given regex pattern to the command list.
305 *
306 * @param cmdList the command list to add properties to
307 * @param regex the regular expression pattern to match property names
308 */
309 protected void addFessCustomSystemProperties(final List<String> cmdList, final String regex) {
310 if (StringUtil.isNotBlank(regex)) {
311 final Pattern pattern = Pattern.compile(regex);
312 System.getProperties()
313 .keySet()
314 .stream()
315 .filter(k -> k != null && pattern.matcher(k.toString()).matches())
316 .forEach(k -> addSystemProperty(cmdList, k.toString(), null, null));
317 }
318 }
319
320 /**
321 * Deletes the specified temporary directory.
322 * Logs a warning if the directory cannot be deleted.
323 *
324 * @param ownTmpDir the temporary directory to delete
325 */
326 protected void deleteTempDir(final File ownTmpDir) {
327 if (ownTmpDir == null) {
328 return;
329 }
330 if (!FileUtils.deleteQuietly(ownTmpDir)) {
331 logger.warn("Could not delete temp directory: path={}", ownTmpDir.getAbsolutePath());
332 }
333 }
334
335 /**
336 * Appends JAR files from the specified directory to the classpath buffer.
337 *
338 * @param cpSeparator the classpath separator to use
339 * @param buf the StringBuilder to append to
340 * @param libDir the directory containing JAR files
341 * @param basePath the base path to prepend to JAR file names
342 */
343 protected void appendJarFile(final String cpSeparator, final StringBuilder buf, final File libDir, final String basePath) {
344 final File[] jarFiles = libDir.listFiles((FilenameFilter) (dir, name) -> name.toLowerCase().endsWith(".jar"));
345 if (jarFiles != null) {
346 for (final File file : jarFiles) {
347 buf.append(cpSeparator);
348 buf.append(basePath);
349 buf.append(file.getName());
350 }
351 }
352 }
353
354 /**
355 * Creates a timeout task for this job execution.
356 * If timeout is not set or is <= 0, returns null.
357 *
358 * @return the timeout task, or null if no timeout is configured
359 */
360 protected TimeoutTask createTimeoutTask() {
361 if (timeout <= 0) {
362 return null;
363 }
364 return TimeoutManager.getInstance().addTimeoutTarget(() -> {
365 logger.warn("Process terminated: timeout={}s exceeded", timeout);
366 ComponentUtil.getProcessHelper().destroyProcess(sessionId);
367 processTimeout = true;
368 }, timeout, false);
369 }
370
371 /**
372 * Creates and stores system properties to the specified file.
373 * Includes system properties and job runtime information if available.
374 *
375 * @param cmdList the command list (used as comment in properties file)
376 * @param propFile the file to store properties to
377 * @throws IORuntimeException if an I/O error occurs
378 */
379 protected void createSystemProperties(final List<String> cmdList, final File propFile) {
380 try (FileOutputStream out = new FileOutputStream(propFile)) {
381 final Properties prop = new Properties();
382 prop.putAll(ComponentUtil.getSystemProperties());
383 final LaJobRuntime jobRuntime = ComponentUtil.getJobHelper().getJobRuntime();
384 if (jobRuntime != null) {
385 final ScheduledJob job = (ScheduledJob) jobRuntime.getParameterMap().get(Constants.SCHEDULED_JOB);
386 if (job != null) {
387 prop.setProperty("job.runtime.id", job.getId());
388 prop.setProperty("job.runtime.name", job.getName());
389 }
390 }
391 prop.store(out, cmdList.toString());
392 } catch (final IOException e) {
393 throw new IORuntimeException(e);
394 }
395 }
396
397 /**
398 * Generates a log name using the specified prefix, execution type, and suffix.
399 *
400 * @param logPrefix the prefix for the log name
401 * @return the generated log name
402 */
403 protected String getLogName(final String logPrefix) {
404 if (logSuffix.length() > 0) {
405 return logPrefix + "-" + getExecuteType() + "-" + logSuffix;
406 }
407 return logPrefix + "-" + getExecuteType();
408 }
409 }