View Javadoc
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 &lt;= 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 }