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.app.web.admin.wizard;
17  
18  import java.io.UnsupportedEncodingException;
19  import java.net.URLEncoder;
20  import java.util.List;
21  
22  import org.apache.commons.lang3.StringUtils;
23  import org.apache.logging.log4j.LogManager;
24  import org.apache.logging.log4j.Logger;
25  import org.codelibs.core.lang.StringUtil;
26  import org.codelibs.core.misc.DynamicProperties;
27  import org.codelibs.fess.Constants;
28  import org.codelibs.fess.annotation.Secured;
29  import org.codelibs.fess.app.service.FileConfigService;
30  import org.codelibs.fess.app.service.ScheduledJobService;
31  import org.codelibs.fess.app.service.WebConfigService;
32  import org.codelibs.fess.app.web.base.FessAdminAction;
33  import org.codelibs.fess.crawler.util.CharUtil;
34  import org.codelibs.fess.helper.ProcessHelper;
35  import org.codelibs.fess.opensearch.config.exentity.FileConfig;
36  import org.codelibs.fess.opensearch.config.exentity.ScheduledJob;
37  import org.codelibs.fess.opensearch.config.exentity.WebConfig;
38  import org.codelibs.fess.util.ComponentUtil;
39  import org.lastaflute.job.JobManager;
40  import org.lastaflute.job.key.LaJobUnique;
41  import org.lastaflute.web.Execute;
42  import org.lastaflute.web.response.HtmlResponse;
43  import org.lastaflute.web.ruts.process.ActionRuntime;
44  
45  import jakarta.annotation.Resource;
46  
47  /**
48   * Admin action for configuration wizard.
49   *
50   */
51  public class AdminWizardAction extends FessAdminAction {
52  
53      /**
54       * Default constructor.
55       */
56      public AdminWizardAction() {
57          super();
58      }
59  
60      /** Role name for admin wizard operations */
61      public static final String ROLE = "admin-wizard";
62  
63      // ===================================================================================
64      //                                                                            Constant
65      //
66      private static final Logger logger = LogManager.getLogger(AdminWizardAction.class);
67  
68      // ===================================================================================
69      //                                                                           Attribute
70      //
71      /** System properties for configuration management */
72      @Resource
73      protected DynamicProperties systemProperties;
74  
75      /** Service for managing web crawler configurations */
76      @Resource
77      protected WebConfigService webConfigService;
78  
79      /** Service for managing file crawler configurations */
80      @Resource
81      protected FileConfigService fileConfigService;
82  
83      /** Helper for managing crawler processes */
84      @Resource
85      protected ProcessHelper processHelper;
86  
87      /** Service for managing scheduled jobs */
88      @Resource
89      protected ScheduledJobService scheduledJobService;
90  
91      // ===================================================================================
92      //                                                                               Hook
93      //                                                                              ======
94      @Override
95      protected void setupHtmlData(final ActionRuntime runtime) {
96          super.setupHtmlData(runtime);
97          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameWizard()));
98      }
99  
100     @Override
101     protected String getActionRole() {
102         return ROLE;
103     }
104 
105     // ===================================================================================
106     //                                                                      Search Execute
107     //                                                                      ==============
108 
109     /**
110      * Displays the wizard index page.
111      *
112      * @return HTML response for the wizard main page
113      */
114     @Execute
115     @Secured({ ROLE, ROLE + VIEW })
116     public HtmlResponse index() {
117         return asIndexHtml();
118     }
119 
120     private HtmlResponse asIndexHtml() {
121         return asHtml(path_AdminWizard_AdminWizardJsp).useForm(IndexForm.class);
122     }
123 
124     /**
125      * Displays the crawling configuration form.
126      *
127      * @return HTML response for the crawling config form
128      */
129     @Execute
130     @Secured({ ROLE })
131     public HtmlResponse crawlingConfigForm() {
132         saveToken();
133         return asHtml(path_AdminWizard_AdminWizardConfigJsp).useForm(CrawlingConfigForm.class);
134     }
135 
136     /**
137      * Creates a crawling configuration and returns to the config form.
138      *
139      * @param form the form containing crawling configuration data
140      * @return HTML response redirecting to the config form
141      */
142     @Execute
143     @Secured({ ROLE })
144     public HtmlResponse crawlingConfig(final CrawlingConfigForm form) {
145         validate(form, messages -> {}, () -> asHtml(path_AdminWizard_AdminWizardConfigJsp));
146         verifyTokenKeep(this::asIndexHtml);
147         final String name = crawlingConfigInternal(form);
148         saveInfo(messages -> messages.addSuccessCreateCrawlingConfigAtWizard(GLOBAL, name));
149         return redirectWith(getClass(), moreUrl("crawlingConfigForm"));
150     }
151 
152     /**
153      * Creates a crawling configuration and proceeds to the start crawling form.
154      *
155      * @param form the form containing crawling configuration data
156      * @return HTML response redirecting to the start crawling form
157      */
158     @Execute
159     @Secured({ ROLE })
160     public HtmlResponse crawlingConfigNext(final CrawlingConfigForm form) {
161         validate(form, messages -> {}, () -> asHtml(path_AdminWizard_AdminWizardConfigJsp));
162         verifyToken(this::asIndexHtml);
163         final String name = crawlingConfigInternal(form);
164         saveInfo(messages -> messages.addSuccessCreateCrawlingConfigAtWizard(GLOBAL, name));
165         return redirectWith(getClass(), moreUrl("startCrawlingForm"));
166     }
167 
168     /**
169      * Internal method to create crawling configuration based on form data.
170      * Determines whether to create a web or file crawler configuration.
171      *
172      * @param form the form containing crawling configuration data
173      * @return the name of the created configuration
174      */
175     protected String crawlingConfigInternal(final CrawlingConfigForm form) {
176 
177         String configName = form.crawlingConfigName;
178         String configPath = form.crawlingConfigPath.trim();
179         if (StringUtil.isBlank(configName)) {
180             configName = StringUtils.abbreviate(configPath, 30);
181         }
182 
183         // normalize
184         final StringBuilder buf = new StringBuilder(1000);
185         for (int i = 0; i < configPath.length(); i++) {
186             final char c = configPath.charAt(i);
187             if (c == '\\') {
188                 buf.append('/');
189             } else if (c == ' ') {
190                 buf.append("%20");
191             } else if (CharUtil.isUrlChar(c)) {
192                 buf.append(c);
193             } else {
194                 try {
195                     buf.append(URLEncoder.encode(String.valueOf(c), Constants.UTF_8));
196                 } catch (final UnsupportedEncodingException e) {
197                     // UTF-8 should always be supported, but log if it somehow isn't
198                     logger.warn("UTF-8 encoding not supported - this should not happen: char={}", c, e);
199                 }
200             }
201         }
202         configPath = convertCrawlingPath(buf.toString());
203 
204         final String username = systemHelper.getUsername();
205         final long now = systemHelper.getCurrentTimeAsLong();
206 
207         try {
208             if (isWebCrawlingPath(configPath)) {
209                 // web
210                 final WebConfig wConfig = new WebConfig();
211                 wConfig.setAvailable(Constants.T);
212                 wConfig.setBoost(1.0f);
213                 wConfig.setCreatedBy(username);
214                 wConfig.setCreatedTime(now);
215                 if (form.depth != null) {
216                     wConfig.setDepth(form.depth);
217                 }
218                 wConfig.setExcludedDocUrls(getDefaultString("default.config.web.excludedDocUrls", StringUtil.EMPTY));
219                 wConfig.setExcludedUrls(getDefaultString("default.config.web.excludedUrls",
220                         fessConfig.getCrawlerDocumentHtmlDefaultExcludeIndexPatterns()));
221                 wConfig.setIncludedDocUrls(getDefaultString("default.config.web.includedDocUrls", StringUtil.EMPTY));
222                 wConfig.setIncludedUrls(getDefaultString("default.config.web.includedUrls", StringUtil.EMPTY));
223                 wConfig.setIntervalTime(getDefaultInteger("default.config.web.intervalTime", Constants.DEFAULT_INTERVAL_TIME_FOR_WEB));
224                 if (form.maxAccessCount != null) {
225                     wConfig.setMaxAccessCount(form.maxAccessCount);
226                 }
227                 wConfig.setName(configName);
228                 wConfig.setNumOfThread(getDefaultInteger("default.config.web.numOfThread", Constants.DEFAULT_NUM_OF_THREAD_FOR_WEB));
229                 wConfig.setSortOrder(getDefaultInteger("default.config.web.sortOrder", 1));
230                 wConfig.setUpdatedBy(username);
231                 wConfig.setUpdatedTime(now);
232                 wConfig.setUrls(configPath);
233                 wConfig.setUserAgent(getDefaultString("default.config.web.userAgent", fessConfig.getUserAgentName()));
234                 wConfig.setPermissions(ComponentUtil.getFessConfig().getSearchDefaultDisplayEncodedPermissions());
235 
236                 webConfigService.store(wConfig);
237 
238             } else {
239                 // file
240                 final FileConfig fConfig = new FileConfig();
241                 fConfig.setAvailable(Constants.T);
242                 fConfig.setBoost(1.0f);
243                 fConfig.setCreatedBy(username);
244                 fConfig.setCreatedTime(now);
245                 if (form.depth != null) {
246                     fConfig.setDepth(form.depth);
247                 }
248                 fConfig.setExcludedDocPaths(getDefaultString("default.config.file.excludedDocPaths", StringUtil.EMPTY));
249                 fConfig.setExcludedPaths(getDefaultString("default.config.file.excludedPaths", StringUtil.EMPTY));
250                 fConfig.setIncludedDocPaths(getDefaultString("default.config.file.includedDocPaths", StringUtil.EMPTY));
251                 fConfig.setIncludedPaths(getDefaultString("default.config.file.includedPaths", StringUtil.EMPTY));
252                 fConfig.setIntervalTime(getDefaultInteger("default.config.file.intervalTime", Constants.DEFAULT_INTERVAL_TIME_FOR_FS));
253                 if (form.maxAccessCount != null) {
254                     fConfig.setMaxAccessCount(form.maxAccessCount);
255                 }
256                 fConfig.setName(configName);
257                 fConfig.setNumOfThread(getDefaultInteger("default.config.file.numOfThread", Constants.DEFAULT_NUM_OF_THREAD_FOR_FS));
258                 fConfig.setSortOrder(getDefaultInteger("default.config.file.sortOrder", 1));
259                 fConfig.setUpdatedBy(username);
260                 fConfig.setUpdatedTime(now);
261                 fConfig.setPaths(configPath);
262                 fConfig.setPermissions(ComponentUtil.getFessConfig().getSearchDefaultDisplayEncodedPermissions());
263 
264                 fileConfigService.store(fConfig);
265             }
266             return configName;
267         } catch (final Exception e) {
268             logger.warn("Failed to create crawling config: {}", form.crawlingConfigPath, e);
269             throwValidationError(messages -> messages.addErrorsFailedToCreateCrawlingConfigAtWizard(GLOBAL),
270                     () -> asHtml(path_AdminWizard_AdminWizardConfigJsp));
271             return null;
272         }
273     }
274 
275     /**
276      * Retrieves an integer value from system properties with a default fallback.
277      *
278      * @param key the property key to look up
279      * @param defaultValue the default value if the property is not found or invalid
280      * @return the integer value or default value
281      */
282     protected Integer getDefaultInteger(final String key, final Integer defaultValue) {
283         final String value = systemProperties.getProperty(key);
284         if (value != null) {
285             try {
286                 return Integer.parseInt(value);
287             } catch (final NumberFormatException e) {
288                 if (logger.isDebugEnabled()) {
289                     logger.debug("Invalid integer property value, using default: key={}, value={}, default={}", key, value, defaultValue);
290                 }
291             }
292         }
293         return defaultValue;
294     }
295 
296     /**
297      * Retrieves a long value from system properties with a default fallback.
298      *
299      * @param key the property key to look up
300      * @param defaultValue the default value if the property is not found or invalid
301      * @return the long value or default value
302      */
303     protected Long getDefaultLong(final String key, final Long defaultValue) {
304         final String value = systemProperties.getProperty(key);
305         if (value != null) {
306             try {
307                 return Long.parseLong(value);
308             } catch (final NumberFormatException e) {
309                 if (logger.isDebugEnabled()) {
310                     logger.debug("Invalid long property value, using default: key={}, value={}, default={}", key, value, defaultValue);
311                 }
312             }
313         }
314         return defaultValue;
315     }
316 
317     /**
318      * Retrieves a string value from system properties with a default fallback.
319      *
320      * @param key the property key to look up
321      * @param defaultValue the default value if the property is not found
322      * @return the string value or default value
323      */
324     protected String getDefaultString(final String key, final String defaultValue) {
325         final String value = systemProperties.getProperty(key);
326         if (value != null) {
327             return value;
328         }
329         return defaultValue;
330     }
331 
332     /**
333      * Determines if the given path represents a web crawling target.
334      * Checks if the path starts with HTTP or HTTPS protocols.
335      *
336      * @param path the path to check
337      * @return true if the path is a web crawling path, false otherwise
338      */
339     protected boolean isWebCrawlingPath(final String path) {
340         if (path.startsWith("http:") || path.startsWith("https:")) {
341             return true;
342         }
343 
344         return false;
345     }
346 
347     /**
348      * Converts a crawling path to the appropriate protocol format.
349      * Handles various path formats and adds proper protocol prefixes.
350      *
351      * @param path the original path to convert
352      * @return the converted path with appropriate protocol prefix
353      */
354     protected String convertCrawlingPath(final String path) {
355         if (ComponentUtil.getProtocolHelper().hasKnownProtocol(path)) {
356             return path;
357         }
358 
359         if (path.startsWith("www.")) {
360             return "http://" + path;
361         }
362 
363         if (path.startsWith("//")) {
364             return "file://" + path;
365         }
366         if (path.startsWith("/")) {
367             return "file:" + path;
368         }
369         if (!path.startsWith("file:")) {
370             return "file:/" + path.replace('\\', '/');
371         }
372         return path;
373     }
374 
375     /**
376      * Displays the start crawling form.
377      *
378      * @return HTML response for the start crawling form
379      */
380     @Execute
381     @Secured({ ROLE })
382     public HtmlResponse startCrawlingForm() {
383         saveToken();
384         return asHtml(path_AdminWizard_AdminWizardStartJsp).useForm(StartCrawlingForm.class);
385     }
386 
387     /**
388      * Starts the crawling process for all configured crawlers.
389      *
390      * @param form the start crawling form
391      * @return HTML response redirecting to the wizard index
392      */
393     @Execute
394     @Secured({ ROLE })
395     public HtmlResponse startCrawling(final StartCrawlingForm form) {
396         verifyToken(this::asIndexHtml);
397         if (!processHelper.isProcessRunning()) {
398             final List<ScheduledJob> scheduledJobList = scheduledJobService.getCrawlerJobList();
399             final JobManager jobManager = ComponentUtil.getJobManager();
400             for (final ScheduledJob scheduledJob : scheduledJobList) {
401                 jobManager.findJobByUniqueOf(LaJobUnique.of(scheduledJob.getId())).ifPresent(job -> {
402                     job.launchNow();
403                 });
404             }
405             saveInfo(messages -> messages.addSuccessStartCrawlProcess(GLOBAL));
406         } else {
407             saveError(messages -> messages.addErrorsFailedToStartCrawlProcess(GLOBAL));
408         }
409         return redirect(AdminWizardAction.class);
410     }
411 }