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.base;
17  
18  import java.util.ArrayList;
19  import java.util.HashSet;
20  import java.util.LinkedHashMap;
21  import java.util.List;
22  import java.util.Locale;
23  import java.util.Map;
24  import java.util.Set;
25  
26  import org.apache.commons.text.StringEscapeUtils;
27  import org.codelibs.core.lang.StringUtil;
28  import org.codelibs.core.net.URLUtil;
29  import org.codelibs.fess.Constants;
30  import org.codelibs.fess.app.web.sso.SsoAction;
31  import org.codelibs.fess.chat.ChatClient;
32  import org.codelibs.fess.entity.SearchRequestParams.SearchRequestType;
33  import org.codelibs.fess.helper.LabelTypeHelper;
34  import org.codelibs.fess.helper.OsddHelper;
35  import org.codelibs.fess.helper.PopularWordHelper;
36  import org.codelibs.fess.helper.QueryHelper;
37  import org.codelibs.fess.helper.RoleQueryHelper;
38  import org.codelibs.fess.helper.SearchHelper;
39  import org.codelibs.fess.helper.UserInfoHelper;
40  import org.codelibs.fess.mylasta.action.FessUserBean;
41  import org.codelibs.fess.query.QueryFieldConfig;
42  import org.codelibs.fess.thumbnail.ThumbnailManager;
43  import org.codelibs.fess.util.ComponentUtil;
44  import org.dbflute.optional.OptionalThing;
45  import org.lastaflute.web.login.LoginManager;
46  import org.lastaflute.web.response.ActionResponse;
47  import org.lastaflute.web.response.HtmlResponse;
48  import org.lastaflute.web.response.next.HtmlNext;
49  import org.lastaflute.web.ruts.process.ActionRuntime;
50  
51  import jakarta.annotation.Resource;
52  import jakarta.servlet.http.HttpServletRequest;
53  import jakarta.servlet.http.HttpSession;
54  
55  /**
56   * Abstract base class for search-related actions in the Fess search application.
57   * Provides common functionality for search operations, including search form handling,
58   * label management, user authentication, and search result processing.
59   *
60   * This class extends FessBaseAction and serves as the foundation for all search-related
61   * web actions in the application.
62   */
63  public abstract class FessSearchAction extends FessBaseAction {
64  
65      /**
66       * Default constructor.
67       */
68      public FessSearchAction() {
69          super();
70      }
71  
72      /** The field name used for label-based search filtering. */
73      protected static final String LABEL_FIELD = "label";
74  
75      /** Helper for performing search operations and managing search requests. */
76      @Resource
77      protected SearchHelper searchHelper;
78  
79      /** Manager for handling thumbnail generation and display. */
80      @Resource
81      protected ThumbnailManager thumbnailManager;
82  
83      /** Helper for managing label types and label-based filtering. */
84      @Resource
85      protected LabelTypeHelper labelTypeHelper;
86  
87      /** Helper for query processing and transformation. */
88      @Resource
89      protected QueryHelper queryHelper;
90  
91      /** Configuration for query field mappings and processing. */
92      @Resource
93      protected QueryFieldConfig queryFieldConfig;
94  
95      /** Helper for role-based query filtering and security. */
96      @Resource
97      protected RoleQueryHelper roleQueryHelper;
98  
99      /** Helper for managing user information and authentication. */
100     @Resource
101     protected UserInfoHelper userInfoHelper;
102 
103     /** Helper for OpenSearch Description Document (OSDD) functionality. */
104     @Resource
105     protected OsddHelper osddHelper;
106 
107     /** Helper for managing popular search words and suggestions. */
108     @Resource
109     protected PopularWordHelper popularWordHelper;
110 
111     /** Client for RAG chat functionality. */
112     @Resource
113     protected ChatClient chatClient;
114 
115     /** The HTTP servlet request object for the current request. */
116     @Resource
117     protected HttpServletRequest request;
118 
119     /** Flag indicating whether search logging is enabled. */
120     protected boolean searchLogSupport;
121 
122     /** Flag indicating whether favorite functionality is enabled. */
123     protected boolean favoriteSupport;
124 
125     /** Flag indicating whether thumbnail generation is enabled. */
126     protected boolean thumbnailSupport;
127 
128     /**
129      * Hook method called before action execution. Sets up search-related flags and
130      * registers popular words if enabled.
131      *
132      * @param runtime the action runtime context
133      * @return the action response, or null to continue with normal processing
134      */
135     @Override
136     public ActionResponse hookBefore(final ActionRuntime runtime) { // application may override
137         searchLogSupport = fessConfig.isSearchLog();
138         favoriteSupport = fessConfig.isUserFavorite();
139         thumbnailSupport = fessConfig.isThumbnailEnabled();
140         runtime.registerData("searchLogSupport", searchLogSupport);
141         runtime.registerData("favoriteSupport", favoriteSupport);
142         runtime.registerData("thumbnailSupport", thumbnailSupport);
143         if (fessConfig.isWebApiPopularWord()) {
144             final List<String> tagList = new ArrayList<>();
145             final String key = ComponentUtil.getVirtualHostHelper().getVirtualHostKey();
146             if (StringUtil.isNotBlank(key)) {
147                 tagList.add(key);
148             }
149             runtime.registerData("popularWords", popularWordHelper.getWordList(SearchRequestType.SEARCH, null,
150                     tagList.toArray(new String[tagList.size()]), null, null, null));
151         }
152         return super.hookBefore(runtime);
153     }
154 
155     /**
156      * Returns the login manager for this action. Search actions do not require
157      * a login manager as they handle authentication differently.
158      *
159      * @return an empty OptionalThing as search actions don't use login managers
160      */
161     @Override
162     protected OptionalThing<LoginManager> myLoginManager() {
163         return OptionalThing.empty();
164     }
165 
166     /**
167      * Sets up HTML data for rendering search-related pages. This includes
168      * label types, language items, user information, and various UI flags.
169      *
170      * @param runtime the action runtime context
171      */
172     @Override
173     protected void setupHtmlData(final ActionRuntime runtime) {
174         super.setupHtmlData(runtime);
175         systemHelper.setupSearchHtmlData(this, runtime);
176 
177         runtime.registerData("osddLink", osddHelper.hasOpenSearchFile());
178         runtime.registerData("clipboardCopyIcon", fessConfig.isClipboardCopyIconEnabled());
179 
180         final List<Map<String, String>> labelTypeItems = labelTypeHelper.getLabelTypeItemList(SearchRequestType.SEARCH,
181                 request.getLocale() == null ? Locale.ROOT : request.getLocale());
182         runtime.registerData("labelTypeItems", labelTypeItems);
183         runtime.registerData("displayLabelTypeItems", labelTypeItems != null && !labelTypeItems.isEmpty());
184 
185         Locale locale = ComponentUtil.getRequestManager().getUserLocale();
186         if (locale == null) {
187             locale = Locale.ENGLISH;
188         }
189         runtime.registerData("langItems", systemHelper.getLanguageItems(locale));
190         final String username = systemHelper.getUsername();
191         runtime.registerData("username", username);
192         runtime.registerData("editableUser", fessLoginAssist.getSavedUserBean().map(FessUserBean::isEditable).orElse(false));
193         runtime.registerData("adminUser",
194                 fessConfig.isAdminUser(username) || fessLoginAssist.getSavedUserBean()
195                         .map(user -> user.hasRoles(fessConfig.getAuthenticationAdminRolesAsArray()))
196                         .orElse(false));
197 
198         runtime.registerData("pageLoginLink", fessConfig.isLoginLinkEnabled());
199         runtime.registerData("chatEnabled", chatClient.isAvailable());
200     }
201 
202     // ===================================================================================
203     //                                                                             Helpers
204     //                                                                           =========
205 
206     /**
207      * Checks if login is required for the current request based on configuration
208      * and user authentication status.
209      *
210      * @return true if login is required, false otherwise
211      */
212     protected boolean isLoginRequired() {
213         if (fessConfig.isLoginRequired() && !fessLoginAssist.getSavedUserBean().isPresent()) {
214             return true;
215         }
216         return false;
217     }
218 
219     /**
220      * Builds and populates search form parameters including results per page,
221      * label filtering, and sort order based on user preferences and configuration.
222      *
223      * @param form the search form to populate with parameters
224      */
225     protected void buildFormParams(final SearchForm form) {
226 
227         final HttpSession session = request.getSession(false);
228         if (session != null) {
229             final Object resultsPerPage = session.getAttribute(Constants.RESULTS_PER_PAGE);
230             if (resultsPerPage instanceof Integer) {
231                 form.num = (Integer) resultsPerPage;
232             }
233         }
234 
235         // label
236         final List<Map<String, String>> labelTypeItems = labelTypeHelper.getLabelTypeItemList(SearchRequestType.SEARCH,
237                 request.getLocale() == null ? Locale.ROOT : request.getLocale());
238 
239         if (!labelTypeItems.isEmpty() && !form.fields.containsKey(FessSearchAction.LABEL_FIELD)) {
240             final String[] defaultLabelValues = fessConfig.getDefaultLabelValues(getUserBean());
241             if (defaultLabelValues.length > 0) {
242                 form.fields.put(FessSearchAction.LABEL_FIELD, defaultLabelValues);
243             }
244         }
245 
246         final Map<String, String> labelMap = new LinkedHashMap<>();
247         if (!labelTypeItems.isEmpty()) {
248             for (final Map<String, String> map : labelTypeItems) {
249                 labelMap.put(map.get(Constants.ITEM_VALUE), map.get(Constants.ITEM_LABEL));
250             }
251         }
252         request.setAttribute(Constants.LABEL_VALUE_MAP, labelMap);
253 
254         // sort
255         if (StringUtil.isBlank(form.sort)) {
256             final String[] defaultSortValues = fessConfig.getDefaultSortValues(getUserBean());
257             if (defaultSortValues.length == 1) {
258                 form.sort = defaultSortValues[0];
259             } else if (defaultSortValues.length >= 2) {
260                 final StringBuilder sortValueSb = new StringBuilder();
261                 final Set<String> sortFieldNames = new HashSet<>();
262                 for (final String defaultSortValue : defaultSortValues) {
263                     for (final String singleValue : defaultSortValue.split(",")) {
264                         final String sortFieldName = singleValue.split("\\.")[0];
265                         if (!sortFieldNames.contains(sortFieldName)) {
266                             sortFieldNames.add(sortFieldName);
267                             if (sortValueSb.length() > 0) {
268                                 sortValueSb.append(",");
269                             }
270                             sortValueSb.append(singleValue);
271                         }
272                     }
273                 }
274                 form.sort = sortValueSb.toString();
275             }
276         }
277     }
278 
279     /**
280      * Builds initial parameters for facet and geo search functionality
281      * by calling buildInitParamMap for both parameter types.
282      */
283     protected void buildInitParams() {
284         buildInitParamMap(viewHelper.getInitFacetParamMap(), Constants.FACET_QUERY, Constants.FACET_FORM);
285         buildInitParamMap(viewHelper.getInitGeoParamMap(), Constants.GEO_QUERY, Constants.GEO_FORM);
286     }
287 
288     /**
289      * Builds parameter maps for search initialization, creating both query strings
290      * and form inputs for the given parameters.
291      *
292      * @param paramMap the parameter map to process
293      * @param queryKey the key for storing query string parameters
294      * @param formKey the key for storing form input parameters
295      */
296     protected void buildInitParamMap(final Map<String, String> paramMap, final String queryKey, final String formKey) {
297         if (!paramMap.isEmpty()) {
298             final StringBuilder queryBuf = new StringBuilder(100);
299             final StringBuilder formBuf = new StringBuilder(100);
300             for (final Map.Entry<String, String> entry : paramMap.entrySet()) {
301                 queryBuf.append('&');
302                 queryBuf.append(URLUtil.encode(entry.getValue(), Constants.UTF_8));
303                 queryBuf.append('=');
304                 queryBuf.append(URLUtil.encode(entry.getKey(), Constants.UTF_8));
305                 formBuf.append("<input type=\"hidden\" name=\"");
306                 formBuf.append(StringEscapeUtils.escapeHtml4(entry.getValue()));
307                 formBuf.append("\" value=\"");
308                 formBuf.append(StringEscapeUtils.escapeHtml4(entry.getKey()));
309                 formBuf.append("\"/>");
310             }
311             request.setAttribute(queryKey, queryBuf.toString());
312             request.setAttribute(formKey, formBuf.toString());
313         }
314     }
315 
316     /**
317      * Redirects the user to the login page after storing current search parameters
318      * for restoration after successful authentication.
319      *
320      * @return HTML response that redirects to the login page
321      */
322     protected HtmlResponse redirectToLogin() {
323         searchHelper.storeSearchParameters();
324         return systemHelper.getRedirectResponseToLogin(redirect(SsoAction.class));
325     }
326 
327     /**
328      * Redirects the user to the root path of the application.
329      *
330      * @return HTML response that redirects to the root page
331      */
332     protected HtmlResponse redirectToRoot() {
333         return systemHelper.getRedirectResponseToRoot(newHtmlResponseAsRedirect("/"));
334     }
335 
336     /**
337      * Processes the given path through the virtual host helper to handle
338      * virtual host configurations and path modifications.
339      *
340      * @param path the HTML path to process
341      * @return the processed path with virtual host handling applied
342      */
343     protected HtmlNext virtualHost(final HtmlNext path) {
344         return ComponentUtil.getVirtualHostHelper().getVirtualHostPath(path);
345     }
346 }