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.exec;
17  
18  import java.io.File;
19  import java.lang.management.ManagementFactory;
20  import java.util.concurrent.ExecutorService;
21  import java.util.concurrent.Executors;
22  import java.util.concurrent.TimeUnit;
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.core.misc.DynamicProperties;
28  import org.codelibs.core.timer.TimeoutManager;
29  import org.codelibs.core.timer.TimeoutTask;
30  import org.codelibs.fess.Constants;
31  import org.codelibs.fess.crawler.client.FesenClient;
32  import org.codelibs.fess.exception.ContainerNotAvailableException;
33  import org.codelibs.fess.opensearch.client.SearchEngineClient;
34  import org.codelibs.fess.timer.LogNotificationTarget;
35  import org.codelibs.fess.timer.SystemMonitorTarget;
36  import org.codelibs.fess.util.ComponentUtil;
37  import org.codelibs.fess.util.SystemUtil;
38  import org.kohsuke.args4j.CmdLineException;
39  import org.kohsuke.args4j.CmdLineParser;
40  import org.kohsuke.args4j.Option;
41  import org.lastaflute.di.core.external.GenericExternalContext;
42  import org.lastaflute.di.core.external.GenericExternalContextComponentDefRegister;
43  import org.lastaflute.di.core.factory.SingletonLaContainerFactory;
44  import org.opensearch.monitor.jvm.JvmInfo;
45  import org.opensearch.monitor.os.OsProbe;
46  import org.opensearch.monitor.process.ProcessProbe;
47  
48  import jakarta.annotation.Resource;
49  
50  /**
51   * Executable class for generating thumbnails for indexed documents.
52   * This class provides a command-line interface for thumbnail generation tasks.
53   */
54  public class ThumbnailGenerator {
55  
56      private static final Logger logger = LogManager.getLogger(ThumbnailGenerator.class);
57  
58      /**
59       * OpenSearch client for accessing indexed documents.
60       */
61      @Resource
62      public SearchEngineClient searchEngineClient;
63  
64      /**
65       * Default constructor for ThumbnailGenerator.
66       */
67      public ThumbnailGenerator() {
68          // Default constructor
69      }
70  
71      /**
72       * Command-line options for the thumbnail generator.
73       */
74      protected static class Options {
75          /**
76           * Session ID for filtering thumbnail generation.
77           */
78          @Option(name = "-s", aliases = "--sessionId", metaVar = "sessionId", usage = "Session ID")
79          protected String sessionId;
80  
81          /**
82           * Name identifier for the thumbnail generation task.
83           */
84          @Option(name = "-n", aliases = "--name", metaVar = "name", usage = "Name")
85          protected String name;
86  
87          /**
88           * Path to the properties file for configuration.
89           */
90          @Option(name = "-p", aliases = "--properties", metaVar = "properties", usage = "Properties File")
91          protected String propertiesPath;
92  
93          /**
94           * Number of threads to use for thumbnail generation.
95           */
96          @Option(name = "-t", aliases = "--numOfThreads", metaVar = "numOfThreads", usage = "The number of threads")
97          protected int numOfThreads = 1;
98  
99          /**
100          * Whether to run in cleanup mode to remove old thumbnails.
101          */
102         @Option(name = "-c", aliases = "--cleanup", usage = "Clean-Up mode")
103         protected boolean cleanup;
104 
105         /**
106          * Default constructor for command-line options.
107          */
108         protected Options() {
109             // nothing
110         }
111 
112         @Override
113         public String toString() {
114             return "Options [sessionId=" + sessionId + ", name=" + name + ", propertiesPath=" + propertiesPath + ", numOfThreads="
115                     + numOfThreads + "]";
116         }
117 
118     }
119 
120     static void initializeProbes() {
121         // Force probes to be loaded
122         ProcessProbe.getInstance();
123         OsProbe.getInstance();
124         JvmInfo.jvmInfo();
125     }
126 
127     /**
128      * Main entry point for the thumbnail generator application.
129      *
130      * @param args command-line arguments
131      */
132     public static void main(final String[] args) {
133         final Options options = new Options();
134 
135         final CmdLineParser parser = new CmdLineParser(options);
136         try {
137             parser.parseArgument(args);
138         } catch (final CmdLineException e) {
139             System.err.println(e.getMessage());
140             System.err.println("java " + ThumbnailGenerator.class.getCanonicalName() + " [options...] arguments...");
141             parser.printUsage(System.err);
142             return;
143         }
144 
145         if (logger.isDebugEnabled()) {
146             try {
147                 ManagementFactory.getRuntimeMXBean().getInputArguments().stream().forEach(s -> logger.debug("Parameter: {}", s));
148                 System.getProperties()
149                         .entrySet()
150                         .stream()
151                         .forEach(e -> logger.debug("Property: {}={}", e.getKey(),
152                                 SystemUtil.maskSensitiveValue(String.valueOf(e.getKey()), String.valueOf(e.getValue()))));
153                 System.getenv()
154                         .entrySet()
155                         .forEach(e -> logger.debug("Env: {}={}", e.getKey(), SystemUtil.maskSensitiveValue(e.getKey(), e.getValue())));
156                 logger.debug("Options: options={}", options);
157             } catch (final Exception e) {
158                 // ignore
159             }
160         }
161 
162         final String httpAddress = SystemUtil.getSearchEngineHttpAddress();
163         if (StringUtil.isNotBlank(httpAddress)) {
164             System.setProperty(FesenClient.HTTP_ADDRESS, httpAddress);
165         }
166 
167         TimeoutTask systemMonitorTask = null;
168         TimeoutTask logNotificationTask = null;
169         LogNotificationTarget logNotificationTarget = null;
170         int exitCode;
171         try {
172             SingletonLaContainerFactory.setConfigPath("app.xml");
173             SingletonLaContainerFactory.setExternalContext(new GenericExternalContext());
174             SingletonLaContainerFactory.setExternalContextComponentDefRegister(new GenericExternalContextComponentDefRegister());
175             SingletonLaContainerFactory.init();
176 
177             final Thread shutdownCallback = new Thread("ShutdownHook") {
178                 @Override
179                 public void run() {
180                     if (logger.isDebugEnabled()) {
181                         logger.debug("Destroying LaContainer...");
182                     }
183                     destroyContainer();
184                 }
185             };
186             Runtime.getRuntime().addShutdownHook(shutdownCallback);
187 
188             systemMonitorTask = TimeoutManager.getInstance()
189                     .addTimeoutTarget(new SystemMonitorTarget(), ComponentUtil.getFessConfig().getThumbnailSystemMonitorIntervalAsInteger(),
190                             true);
191 
192             if (ComponentUtil.getFessConfig().isLogNotificationEnabled()) {
193                 logNotificationTarget = new LogNotificationTarget();
194                 logNotificationTask = TimeoutManager.getInstance()
195                         .addTimeoutTarget(logNotificationTarget, ComponentUtil.getFessConfig().getLogNotificationFlushIntervalAsInteger(),
196                                 true);
197             }
198 
199             final int totalCount = process(options);
200             if (totalCount != 0) {
201                 logger.info("Processed {} thumbnail task(s).", totalCount);
202             } else {
203                 logger.info("No thumbnail tasks to process.");
204             }
205             exitCode = 0;
206         } catch (final ContainerNotAvailableException e) {
207             if (logger.isDebugEnabled()) {
208                 logger.debug("ThumbnailGenerator is stopped.", e);
209             } else if (logger.isInfoEnabled()) {
210                 logger.info("ThumbnailGenerator is stopped.");
211             }
212             exitCode = Constants.EXIT_FAIL;
213         } catch (final Throwable t) {
214             logger.error("ThumbnailGenerator terminated unexpectedly.", t);
215             exitCode = Constants.EXIT_FAIL;
216         } finally {
217             if (systemMonitorTask != null) {
218                 systemMonitorTask.cancel();
219             }
220             if (logNotificationTask != null) {
221                 logNotificationTask.cancel();
222             }
223             if (logNotificationTarget != null) {
224                 logNotificationTarget.flush();
225             }
226             destroyContainer();
227         }
228 
229         System.exit(exitCode);
230     }
231 
232     private static void destroyContainer() {
233         TimeoutManager.getInstance().stop();
234         synchronized (SingletonLaContainerFactory.class) {
235             SingletonLaContainerFactory.destroy();
236         }
237     }
238 
239     private static int process(final Options options) {
240         final DynamicProperties systemProperties = ComponentUtil.getSystemProperties();
241 
242         if (StringUtil.isNotBlank(options.propertiesPath)) {
243             systemProperties.reload(options.propertiesPath);
244         } else {
245             try {
246                 final File propFile = ComponentUtil.getSystemHelper().createTempFile("thumbnail_", ".properties");
247                 if (propFile.delete() && logger.isDebugEnabled()) {
248                     logger.debug("Deleted temp file: path={}", propFile.getAbsolutePath());
249                 }
250                 systemProperties.reload(propFile.getAbsolutePath());
251                 propFile.deleteOnExit();
252             } catch (final Exception e) {
253                 logger.warn("Failed to create system properties file.", e);
254             }
255         }
256 
257         int totalCount = 0;
258         int count = 1;
259         final ExecutorService executorService = Executors.newFixedThreadPool(options.numOfThreads);
260         try {
261             while (count != 0) {
262                 count = ComponentUtil.getThumbnailManager().generate(executorService, options.cleanup);
263                 totalCount += count;
264             }
265             executorService.shutdown();
266             executorService.awaitTermination(60, TimeUnit.SECONDS);
267         } catch (final InterruptedException e) {
268             if (logger.isDebugEnabled()) {
269                 logger.debug("Interrupted.", e);
270             }
271         } finally {
272             executorService.shutdownNow();
273         }
274         return totalCount;
275     }
276 }