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.helper;
17  
18  import static org.codelibs.core.stream.StreamUtil.split;
19  import static org.codelibs.core.stream.StreamUtil.stream;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.UnsupportedEncodingException;
25  import java.net.InetAddress;
26  import java.net.URLEncoder;
27  import java.net.UnknownHostException;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.time.Instant;
31  import java.time.LocalDateTime;
32  import java.time.ZoneId;
33  import java.util.ArrayList;
34  import java.util.Calendar;
35  import java.util.Collections;
36  import java.util.Date;
37  import java.util.HashMap;
38  import java.util.HashSet;
39  import java.util.LinkedHashMap;
40  import java.util.List;
41  import java.util.Locale;
42  import java.util.Map;
43  import java.util.Map.Entry;
44  import java.util.Properties;
45  import java.util.Set;
46  import java.util.TimeZone;
47  import java.util.UUID;
48  import java.util.concurrent.ExecutionException;
49  import java.util.concurrent.TimeUnit;
50  import java.util.concurrent.atomic.AtomicBoolean;
51  import java.util.concurrent.atomic.AtomicInteger;
52  import java.util.function.Supplier;
53  import java.util.regex.Pattern;
54  import java.util.stream.Collectors;
55  
56  import org.apache.commons.lang3.LocaleUtils;
57  import org.apache.commons.lang3.StringUtils;
58  import org.apache.logging.log4j.Level;
59  import org.apache.logging.log4j.LogManager;
60  import org.apache.logging.log4j.Logger;
61  import org.apache.logging.log4j.core.config.Configurator;
62  import org.codelibs.core.exception.IORuntimeException;
63  import org.codelibs.core.lang.StringUtil;
64  import org.codelibs.core.lang.ThreadUtil;
65  import org.codelibs.core.misc.Pair;
66  import org.codelibs.core.timer.TimeoutManager;
67  import org.codelibs.core.timer.TimeoutTask;
68  import org.codelibs.fess.Constants;
69  import org.codelibs.fess.crawler.util.CharUtil;
70  import org.codelibs.fess.exception.FessSystemException;
71  import org.codelibs.fess.mylasta.action.FessMessages;
72  import org.codelibs.fess.mylasta.action.FessUserBean;
73  import org.codelibs.fess.mylasta.direction.FessConfig;
74  import org.codelibs.fess.timer.LoadControlMonitorTarget;
75  import org.codelibs.fess.util.ComponentUtil;
76  import org.codelibs.fess.util.GsaConfigParser;
77  import org.codelibs.fess.util.IpAddressUtil;
78  import org.codelibs.fess.util.ParameterUtil;
79  import org.codelibs.fess.util.ResourceUtil;
80  import org.codelibs.fess.validation.FessActionValidator;
81  import org.lastaflute.core.message.supplier.UserMessagesCreator;
82  import org.lastaflute.web.TypicalAction;
83  import org.lastaflute.web.response.HtmlResponse;
84  import org.lastaflute.web.ruts.process.ActionRuntime;
85  import org.lastaflute.web.servlet.request.RequestManager;
86  import org.lastaflute.web.util.LaServletContextUtil;
87  import org.lastaflute.web.validation.ActionValidator;
88  import org.opensearch.monitor.os.OsProbe;
89  
90  import com.google.common.cache.CacheBuilder;
91  import com.google.common.cache.CacheLoader;
92  import com.google.common.cache.LoadingCache;
93  import com.ibm.icu.util.ULocale;
94  
95  import jakarta.annotation.PostConstruct;
96  import jakarta.annotation.PreDestroy;
97  
98  /**
99   * Helper class for system-level operations and utilities.
100  * This class provides methods for managing system properties, handling JSP files,
101  * normalizing configurations, and other system-related tasks.
102  */
103 public class SystemHelper {
104 
105     /**
106      * Constructs a new system helper.
107      */
108     public SystemHelper() {
109         // do nothing
110     }
111 
112     private static final Logger logger = LogManager.getLogger(SystemHelper.class);
113 
114     /** A map of design JSP file names. */
115     protected final Map<String, String> designJspFileNameMap = new LinkedHashMap<>();
116 
117     /** A flag to indicate if the system should be forcefully stopped. */
118     protected final AtomicBoolean forceStop = new AtomicBoolean(false);
119 
120     /** A cache for language items. */
121     protected LoadingCache<String, List<Map<String, String>>> langItemsCache;
122 
123     /** The encoding for filtering paths. */
124     protected String filterPathEncoding;
125 
126     /** An array of supported language codes. */
127     protected String[] supportedLanguages;
128 
129     /** A list of shutdown hooks to be executed on system shutdown. */
130     protected List<Runnable> shutdownHookList = new ArrayList<>();
131 
132     /** The previous state of the cluster. */
133     protected AtomicInteger previousClusterState = new AtomicInteger(0);
134 
135     /** The version of the Fess application. */
136     protected String version;
137 
138     /** The major version number. */
139     protected int majorVersion;
140 
141     /** The minor version number. */
142     protected int minorVersion;
143 
144     /** The product version string. */
145     protected String productVersion;
146 
147     /** The end-of-life timestamp for the application. */
148     protected long eolTime;
149 
150     private short systemCpuPercent;
151 
152     private long systemCpuCheckTime;
153 
154     private long systemCpuCheckInterval = 1000L;
155 
156     private volatile short searchEngineCpuPercent;
157 
158     private volatile TimeoutTask loadControlMonitorTask;
159 
160     /** A map of listeners for configuration updates. */
161     protected Map<String, Supplier<String>> updateConfigListenerMap = new HashMap<>();
162 
163     /** A set of names of threads that are currently waiting. */
164     protected Set<String> waitingThreadNames = Collections.synchronizedSet(new HashSet<>());
165 
166     /**
167      * Initializes the SystemHelper.
168      * This method sets up system properties, caches, and other initial configurations.
169      */
170     @PostConstruct
171     public void init() {
172         if (logger.isDebugEnabled()) {
173             logger.debug("Initializing {}", this.getClass().getSimpleName());
174         }
175         final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
176         cal.set(2027, 10 - 1, 1); // EOL Date
177         eolTime = cal.getTimeInMillis();
178         if (isEoled()) {
179             logger.error("Your system is out of support. See https://fess.codelibs.org/eol.html");
180         }
181         updateSystemProperties();
182         final FessConfig fessConfig = ComponentUtil.getFessConfig();
183         filterPathEncoding = fessConfig.getPathEncoding();
184         supportedLanguages = fessConfig.getSupportedLanguagesAsArray();
185         langItemsCache = CacheBuilder.newBuilder()
186                 .maximumSize(20)
187                 .expireAfterAccess(1, TimeUnit.HOURS)
188                 .build(new CacheLoader<String, List<Map<String, String>>>() {
189                     @Override
190                     public List<Map<String, String>> load(final String key) throws Exception {
191                         final ULocale uLocale = new ULocale(key);
192                         final Locale displayLocale = uLocale.toLocale();
193                         final List<Map<String, String>> langItems = new ArrayList<>(supportedLanguages.length);
194                         final String msg = ComponentUtil.getMessageManager().getMessage(displayLocale, "labels.allLanguages");
195                         final Map<String, String> defaultMap = new HashMap<>(2);
196                         defaultMap.put(Constants.ITEM_LABEL, msg);
197                         defaultMap.put(Constants.ITEM_VALUE, "all");
198                         langItems.add(defaultMap);
199 
200                         for (final String lang : supportedLanguages) {
201                             final Locale locale = LocaleUtils.toLocale(lang);
202                             final String label = locale.getDisplayName(displayLocale);
203                             final Map<String, String> map = new HashMap<>(2);
204                             map.put(Constants.ITEM_LABEL, label);
205                             map.put(Constants.ITEM_VALUE, lang);
206                             langItems.add(map);
207                         }
208                         return langItems;
209                     }
210                 });
211 
212         ComponentUtil.doInitProcesses(Runnable::run);
213 
214         parseProjectProperties(ResourceUtil.getProjectPropertiesFile());
215 
216         updateConfigListenerMap.put("Label", () -> Integer.toString(ComponentUtil.getLabelTypeHelper().load()));
217         updateConfigListenerMap.put("PathMapping", () -> Integer.toString(ComponentUtil.getPathMappingHelper().load()));
218         updateConfigListenerMap.put("RelatedContent", () -> Integer.toString(ComponentUtil.getRelatedContentHelper().load()));
219         updateConfigListenerMap.put("RelatedQuery", () -> Integer.toString(ComponentUtil.getRelatedQueryHelper().load()));
220         updateConfigListenerMap.put("KeyMatch", () -> Integer.toString(ComponentUtil.getKeyMatchHelper().load()));
221     }
222 
223     /**
224      * Parses project properties from the given path.
225      *
226      * @param propPath The path to the project properties file.
227      * @throws FessSystemException if the properties file cannot be parsed.
228      */
229     protected void parseProjectProperties(final Path propPath) {
230         try (final InputStream in = Files.newInputStream(propPath)) {
231             final Properties prop = new Properties();
232             prop.load(in);
233             version = prop.getProperty("fess.version", "0.0.0");
234             final String[] values = version.split("\\.");
235             majorVersion = Integer.parseInt(values[0]);
236             minorVersion = Integer.parseInt(values[1]);
237             productVersion = majorVersion + "." + minorVersion;
238             System.setProperty("fess.version", version);
239             System.setProperty("fess.product.version", productVersion);
240         } catch (final Exception e) {
241             throw new FessSystemException("Failed to parse project.properties.", e);
242         }
243     }
244 
245     /**
246      * Destroys the SystemHelper and executes shutdown hooks.
247      */
248     @PreDestroy
249     public void destroy() {
250         shutdownHookList.forEach(action -> {
251             try {
252                 action.run();
253             } catch (final Exception e) {
254                 logger.warn("Failed to process shutdown task.", e);
255             }
256         });
257     }
258 
259     /**
260      * Gets the username of the current user.
261      *
262      * @return The username, or "guest" if not logged in.
263      */
264     public String getUsername() {
265         return getRequestManager().findUserBean(FessUserBean.class).map(FessUserBean::getUserId).orElse(Constants.GUEST_USER);
266     }
267 
268     /**
269      * Gets the request manager.
270      *
271      * @return The request manager.
272      */
273     protected RequestManager getRequestManager() {
274         return ComponentUtil.getRequestManager();
275     }
276 
277     /**
278      * Gets the current time as a Date object.
279      *
280      * @return The current time.
281      */
282     public Date getCurrentTime() {
283         return new Date(getCurrentTimeAsLong());
284     }
285 
286     /**
287      * Gets the current time in milliseconds.
288      *
289      * @return The current time in milliseconds.
290      */
291     public long getCurrentTimeAsLong() {
292         return System.currentTimeMillis();
293     }
294 
295     /**
296      * Gets the current time as a LocalDateTime object.
297      *
298      * @return The current time.
299      */
300     public LocalDateTime getCurrentTimeAsLocalDateTime() {
301         final Instant instant = Instant.ofEpochMilli(getCurrentTimeAsLong());
302         return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
303     }
304 
305     /**
306      * Gets the path to the log file.
307      *
308      * @return The log file path.
309      */
310     public String getLogFilePath() {
311         final String value = System.getProperty("fess.log.path");
312         if (value != null) {
313             return value;
314         }
315         final String userDir = System.getProperty("user.dir");
316         final File targetDir = new File(userDir, "target");
317         return new File(targetDir, "logs").getAbsolutePath();
318     }
319 
320     /**
321      * Encodes a URL path for filtering.
322      *
323      * @param path The path to encode.
324      * @return The encoded path.
325      */
326     public String encodeUrlFilter(final String path) {
327         if (filterPathEncoding == null || path == null) {
328             return path;
329         }
330 
331         try {
332             final StringBuilder buf = new StringBuilder(path.length() + 100);
333             for (int i = 0; i < path.length(); i++) {
334                 final char c = path.charAt(i);
335                 if (CharUtil.isUrlChar(c) || c == '^' || c == '{' || c == '}' || c == '|' || c == '\\') {
336                     buf.append(c);
337                 } else {
338                     buf.append(URLEncoder.encode(String.valueOf(c), filterPathEncoding));
339                 }
340             }
341             return buf.toString();
342         } catch (final UnsupportedEncodingException e) {
343             return path;
344         }
345     }
346 
347     /**
348      * Normalizes a configuration path.
349      *
350      * @param path The path to normalize.
351      * @return The normalized path.
352      */
353     public String normalizeConfigPath(final String path) {
354 
355         if (StringUtil.isBlank(path)) {
356             return StringUtils.EMPTY;
357         }
358 
359         final String p = path.trim();
360         if (p.startsWith("#")) {
361             return StringUtils.EMPTY;
362         }
363 
364         if (p.startsWith(GsaConfigParser.CONTAINS)) {
365             return ".*" + Pattern.quote(p.substring(GsaConfigParser.CONTAINS.length())) + ".*";
366         }
367 
368         if (p.startsWith(GsaConfigParser.REGEXP)) {
369             return p.substring(GsaConfigParser.REGEXP.length());
370         }
371 
372         if (p.startsWith(GsaConfigParser.REGEXP_CASE)) {
373             return p.substring(GsaConfigParser.REGEXP_CASE.length());
374         }
375 
376         if (p.startsWith(GsaConfigParser.REGEXP_IGNORE_CASE)) {
377             return "(?i)" + p.substring(GsaConfigParser.REGEXP_IGNORE_CASE.length());
378         }
379 
380         return p;
381     }
382 
383     /**
384      * Gets the link to the Fess forum.
385      *
386      * @return The forum link.
387      */
388     public String getForumLink() {
389         final String url = ComponentUtil.getFessConfig().getForumLink();
390         if (StringUtil.isBlank(url)) {
391             return null;
392         }
393         String target = null;
394         final Locale locale = ComponentUtil.getRequestManager().getUserLocale();
395         if (locale != null) {
396             final String lang = locale.getLanguage();
397             if (ComponentUtil.getFessConfig().isOnlineHelpSupportedLang(lang)) {
398                 target = lang.toUpperCase(Locale.ROOT);
399             }
400         }
401         return url.replaceFirst("\\{lang\\}", target == null ? "EN" : target);
402     }
403 
404     /**
405      * Gets the help link for a specific page.
406      *
407      * @param name The name of the help page.
408      * @return The help link.
409      */
410     public String getHelpLink(final String name) {
411         final String url = ComponentUtil.getFessConfig().getOnlineHelpBaseLink() + name + "-guide.html";
412         return getHelpUrl(url);
413     }
414 
415     /**
416      * Gets the help URL for a given base URL.
417      *
418      * @param url The base URL.
419      * @return The localized help URL.
420      */
421     protected String getHelpUrl(final String url) {
422         final Locale locale = ComponentUtil.getRequestManager().getUserLocale();
423         if (locale != null) {
424             final String lang = locale.getLanguage();
425             if (ComponentUtil.getFessConfig().isOnlineHelpSupportedLang(lang)) {
426                 return url.replaceFirst("\\{lang\\}", lang).replaceFirst("\\{version\\}", majorVersion + "." + minorVersion);
427             }
428         }
429         return getDefaultHelpLink(url);
430     }
431 
432     /**
433      * Gets the default help link for a given URL.
434      *
435      * @param url The URL.
436      * @return The default help link.
437      */
438     protected String getDefaultHelpLink(final String url) {
439         return url.replaceFirst("/\\{lang\\}/", "/").replaceFirst("\\{version\\}", majorVersion + "." + minorVersion);
440     }
441 
442     /**
443      * Adds a design JSP file name to the map.
444      *
445      * @param key   The key for the JSP file.
446      * @param value The file name.
447      */
448     public void addDesignJspFileName(final String key, final String value) {
449         designJspFileNameMap.put(key, value);
450     }
451 
452     /**
453      * Gets the design JSP file name for a given key.
454      *
455      * @param fileName The key for the JSP file.
456      * @return The file name.
457      */
458     public String getDesignJspFileName(final String fileName) {
459         return designJspFileNameMap.get(fileName);
460     }
461 
462     /**
463      * Gets an array of design JSP file names.
464      *
465      * @return An array of pairs of keys and file names.
466      */
467     @SuppressWarnings("unchecked")
468     public Pair<String, String>[] getDesignJspFileNames() {
469         return designJspFileNameMap.entrySet().stream().map(e -> new Pair<>(e.getKey(), e.getValue())).toArray(n -> new Pair[n]);
470     }
471 
472     /**
473      * Refreshes the design JSP files for all virtual hosts.
474      *
475      * @return A list of paths to the refreshed files.
476      */
477     public List<Path> refreshDesignJspFiles() {
478         final List<Path> fileList = new ArrayList<>();
479         stream(ComponentUtil.getVirtualHostHelper().getVirtualHostPaths())
480                 .of(stream -> stream.filter(s -> s != null && !"/".equals(s)).forEach(key -> {
481                     designJspFileNameMap.entrySet().stream().forEach(e -> {
482                         final File jspFile = getDesignJspFile("/WEB-INF/view" + key + "/" + e.getValue());
483                         if (!jspFile.exists()) {
484                             jspFile.getParentFile().mkdirs();
485                             final File baseJspFile = getDesignJspFile("/WEB-INF/view/" + e.getValue());
486                             try {
487                                 final Path jspPath = jspFile.toPath();
488                                 Files.copy(baseJspFile.toPath(), jspPath);
489                                 fileList.add(jspPath);
490                             } catch (final IOException ex) {
491                                 logger.warn("Could not copy from {} to {}", baseJspFile.getAbsolutePath(), jspFile.getAbsolutePath(), ex);
492                             }
493                         }
494                     });
495                 }));
496         return fileList;
497     }
498 
499     /**
500      * Gets a design JSP file for a given path.
501      *
502      * @param path The path to the JSP file.
503      * @return The JSP file.
504      */
505     protected File getDesignJspFile(final String path) {
506         return new File(LaServletContextUtil.getServletContext().getRealPath(path));
507     }
508 
509     /**
510      * Checks if the system is in a force-stop state.
511      *
512      * @return true if the system is force-stopping, false otherwise.
513      */
514     public boolean isForceStop() {
515         return forceStop.get();
516     }
517 
518     /**
519      * Sets the force-stop state of the system.
520      *
521      * @param b true to force-stop the system.
522      */
523     public void setForceStop(final boolean b) {
524         forceStop.set(b);
525     }
526 
527     /**
528      * Generates a document ID.
529      *
530      * @param map A map of data for the document.
531      * @return A unique document ID.
532      */
533     public String generateDocId(final Map<String, Object> map) {
534         return UUID.randomUUID().toString().replace("-", StringUtil.EMPTY);
535     }
536 
537     /**
538      * Abbreviates a long text string.
539      *
540      * @param str The string to abbreviate.
541      * @return The abbreviated string.
542      */
543     public String abbreviateLongText(final String str) {
544         return StringUtils.abbreviate(str, ComponentUtil.getFessConfig().getMaxLogOutputLengthAsInteger());
545     }
546 
547     /**
548      * Normalizes an HTML language string.
549      *
550      * @param value The language string to normalize.
551      * @return The normalized language string.
552      */
553     public String normalizeHtmlLang(final String value) {
554         final String defaultLang = ComponentUtil.getFessConfig().getCrawlerDocumentHtmlDefaultLang();
555         if (StringUtil.isNotBlank(defaultLang)) {
556             return defaultLang;
557         }
558 
559         return normalizeLang(value);
560     }
561 
562     /**
563      * Normalizes a language string.
564      *
565      * @param value The language string to normalize.
566      * @return The normalized language string.
567      */
568     public String normalizeLang(final String value) {
569         if (StringUtil.isBlank(value)) {
570             return null;
571         }
572 
573         final String localeName = value.trim().toLowerCase(Locale.ENGLISH).replace("-", "_");
574 
575         for (final String supportedLang : supportedLanguages) {
576             if (localeName.startsWith(supportedLang.toLowerCase(Locale.ENGLISH))) {
577                 return supportedLang;
578             }
579         }
580         return null;
581     }
582 
583     /**
584      * Gets a list of language items for a given locale.
585      *
586      * @param locale The locale.
587      * @return A list of language items.
588      */
589     public List<Map<String, String>> getLanguageItems(final Locale locale) {
590         try {
591             final String localeStr = locale.toString();
592             return langItemsCache.get(localeStr);
593         } catch (final ExecutionException e) {
594             final List<Map<String, String>> langItems = new ArrayList<>(supportedLanguages.length);
595             final String msg = ComponentUtil.getMessageManager().getMessage(locale, "labels.allLanguages");
596             final Map<String, String> defaultMap = new HashMap<>(2);
597             defaultMap.put(Constants.ITEM_LABEL, msg);
598             defaultMap.put(Constants.ITEM_VALUE, "all");
599             langItems.add(defaultMap);
600             return langItems;
601         }
602     }
603 
604     /**
605      * Adds a shutdown hook.
606      *
607      * @param hook The shutdown hook to add.
608      */
609     public void addShutdownHook(final Runnable hook) {
610         shutdownHookList.add(hook);
611     }
612 
613     /**
614      * Gets the hostname of the local machine.
615      *
616      * @return The hostname.
617      */
618     public String getHostname() {
619         final Map<String, String> env = getEnvMap();
620         if (env.containsKey("COMPUTERNAME")) {
621             return env.get("COMPUTERNAME");
622         }
623         if (env.containsKey("HOSTNAME")) {
624             return env.get("HOSTNAME");
625         }
626         try {
627             return IpAddressUtil.getUrlHost(InetAddress.getLocalHost());
628         } catch (final UnknownHostException e) {
629             logger.debug("Unknown hostname.", e);
630         }
631         return "Unknown";
632     }
633 
634     /**
635      * Gets the instance ID for this Fess instance.
636      * Combines scheduler.target.name (if configured), hostname, and PID
637      * to ensure uniqueness across multiple JVMs on the same host.
638      *
639      * @return The instance ID.
640      */
641     public String getInstanceId() {
642         final String targetName = ComponentUtil.getFessConfig().getSchedulerTargetName();
643         final String hostname = getHostname();
644         final long pid = ProcessHandle.current().pid();
645         if (StringUtil.isNotBlank(targetName)) {
646             return targetName + "@" + hostname + ":" + pid;
647         }
648         return hostname + ":" + pid;
649     }
650 
651     /**
652      * Sets up administrative HTML data for a given action.
653      *
654      * @param action  The action to set up data for.
655      * @param runtime The action runtime.
656      */
657     public void setupAdminHtmlData(final TypicalAction action, final ActionRuntime runtime) {
658         runtime.registerData("developmentMode", ComponentUtil.getSearchEngineClient().isEmbedded());
659         final FessConfig fessConfig = ComponentUtil.getFessConfig();
660         final String installationLink = fessConfig.getOnlineHelpInstallation();
661         runtime.registerData("installationLink", getHelpUrl(installationLink));
662         runtime.registerData("storageEnabled",
663                 StringUtil.isNotBlank(fessConfig.getStorageEndpoint()) && StringUtil.isNotBlank(fessConfig.getStorageBucket()));
664         final boolean eoled = isEoled();
665         runtime.registerData("eoled", eoled);
666         if (eoled) {
667             final String eolLink = fessConfig.getOnlineHelpEol();
668             runtime.registerData("eolLink", getHelpUrl(eolLink));
669         }
670     }
671 
672     /**
673      * Sets up search HTML data for a given action.
674      *
675      * @param action  The action to set up data for.
676      * @param runtime The action runtime.
677      */
678     public void setupSearchHtmlData(final TypicalAction action, final ActionRuntime runtime) {
679         runtime.registerData("developmentMode", ComponentUtil.getSearchEngineClient().isEmbedded());
680         final FessConfig fessConfig = ComponentUtil.getFessConfig();
681         final String installationLink = fessConfig.getOnlineHelpInstallation();
682         runtime.registerData("installationLink", getHelpUrl(installationLink));
683         final boolean eoled = isEoled();
684         runtime.registerData("eoled", eoled);
685         if (eoled) {
686             final String eolLink = fessConfig.getOnlineHelpEol();
687             runtime.registerData("eolLink", getHelpUrl(eolLink));
688         }
689     }
690 
691     /**
692      * Checks if the application has reached its end-of-life.
693      *
694      * @return true if the application is EOL, false otherwise.
695      */
696     protected boolean isEoled() {
697         return getCurrentTimeAsLong() > eolTime;
698     }
699 
700     /**
701      * Checks if a permission string is a user permission.
702      *
703      * @param permission The permission string.
704      * @return true if it is a user permission, false otherwise.
705      */
706     public boolean isUserPermission(final String permission) {
707         if (StringUtil.isNotBlank(permission)) {
708             return permission.startsWith(ComponentUtil.getFessConfig().getRoleSearchUserPrefix());
709         }
710         return false;
711     }
712 
713     /**
714      * Gets the search role for a given user.
715      *
716      * @param name The username.
717      * @return The search role.
718      */
719     public String getSearchRoleByUser(final String name) {
720         return createSearchRole(ComponentUtil.getFessConfig().getRoleSearchUserPrefix(), name);
721     }
722 
723     /**
724      * Gets the search role for a given group.
725      *
726      * @param name The group name.
727      * @return The search role.
728      */
729     public String getSearchRoleByGroup(final String name) {
730         return createSearchRole(ComponentUtil.getFessConfig().getRoleSearchGroupPrefix(), name);
731     }
732 
733     /**
734      * Gets the search role for a given role.
735      *
736      * @param name The role name.
737      * @return The search role.
738      */
739     public String getSearchRoleByRole(final String name) {
740         return createSearchRole(ComponentUtil.getFessConfig().getRoleSearchRolePrefix(), name);
741     }
742 
743     /**
744      * Creates a search role string.
745      *
746      * @param type The type of the role.
747      * @param name The name of the role.
748      * @return The search role string.
749      */
750     protected String createSearchRole(final String type, final String name) {
751         final String value = type + ComponentUtil.getFessConfig().getCanonicalLdapName(name);
752         if (logger.isDebugEnabled()) {
753             logger.debug("Search Role: {}:{}={}", type, name, value);
754         }
755         return value;
756     }
757 
758     /**
759      * Reloads the application configuration.
760      */
761     public void reloadConfiguration() {
762         reloadConfiguration(true);
763     }
764 
765     /**
766      * Reloads the application configuration.
767      *
768      * @param resetJobs true to reset scheduled jobs.
769      */
770     public void reloadConfiguration(final boolean resetJobs) {
771         ComponentUtil.getSearchEngineClient().refresh();
772 
773         ComponentUtil.getSuggestHelper().init();
774         ComponentUtil.getPopularWordHelper().init();
775 
776         ComponentUtil.getLabelTypeHelper().update();
777         ComponentUtil.getPathMappingHelper().update();
778         ComponentUtil.getRelatedContentHelper().update();
779         ComponentUtil.getRelatedQueryHelper().update();
780         ComponentUtil.getKeyMatchHelper().update();
781 
782         ComponentUtil.getLdapManager().updateConfig();
783         if (resetJobs) {
784             ComponentUtil.getJobManager().reboot();
785         }
786         updateSystemProperties();
787 
788         ComponentUtil.getRankFusionProcessor().update();
789     }
790 
791     /**
792      * Updates system properties from the application configuration.
793      */
794     public void updateSystemProperties() {
795         final String value = ComponentUtil.getFessConfig().getAppValue();
796         if (logger.isDebugEnabled()) {
797             logger.debug("system.properties: {}", value);
798         }
799         if (StringUtil.isNotBlank(value)) {
800             ParameterUtil.parse(ParameterUtil.encrypt(value)).entrySet().stream().filter(e -> {
801                 final String key = e.getKey();
802                 if (StringUtil.isBlank(key)) {
803                     return false;
804                 }
805                 if (key.startsWith("fess.")) {
806                     return true;
807                 }
808                 return System.getProperty(key) == null;
809             }).forEach(e -> {
810                 if (logger.isDebugEnabled()) {
811                     logger.debug("system.properties: setProperty({}, {})", e.getKey(), e.getValue());
812                 }
813                 System.setProperty(e.getKey(), e.getValue());
814             });
815         }
816     }
817 
818     /**
819      * Updates the application configuration.
820      *
821      * @return A string containing the results of the update.
822      */
823     public String updateConfiguration() {
824         final StringBuilder buf = new StringBuilder();
825         updateConfigListenerMap.entrySet().stream().forEach(e -> {
826             buf.append(e.getKey()).append(": ");
827             try {
828                 buf.append(e.getValue().get());
829             } catch (final Exception ex) {
830                 logger.warn("Failed to process {} task.", e.getKey(), ex);
831                 buf.append(ex.getMessage());
832             }
833             buf.append('\n');
834         });
835         return buf.toString();
836     }
837 
838     /**
839      * Adds a listener for configuration updates.
840      *
841      * @param name     The name of the listener.
842      * @param listener The listener to add.
843      */
844     public void addUpdateConfigListener(final String name, final Supplier<String> listener) {
845         updateConfigListenerMap.put(name, listener);
846     }
847 
848     /**
849      * Checks if the cluster state has changed.
850      *
851      * @param status The current cluster status.
852      * @return true if the state has changed, false otherwise.
853      */
854     public boolean isChangedClusterState(final int status) {
855         return previousClusterState.getAndSet(status) != status;
856     }
857 
858     /**
859      * Creates a new action validator.
860      *
861      * @param requestManager  The request manager.
862      * @param messagesCreator The messages creator.
863      * @param runtimeGroups   The runtime groups.
864      * @return A new action validator.
865      */
866     public ActionValidator<FessMessages> createValidator(final RequestManager requestManager,
867             final UserMessagesCreator<FessMessages> messagesCreator, final Class<?>[] runtimeGroups) {
868         return new FessActionValidator<>(requestManager, messagesCreator, runtimeGroups);
869     }
870 
871     /**
872      * Gets the redirect response to the login page.
873      *
874      * @param response The original response.
875      * @return The redirect response.
876      */
877     public HtmlResponse getRedirectResponseToLogin(final HtmlResponse response) {
878         return response;
879     }
880 
881     /**
882      * Gets the redirect response to the root page.
883      *
884      * @param response The original response.
885      * @return The redirect response.
886      */
887     public HtmlResponse getRedirectResponseToRoot(final HtmlResponse response) {
888         return response;
889     }
890 
891     /**
892      * Sets the log level for the application.
893      *
894      * @param level The log level to set.
895      */
896     public void setLogLevel(final String level) {
897         final Level logLevel = Level.toLevel(level, Level.WARN);
898         System.setProperty(Constants.FESS_LOG_LEVEL, logLevel.toString());
899         split(ComponentUtil.getFessConfig().getLoggingAppPackages(), ",")
900                 .of(stream -> stream.map(String::trim).filter(StringUtil::isNotEmpty).forEach(s -> Configurator.setLevel(s, logLevel)));
901     }
902 
903     /**
904      * Gets the current log level.
905      *
906      * @return The current log level.
907      */
908     public String getLogLevel() {
909         return System.getProperty(Constants.FESS_LOG_LEVEL, Level.WARN.toString());
910     }
911 
912     private static final String[] LLM_LOG_PACKAGES =
913             { "org.codelibs.fess.llm", "org.codelibs.fess.chat", "org.codelibs.fess.api.chat", "org.codelibs.fess.app.web.chat" };
914 
915     /**
916      * Sets the log level for LLM-related packages.
917      *
918      * @param level the log level to set
919      */
920     public void setLlmLogLevel(final String level) {
921         final Level logLevel = Level.toLevel(level, Level.INFO);
922         System.setProperty(Constants.FESS_LLM_LOG_LEVEL, logLevel.toString());
923         for (final String pkg : LLM_LOG_PACKAGES) {
924             Configurator.setLevel(pkg, logLevel);
925         }
926     }
927 
928     /**
929      * Returns the current log level for LLM-related packages.
930      *
931      * @return the LLM log level
932      */
933     public String getLlmLogLevel() {
934         return System.getProperty(Constants.FESS_LLM_LOG_LEVEL, Level.INFO.toString());
935     }
936 
937     /**
938      * Creates a temporary file.
939      *
940      * @param prefix The prefix for the file name.
941      * @param suffix The suffix for the file name.
942      * @return The created temporary file.
943      * @throws IORuntimeException if the file cannot be created.
944      */
945     public File createTempFile(final String prefix, final String suffix) {
946         try {
947             final File file = File.createTempFile(prefix, suffix);
948             file.setReadable(false, false);
949             file.setReadable(true, true);
950             file.setWritable(false, false);
951             file.setWritable(true, true);
952             if (logger.isDebugEnabled()) {
953                 logger.debug("Create {} as a temp file.", file.getAbsolutePath());
954             }
955             return file;
956         } catch (final IOException e) {
957             throw new IORuntimeException(e);
958         }
959     }
960 
961     /**
962      * Calibrates the CPU load.
963      *
964      * @return true if the CPU load is within the acceptable range, false otherwise.
965      */
966     public boolean calibrateCpuLoad() {
967         return calibrateCpuLoad(0L);
968     }
969 
970     /**
971      * Calibrates the CPU load with a timeout.
972      *
973      * @param timeoutInMillis The timeout in milliseconds.
974      * @return true if the CPU load is within the acceptable range, false otherwise.
975      */
976     public boolean calibrateCpuLoad(final long timeoutInMillis) {
977         final short percent = ComponentUtil.getFessConfig().getAdaptiveLoadControlAsInteger().shortValue();
978         if (percent <= 0) {
979             return true;
980         }
981         short current = getSystemCpuPercent();
982         if (current < percent) {
983             return true;
984         }
985         final long startTime = getCurrentTimeAsLong();
986         final String threadName = Thread.currentThread().getName();
987         try {
988             waitingThreadNames.add(threadName);
989             while (current >= percent) {
990                 if (timeoutInMillis > 0 && getCurrentTimeAsLong() - startTime > timeoutInMillis) {
991                     if (logger.isInfoEnabled()) {
992                         logger.info("Cpu Load {}% is greater than {}%. {} waiting thread(s). {} thread is timed out.", current, percent,
993                                 waitingThreadNames.size(), threadName);
994                     }
995                     return false;
996                 }
997                 if (logger.isInfoEnabled()) {
998                     logger.info("Cpu Load {}% is greater than {}%. {} waiting thread(s).", current, percent, waitingThreadNames.size());
999                 }
1000                 if (logger.isDebugEnabled()) {
1001                     logger.debug("Waiting threads: {}", waitingThreadNames);
1002                 }
1003                 ThreadUtil.sleep(systemCpuCheckInterval);
1004                 current = getSystemCpuPercent();
1005             }
1006         } finally {
1007             waitingThreadNames.remove(threadName);
1008         }
1009         return true;
1010     }
1011 
1012     /**
1013      * Waits for all waiting threads to complete.
1014      */
1015     public void waitForNoWaitingThreads() {
1016         int count = waitingThreadNames.size();
1017         while (count > 0) {
1018             if (logger.isInfoEnabled()) {
1019                 logger.info("{} waiting thread(s).", count);
1020             }
1021             ThreadUtil.sleep(systemCpuCheckInterval);
1022             count = waitingThreadNames.size();
1023         }
1024     }
1025 
1026     /**
1027      * Gets the system CPU usage percentage.
1028      *
1029      * @return The system CPU usage percentage.
1030      */
1031     protected short getSystemCpuPercent() {
1032         final long now = getCurrentTimeAsLong();
1033         if (now - systemCpuCheckTime > systemCpuCheckInterval) {
1034             synchronized (this) {
1035                 if (now - systemCpuCheckTime > systemCpuCheckInterval) {
1036                     try {
1037                         final OsProbe osProbe = OsProbe.getInstance();
1038                         systemCpuPercent = osProbe.getSystemCpuPercent();
1039                         if (logger.isDebugEnabled()) {
1040                             logger.debug("Updated System Cpu {}%", systemCpuPercent);
1041                         }
1042                     } catch (final Exception e) {
1043                         logger.warn("Failed to get SystemCpuPercent.", e);
1044                         return 0;
1045                     }
1046                     systemCpuCheckTime = now;
1047                 }
1048             }
1049         }
1050         return systemCpuPercent;
1051     }
1052 
1053     /**
1054      * Gets the current system CPU usage percentage.
1055      *
1056      * @return The system CPU usage percentage.
1057      */
1058     public short currentSystemCpuPercent() {
1059         return getSystemCpuPercent();
1060     }
1061 
1062     /**
1063      * Gets the search engine CPU usage percentage.
1064      *
1065      * @return The search engine CPU usage percentage.
1066      */
1067     public short getSearchEngineCpuPercent() {
1068         ensureLoadControlMonitorStarted();
1069         return searchEngineCpuPercent;
1070     }
1071 
1072     /**
1073      * Sets the search engine CPU usage percentage.
1074      *
1075      * @param percent The search engine CPU usage percentage.
1076      */
1077     public void setSearchEngineCpuPercent(final short percent) {
1078         searchEngineCpuPercent = percent;
1079     }
1080 
1081     private void ensureLoadControlMonitorStarted() {
1082         if (loadControlMonitorTask == null) {
1083             final FessConfig fessConfig = ComponentUtil.getFessConfig();
1084             if (fessConfig.getWebLoadControlAsInteger() < 100 || fessConfig.getApiLoadControlAsInteger() < 100) {
1085                 synchronized (this) {
1086                     if (loadControlMonitorTask == null) {
1087                         final int interval = fessConfig.getLoadControlMonitorIntervalAsInteger();
1088                         loadControlMonitorTask =
1089                                 TimeoutManager.getInstance().addTimeoutTarget(new LoadControlMonitorTarget(this), interval, true);
1090                     }
1091                 }
1092             }
1093         }
1094     }
1095 
1096     /**
1097      * Gets a map of filtered environment variables.
1098      *
1099      * @param keyPattern The pattern to filter environment variable keys.
1100      * @return A map of filtered environment variables.
1101      */
1102     public Map<String, String> getFilteredEnvMap(final String keyPattern) {
1103         final Pattern pattern = Pattern.compile(keyPattern);
1104         return getEnvMap().entrySet().stream().filter(e -> {
1105             final String key = e.getKey();
1106             if (StringUtil.isBlank(key)) {
1107                 return false;
1108             }
1109             return pattern.matcher(key).matches();
1110         }).collect(Collectors.toMap(Entry<String, String>::getKey, Entry<String, String>::getValue));
1111     }
1112 
1113     /**
1114      * Gets a map of environment variables.
1115      *
1116      * @return A map of environment variables.
1117      */
1118     protected Map<String, String> getEnvMap() {
1119         return System.getenv();
1120     }
1121 
1122     /**
1123      * Gets the version of the Fess application.
1124      *
1125      * @return The version string.
1126      */
1127     public String getVersion() {
1128         return version;
1129     }
1130 
1131     /**
1132      * Gets the major version number.
1133      *
1134      * @return The major version number.
1135      */
1136     public int getMajorVersion() {
1137         return majorVersion;
1138     }
1139 
1140     /**
1141      * Gets the minor version number.
1142      *
1143      * @return The minor version number.
1144      */
1145     public int getMinorVersion() {
1146         return minorVersion;
1147     }
1148 
1149     /**
1150      * Gets the product version string.
1151      *
1152      * @return The product version string.
1153      */
1154     public String getProductVersion() {
1155         return productVersion;
1156     }
1157 
1158     /**
1159      * Sets the interval for checking the system CPU load.
1160      *
1161      * @param systemCpuCheckInterval The interval in milliseconds.
1162      */
1163     public void setSystemCpuCheckInterval(final long systemCpuCheckInterval) {
1164         this.systemCpuCheckInterval = systemCpuCheckInterval;
1165     }
1166 
1167     /**
1168      * Validates a password against configured password policy requirements.
1169      *
1170      * @param password The password to validate.
1171      * @return An empty string if the password is valid, or an error key for the validation failure.
1172      */
1173     public String validatePassword(final String password) {
1174         if (StringUtil.isBlank(password)) {
1175             return "errors.blank_password";
1176         }
1177 
1178         final FessConfig fessConfig = ComponentUtil.getFessConfig();
1179 
1180         final Integer minLength = fessConfig.getPasswordMinLengthAsInteger();
1181         if (minLength != null && minLength > 0 && password.length() < minLength) {
1182             return "errors.password_length";
1183         }
1184 
1185         if (fessConfig.isPasswordRequireUppercase() && !containsUppercase(password)) {
1186             return "errors.password_no_uppercase";
1187         }
1188 
1189         if (fessConfig.isPasswordRequireLowercase() && !containsLowercase(password)) {
1190             return "errors.password_no_lowercase";
1191         }
1192 
1193         if (fessConfig.isPasswordRequireDigit() && !containsDigit(password)) {
1194             return "errors.password_no_digit";
1195         }
1196 
1197         if (fessConfig.isPasswordRequireSpecialChar() && !containsSpecialChar(password)) {
1198             return "errors.password_no_special_char";
1199         }
1200 
1201         if (!fessConfig.isValidAdminPassword(password)) {
1202             return "errors.password_is_blacklisted";
1203         }
1204 
1205         return StringUtil.EMPTY;
1206     }
1207 
1208     /**
1209      * Checks if the password contains at least one uppercase letter.
1210      *
1211      * @param password the password to check
1212      * @return true if the password contains an uppercase letter, false otherwise
1213      */
1214     protected boolean containsUppercase(final String password) {
1215         for (int i = 0; i < password.length(); i++) {
1216             if (Character.isUpperCase(password.charAt(i))) {
1217                 return true;
1218             }
1219         }
1220         return false;
1221     }
1222 
1223     /**
1224      * Checks if the password contains at least one lowercase letter.
1225      *
1226      * @param password the password to check
1227      * @return true if the password contains a lowercase letter, false otherwise
1228      */
1229     protected boolean containsLowercase(final String password) {
1230         for (int i = 0; i < password.length(); i++) {
1231             if (Character.isLowerCase(password.charAt(i))) {
1232                 return true;
1233             }
1234         }
1235         return false;
1236     }
1237 
1238     /**
1239      * Checks if the password contains at least one digit.
1240      *
1241      * @param password the password to check
1242      * @return true if the password contains a digit, false otherwise
1243      */
1244     protected boolean containsDigit(final String password) {
1245         for (int i = 0; i < password.length(); i++) {
1246             if (Character.isDigit(password.charAt(i))) {
1247                 return true;
1248             }
1249         }
1250         return false;
1251     }
1252 
1253     /**
1254      * Checks if the password contains at least one special character.
1255      * A special character is defined as any character that is not a letter, digit, or whitespace.
1256      *
1257      * @param password the password to check
1258      * @return true if the password contains a special character, false otherwise
1259      */
1260     protected boolean containsSpecialChar(final String password) {
1261         for (int i = 0; i < password.length(); i++) {
1262             final char c = password.charAt(i);
1263             if (!Character.isLetterOrDigit(c) && !Character.isWhitespace(c)) {
1264                 return true;
1265             }
1266         }
1267         return false;
1268     }
1269 }