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.search;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Locale;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import org.apache.logging.log4j.LogManager;
29  import org.apache.logging.log4j.Logger;
30  import org.codelibs.core.lang.StringUtil;
31  import org.codelibs.fess.Constants;
32  import org.codelibs.fess.app.web.base.FessSearchAction;
33  import org.codelibs.fess.app.web.base.SearchForm;
34  import org.codelibs.fess.entity.SearchRenderData;
35  import org.codelibs.fess.entity.SearchRequestParams.SearchRequestType;
36  import org.codelibs.fess.exception.InvalidQueryException;
37  import org.codelibs.fess.exception.ResultOffsetExceededException;
38  import org.codelibs.fess.helper.RelatedContentHelper;
39  import org.codelibs.fess.helper.RelatedQueryHelper;
40  import org.codelibs.fess.util.RenderDataUtil;
41  import org.lastaflute.taglib.function.LaFunctions;
42  import org.lastaflute.web.Execute;
43  import org.lastaflute.web.response.HtmlResponse;
44  import org.lastaflute.web.response.render.RenderData;
45  import org.lastaflute.web.util.LaRequestUtil;
46  
47  import jakarta.annotation.Resource;
48  import jakarta.servlet.http.HttpSession;
49  
50  /**
51   * The action for search operations.
52   */
53  public class SearchAction extends FessSearchAction {
54  
55      /**
56       * Default constructor.
57       */
58      public SearchAction() {
59          super();
60      }
61  
62      // ===================================================================================
63      //                                                                            Constant
64      //
65      private static final Logger logger = LogManager.getLogger(SearchAction.class);
66  
67      // ===================================================================================
68      //                                                                           Attribute
69      //
70  
71      /**
72       * The helper for related content.
73       */
74      @Resource
75      protected RelatedContentHelper relatedContentHelper;
76  
77      /**
78       * The helper for related queries.
79       */
80      @Resource
81      protected RelatedQueryHelper relatedQueryHelper;
82  
83      // ===================================================================================
84      //                                                                               Hook
85      //                                                                              ======
86  
87      // ===================================================================================
88      //                                                                      Search Execute
89      //                                                                      ==============
90      /**
91       * The index page.
92       * @param form The search form.
93       * @return The HTML response.
94       */
95      @Execute
96      public HtmlResponse index(final SearchForm form) {
97          return search(form);
98      }
99  
100     /**
101      * The advance search page.
102      * @param form The search form.
103      * @return The HTML response.
104      */
105     @Execute
106     public HtmlResponse advance(final SearchForm form) {
107         if (isLoginRequired()) {
108             return redirectToLogin();
109         }
110         validate(form, messages -> {}, () -> asHtml(virtualHost(path_IndexJsp)).renderWith(data -> {
111             buildInitParams();
112             RenderDataUtil.register(data, "notification", fessConfig.getNotificationSearchTop());
113         }));
114         if (!form.hasConditionQuery()) {
115             if (StringUtil.isNotBlank(form.q)) {
116                 form.as.put("q", new String[] { form.q });
117             } else {
118                 // TODO set default?
119             }
120         }
121         return asHtml(virtualHost(path_AdvanceJsp)).renderWith(data -> {
122             buildInitParams();
123             RenderDataUtil.register(data, "notification", fessConfig.getNotificationAdvanceSearch());
124         });
125     }
126 
127     /**
128      * The search page.
129      * @param form The search form.
130      * @return The HTML response.
131      */
132     @Execute
133     public HtmlResponse search(final SearchForm form) {
134         if (viewHelper.isUseSession()) {
135             LaRequestUtil.getOptionalRequest().ifPresent(request -> {
136                 final HttpSession session = request.getSession(false);
137                 if (session != null && form.num != null) {
138                     session.setAttribute(Constants.RESULTS_PER_PAGE, form.num);
139                 }
140             });
141         }
142         return doSearch(form);
143     }
144 
145     /**
146      * The previous page.
147      * @param form The search form.
148      * @return The HTML response.
149      */
150     @Execute
151     public HtmlResponse prev(final SearchForm form) {
152         return doMove(form, -1);
153     }
154 
155     /**
156      * The next page.
157      * @param form The search form.
158      * @return The HTML response.
159      */
160     @Execute
161     public HtmlResponse next(final SearchForm form) {
162         return doMove(form, 1);
163     }
164 
165     /**
166      * The move page.
167      * @param form The search form.
168      * @return The HTML response.
169      */
170     @Execute
171     public HtmlResponse move(final SearchForm form) {
172         return doMove(form, 0);
173     }
174 
175     /**
176      * Performs a search.
177      * @param form The search form.
178      * @return The HTML response.
179      */
180     protected HtmlResponse doSearch(final SearchForm form) {
181         validate(form, messages -> {}, () -> asHtml(virtualHost(path_SearchJsp)));
182         if (isLoginRequired()) {
183             return redirectToLogin();
184         }
185 
186         if (viewHelper.isUseSession()) {
187             final HttpSession session = request.getSession(false);
188             if (session != null) {
189                 final Object resultsPerPage = session.getAttribute(Constants.RESULTS_PER_PAGE);
190                 if (resultsPerPage instanceof Integer) {
191                     form.num = (Integer) resultsPerPage;
192                 }
193             }
194         }
195 
196         if (StringUtil.isBlank(form.q) && form.fields.isEmpty() && !form.hasConditionQuery()) {
197             // redirect to index page
198             form.q = null;
199             return redirectToRoot();
200         }
201 
202         try {
203             buildFormParams(form);
204             form.lang = searchHelper.getLanguages(request, form);
205             final WebRenderData renderData = new WebRenderData();
206             searchHelper.search(form, renderData, getUserBean());
207             return asHtml(virtualHost(path_SearchJsp)).renderWith(data -> {
208                 if (form.hasConditionQuery()) {
209                     form.q = renderData.getSearchQuery();
210                 }
211                 renderData.register(data);
212                 RenderDataUtil.register(data, "displayQuery", getDisplayQuery(form, labelTypeHelper
213                         .getLabelTypeItemList(SearchRequestType.SEARCH, request.getLocale() == null ? Locale.ROOT : request.getLocale())));
214                 createPagingQuery(form);
215                 final String[] relatedContents = relatedContentHelper.getRelatedContents(form.getQuery());
216                 RenderDataUtil.register(data, "relatedContents", relatedContents);
217                 final String[] relatedQueries = relatedQueryHelper.getRelatedQueries(form.getQuery());
218                 if (relatedQueries.length > 0) {
219                     RenderDataUtil.register(data, "relatedQueries", relatedQueries);
220                 }
221             });
222         } catch (final InvalidQueryException e) {
223             if (logger.isDebugEnabled()) {
224                 logger.debug(e.getMessage(), e);
225             }
226             saveError(e.getMessageCode());
227             return redirectToRoot();
228         } catch (final ResultOffsetExceededException e) {
229             if (logger.isDebugEnabled()) {
230                 logger.debug(e.getMessage(), e);
231             }
232             saveError(messages -> {
233                 messages.addErrorsResultSizeExceeded(GLOBAL);
234             });
235             return redirectToRoot();
236         }
237     }
238 
239     /**
240      * Moves to a specific page.
241      * @param form The search form.
242      * @param move The number of pages to move.
243      * @return The HTML response.
244      */
245     protected HtmlResponse doMove(final SearchForm form, final int move) {
246         int start = fessConfig.getPagingSearchPageStartAsInteger();
247         if (form.pn != null) {
248             int pageNumber = form.pn;
249             if (pageNumber > 0) {
250                 pageNumber = pageNumber + move;
251                 if (pageNumber < 1) {
252                     pageNumber = 1;
253                 }
254                 start = (pageNumber - 1) * form.getPageSize();
255             }
256         }
257         form.start = start;
258 
259         return doSearch(form);
260     }
261 
262     /**
263      * Gets the display query.
264      * @param form The search form.
265      * @param labelTypeItems The list of label type items.
266      * @return The display query.
267      */
268     protected String getDisplayQuery(final SearchForm form, final List<Map<String, String>> labelTypeItems) {
269         final StringBuilder buf = new StringBuilder(100);
270         buf.append(form.q);
271         if (!form.fields.isEmpty() && form.fields.containsKey(LABEL_FIELD)) {
272             final String[] values = form.fields.get(LABEL_FIELD);
273             final List<String> labelList = new ArrayList<>();
274             if (values != null) {
275                 Collections.addAll(labelList, values);
276             }
277             for (final String labelTypeValue : labelList) {
278                 for (final Map<String, String> map : labelTypeItems) {
279                     if (map.get(Constants.ITEM_VALUE).equals(labelTypeValue)) {
280                         buf.append(' ');
281                         buf.append(map.get(Constants.ITEM_LABEL));
282                         break;
283                     }
284                 }
285             }
286         }
287         return buf.toString();
288     }
289 
290     /**
291      * Creates a paging query.
292      * @param form The search form.
293      */
294     protected void createPagingQuery(final SearchForm form) {
295         final List<String> pagingQueryList = new ArrayList<>();
296         if (form.ex_q != null) {
297             stream(form.ex_q).of(stream -> stream.filter(StringUtil::isNotBlank)
298                     .distinct()
299                     .forEach(q -> pagingQueryList.add("ex_q=" + LaFunctions.u(q))));
300         }
301         if (StringUtil.isNotBlank(form.sort)) {
302             pagingQueryList.add("sort=" + LaFunctions.u(form.sort));
303         }
304         if (form.lang != null) {
305             final Set<String> langSet = new HashSet<>();
306             for (final String lang : form.lang) {
307                 if (StringUtil.isNotBlank(lang) && lang.length() < 1000) {
308                     if (Constants.ALL_LANGUAGES.equals(lang)) {
309                         langSet.clear();
310                         break;
311                     }
312                     final String normalizeLang = systemHelper.normalizeLang(lang);
313                     if (normalizeLang != null) {
314                         langSet.add(normalizeLang);
315                     }
316                 }
317             }
318             if (!langSet.isEmpty()) {
319                 for (final String lang : langSet) {
320                     pagingQueryList.add("&lang=" + LaFunctions.u(lang));
321                 }
322             }
323         }
324         form.fields.entrySet().stream().filter(e -> e.getValue() != null).forEach(e -> {
325             final String key = LaFunctions.u(e.getKey());
326             stream(e.getValue()).of(stream -> stream.filter(StringUtil::isNotBlank)
327                     .forEach(s -> pagingQueryList.add("fields." + key + "=" + LaFunctions.u(s))));
328         });
329         form.as.entrySet().stream().filter(e -> e.getValue() != null).forEach(e -> {
330             final String key = LaFunctions.u(e.getKey());
331             stream(e.getValue()).of(stream -> stream.filter(StringUtil::isNotBlank)
332                     .forEach(s -> pagingQueryList.add("as." + key + "=" + LaFunctions.u(s))));
333         });
334         request.setAttribute(Constants.PAGING_QUERY_LIST, pagingQueryList);
335     }
336 
337     /**
338      * The render data for the search page.
339      */
340     protected static class WebRenderData extends SearchRenderData {
341 
342         /**
343          * Default constructor.
344          */
345         public WebRenderData() {
346             super();
347         }
348 
349         /**
350          * Registers the render data.
351          * @param data The render data.
352          */
353         public void register(final RenderData data) {
354             RenderDataUtil.register(data, "queryId", queryId);
355             RenderDataUtil.register(data, "documentItems", documentItems);
356             RenderDataUtil.register(data, "facetResponse", facetResponse);
357             RenderDataUtil.register(data, "appendHighlightParams", appendHighlightParams);
358             RenderDataUtil.register(data, "execTime", execTime);
359             RenderDataUtil.register(data, "pageSize", pageSize);
360             RenderDataUtil.register(data, "currentPageNumber", currentPageNumber);
361             RenderDataUtil.register(data, "allRecordCount", allRecordCount);
362             RenderDataUtil.register(data, "allRecordCountRelation", allRecordCountRelation);
363             RenderDataUtil.register(data, "allPageCount", allPageCount);
364             RenderDataUtil.register(data, "existNextPage", existNextPage);
365             RenderDataUtil.register(data, "existPrevPage", existPrevPage);
366             RenderDataUtil.register(data, "currentStartRecordNumber", currentStartRecordNumber);
367             RenderDataUtil.register(data, "currentEndRecordNumber", currentEndRecordNumber);
368             RenderDataUtil.register(data, "pageNumberList", pageNumberList);
369             RenderDataUtil.register(data, "partialResults", partialResults);
370             RenderDataUtil.register(data, "queryTime", queryTime);
371             RenderDataUtil.register(data, "searchQuery", searchQuery);
372             RenderDataUtil.register(data, "requestedTime", requestedTime);
373         }
374     }
375 }