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 static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.io.File;
21  import java.util.ArrayList;
22  import java.util.List;
23  
24  import org.apache.commons.lang3.RandomStringUtils;
25  import org.apache.commons.lang3.SystemUtils;
26  import org.apache.logging.log4j.LogManager;
27  import org.apache.logging.log4j.Logger;
28  import org.codelibs.core.lang.StringUtil;
29  import org.codelibs.core.timer.TimeoutTask;
30  import org.codelibs.fess.Constants;
31  import org.codelibs.fess.exception.JobProcessingException;
32  import org.codelibs.fess.exec.ThumbnailGenerator;
33  import org.codelibs.fess.helper.ProcessHelper;
34  import org.codelibs.fess.mylasta.direction.FessConfig;
35  import org.codelibs.fess.util.ComponentUtil;
36  import org.codelibs.fess.util.InputStreamThread;
37  import org.codelibs.fess.util.JobProcess;
38  import org.codelibs.fess.util.ResourceUtil;
39  import org.codelibs.fess.util.SystemUtil;
40  
41  import jakarta.servlet.ServletContext;
42  
43  /**
44   * Job class for generating thumbnails for documents in the search engine.
45   * This job executes the ThumbnailGenerator process as a separate JVM process
46   * to create thumbnail images for supported document types.
47   */
48  public class GenerateThumbnailJob extends ExecJob {
49      /** Logger for this class. */
50      static final Logger logger = LogManager.getLogger(GenerateThumbnailJob.class);
51  
52      /** Number of threads to use for thumbnail generation. */
53      protected int numOfThreads = 1;
54  
55      /** Flag indicating whether to perform cleanup operations. */
56      protected boolean cleanup = false;
57  
58      /**
59       * Default constructor for the GenerateThumbnailJob.
60       */
61      public GenerateThumbnailJob() {
62          super();
63      }
64  
65      /**
66       * Sets the number of threads to use for thumbnail generation.
67       *
68       * @param numOfThreads the number of threads
69       * @return this job instance for method chaining
70       */
71      public GenerateThumbnailJob numOfThreads(final int numOfThreads) {
72          this.numOfThreads = numOfThreads;
73          return this;
74      }
75  
76      /**
77       * Enables cleanup operations for this job.
78       *
79       * @return this job instance for method chaining
80       */
81      public GenerateThumbnailJob cleanup() {
82          cleanup = true;
83          return this;
84      }
85  
86      @Override
87      public String execute() {
88          final StringBuilder resultBuf = new StringBuilder();
89  
90          if (sessionId == null) { // create session id
91              sessionId = RandomStringUtils.randomAlphabetic(15);
92          }
93          resultBuf.append("Session Id: ").append(sessionId).append("\n");
94          if (jobExecutor != null) {
95              jobExecutor.addShutdownListener(() -> ComponentUtil.getProcessHelper().destroyProcess(sessionId));
96          }
97  
98          final TimeoutTask timeoutTask = createTimeoutTask();
99          try {
100             executeThumbnailGenerator();
101         } catch (final Exception e) {
102             logger.warn("Failed to generate thumbnails.", e);
103             resultBuf.append(e.getMessage()).append("\n");
104         } finally {
105             if (timeoutTask != null && !timeoutTask.isCanceled()) {
106                 timeoutTask.cancel();
107             }
108         }
109 
110         return resultBuf.toString();
111 
112     }
113 
114     /**
115      * Executes the thumbnail generator process.
116      * Sets up the classpath, JVM options, and command line arguments
117      * to launch the ThumbnailGenerator in a separate process.
118      *
119      * @throws JobProcessingException if the thumbnail generation process fails
120      */
121     protected void executeThumbnailGenerator() {
122         final List<String> cmdList = new ArrayList<>();
123         final String cpSeparator = SystemUtils.IS_OS_WINDOWS ? ";" : ":";
124         final ServletContext servletContext = ComponentUtil.getComponent(ServletContext.class);
125         final ProcessHelper processHelper = ComponentUtil.getProcessHelper();
126         final FessConfig fessConfig = ComponentUtil.getFessConfig();
127 
128         cmdList.add(fessConfig.getJavaCommandPath());
129 
130         // -cp
131         cmdList.add("-cp");
132         final StringBuilder buf = new StringBuilder(100);
133         ResourceUtil.getOverrideConfPath().ifPresent(p -> {
134             buf.append(p);
135             buf.append(cpSeparator);
136         });
137         final String confPath = System.getProperty(Constants.FESS_CONF_PATH);
138         if (StringUtil.isNotBlank(confPath)) {
139             buf.append(confPath);
140             buf.append(cpSeparator);
141         }
142         // WEB-INF/env/thumbnail/resources
143         buf.append("WEB-INF");
144         buf.append(File.separator);
145         buf.append("env");
146         buf.append(File.separator);
147         buf.append(getExecuteType());
148         buf.append(File.separator);
149         buf.append("resources");
150         buf.append(cpSeparator);
151         // WEB-INF/classes
152         buf.append("WEB-INF");
153         buf.append(File.separator);
154         buf.append("classes");
155         // target/classes
156         final String userDir = System.getProperty("user.dir");
157         final File targetDir = new File(userDir, "target");
158         final File targetClassesDir = new File(targetDir, "classes");
159         if (targetClassesDir.isDirectory()) {
160             buf.append(cpSeparator);
161             buf.append(targetClassesDir.getAbsolutePath());
162         }
163         // WEB-INF/lib
164         appendJarFile(cpSeparator, buf, new File(servletContext.getRealPath("/WEB-INF/lib")),
165                 "WEB-INF" + File.separator + "lib" + File.separator);
166         // WEB-INF/env/thumbnail/lib
167         appendJarFile(cpSeparator, buf, new File(servletContext.getRealPath("/WEB-INF/env/" + getExecuteType() + "/lib")),
168                 "WEB-INF" + File.separator + "env" + File.separator + getExecuteType() + File.separator + "lib" + File.separator);
169         // WEB-INF/plugin
170         appendJarFile(cpSeparator, buf, new File(servletContext.getRealPath("/WEB-INF/plugin")),
171                 "WEB-INF" + File.separator + "plugin" + File.separator);
172         final File targetLibDir = new File(targetDir, "fess" + File.separator + "WEB-INF" + File.separator + "lib");
173         if (targetLibDir.isDirectory()) {
174             appendJarFile(cpSeparator, buf, targetLibDir, targetLibDir.getAbsolutePath() + File.separator);
175         }
176         cmdList.add(buf.toString());
177 
178         if (useLocalFesen) {
179             final String httpAddress = SystemUtil.getSearchEngineHttpAddress();
180             if (StringUtil.isNotBlank(httpAddress)) {
181                 cmdList.add("-D" + Constants.FESS_SEARCH_ENGINE_HTTP_ADDRESS + "=" + httpAddress);
182             }
183         }
184 
185         final String systemLastaEnv = System.getProperty("lasta.env");
186         if (StringUtil.isNotBlank(systemLastaEnv)) {
187             if ("web".equals(systemLastaEnv)) {
188                 cmdList.add("-Dlasta.env=" + getExecuteType());
189             } else {
190                 cmdList.add("-Dlasta.env=" + systemLastaEnv);
191             }
192         } else if (StringUtil.isNotBlank(lastaEnv)) {
193             cmdList.add("-Dlasta.env=" + lastaEnv);
194         } else {
195             cmdList.add("-Dlasta.env=" + getExecuteType());
196         }
197 
198         addFessConfigProperties(cmdList);
199         addFessSystemProperties(cmdList);
200         addFessCustomSystemProperties(cmdList, fessConfig.getJobSystemPropertyFilterPattern());
201         addSystemProperty(cmdList, Constants.FESS_CONF_PATH, null, null);
202         cmdList.add("-Dfess." + getExecuteType() + ".process=true");
203         if (logFilePath == null) {
204             final String value = System.getProperty("fess.log.path");
205             logFilePath = value != null ? value : new File(targetDir, "logs").getAbsolutePath();
206         }
207         cmdList.add("-Dfess.log.path=" + logFilePath);
208         addSystemProperty(cmdList, Constants.FESS_VAR_PATH, null, null);
209         addSystemProperty(cmdList, Constants.FESS_THUMBNAIL_PATH, null, null);
210         addSystemProperty(cmdList, "fess.log.name", getLogName("fess"), getLogName(StringUtil.EMPTY));
211         if (logLevel != null) {
212             cmdList.add("-Dfess.log.level=" + logLevel);
213         }
214         stream(fessConfig.getJvmThumbnailOptionsAsArray())
215                 .of(stream -> stream.filter(StringUtil::isNotBlank).forEach(value -> cmdList.add(value)));
216 
217         File ownTmpDir = null;
218         final String tmpDir = System.getProperty("java.io.tmpdir");
219         if (fessConfig.isUseOwnTmpDir() && StringUtil.isNotBlank(tmpDir)) {
220             ownTmpDir = new File(tmpDir, "fessTmpDir_" + sessionId);
221             if (ownTmpDir.mkdirs()) {
222                 cmdList.add("-Djava.io.tmpdir=" + ownTmpDir.getAbsolutePath());
223             } else {
224                 ownTmpDir = null;
225             }
226         }
227 
228         if (!jvmOptions.isEmpty()) {
229             jvmOptions.stream().filter(StringUtil::isNotBlank).forEach(cmdList::add);
230         }
231 
232         cmdList.add(ThumbnailGenerator.class.getCanonicalName());
233 
234         cmdList.add("--sessionId");
235         cmdList.add(sessionId);
236         cmdList.add("--numOfThreads");
237         cmdList.add(Integer.toString(numOfThreads));
238         if (cleanup) {
239             cmdList.add("--cleanup");
240         }
241 
242         final File propFile = ComponentUtil.getSystemHelper().createTempFile(getExecuteType() + "_", ".properties");
243         try {
244             cmdList.add("-p");
245             cmdList.add(propFile.getAbsolutePath());
246             createSystemProperties(cmdList, propFile);
247 
248             final File baseDir = new File(servletContext.getRealPath("/WEB-INF")).getParentFile();
249 
250             if (logger.isInfoEnabled()) {
251                 logger.info("ThumbnailGenerator: \nDirectory={}\nOptions={}", baseDir, cmdList);
252             }
253 
254             final JobProcess jobProcess = processHelper.startProcess(sessionId, cmdList, pb -> {
255                 pb.directory(baseDir);
256                 pb.redirectErrorStream(true);
257             });
258 
259             final InputStreamThread it = jobProcess.getInputStreamThread();
260             it.start();
261 
262             final Process currentProcess = jobProcess.getProcess();
263             currentProcess.waitFor();
264             it.join(5000);
265 
266             final int exitValue = currentProcess.exitValue();
267 
268             if (logger.isInfoEnabled()) {
269                 logger.info("ThumbnailGenerator: Exit Code={} - Process Output:\n{}", exitValue, it.getOutput());
270             }
271             if (exitValue != 0) {
272                 final StringBuilder out = new StringBuilder();
273                 if (processTimeout) {
274                     out.append("Process is terminated due to ").append(timeout).append(" second exceeded.\n");
275                 }
276                 out.append("Exit Code: ").append(exitValue).append("\nOutput:\n").append(it.getOutput());
277                 throw new JobProcessingException(out.toString());
278             }
279             ComponentUtil.getPopularWordHelper().clearCache();
280         } catch (final JobProcessingException e) {
281             throw e;
282         } catch (final Exception e) {
283             throw new JobProcessingException("ThumbnailGenerator Process terminated.", e);
284         } finally {
285             try {
286                 processHelper.destroyProcess(sessionId);
287             } finally {
288                 if (propFile != null && !propFile.delete()) {
289                     logger.warn("Failed to delete properties file: {}", propFile.getAbsolutePath());
290                 }
291                 deleteTempDir(ownTmpDir);
292             }
293         }
294     }
295 
296     @Override
297     protected String getExecuteType() {
298         return Constants.EXECUTE_TYPE_THUMBNAIL;
299     }
300 }