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.searchlist;
17  
18  import java.util.ArrayList;
19  import java.util.HashMap;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Set;
23  import java.util.TreeMap;
24  import java.util.function.Consumer;
25  
26  import org.apache.logging.log4j.LogManager;
27  import org.apache.logging.log4j.Logger;
28  import org.codelibs.core.lang.StringUtil;
29  import org.codelibs.core.net.URLUtil;
30  import org.codelibs.fess.Constants;
31  import org.codelibs.fess.annotation.Secured;
32  import org.codelibs.fess.app.web.CrudMode;
33  import org.codelibs.fess.app.web.base.FessAdminAction;
34  import org.codelibs.fess.entity.SearchRenderData;
35  import org.codelibs.fess.exception.InvalidQueryException;
36  import org.codelibs.fess.exception.ResultOffsetExceededException;
37  import org.codelibs.fess.helper.QueryHelper;
38  import org.codelibs.fess.helper.SearchHelper;
39  import org.codelibs.fess.helper.SystemHelper;
40  import org.codelibs.fess.mylasta.action.FessMessages;
41  import org.codelibs.fess.mylasta.direction.FessConfig;
42  import org.codelibs.fess.opensearch.client.SearchEngineClient;
43  import org.codelibs.fess.util.ComponentUtil;
44  import org.codelibs.fess.util.RenderDataUtil;
45  import org.dbflute.optional.OptionalEntity;
46  import org.lastaflute.web.Execute;
47  import org.lastaflute.web.response.HtmlResponse;
48  import org.lastaflute.web.response.render.RenderData;
49  import org.lastaflute.web.ruts.process.ActionRuntime;
50  import org.lastaflute.web.validation.VaMessenger;
51  import org.opensearch.index.query.QueryBuilder;
52  import org.opensearch.index.query.QueryBuilders;
53  
54  import jakarta.annotation.Resource;
55  import jakarta.servlet.http.HttpServletRequest;
56  
57  /**
58   * Admin action for Search List.
59   *
60   */
61  public class AdminSearchlistAction extends FessAdminAction {
62  
63      /**
64       * Default constructor.
65       */
66      public AdminSearchlistAction() {
67          super();
68      }
69  
70      /** Role name for admin search list operations */
71      public static final String ROLE = "admin-searchlist";
72  
73      // ===================================================================================
74      // Constant
75      //
76      private static final Logger logger = LogManager.getLogger(AdminSearchlistAction.class);
77  
78      private static final Set<String> STANDARD_EDIT_FIELDS = Set.of("url", "title", "role", "boost", "label", "lang", "mimetype", "filetype",
79              "filename", "content", "has_cache", "cache", "digest", "host", "site", "segment", "config_id", "parent_id", "content_length",
80              "favorite_count", "click_count", "created", "timestamp", "last_modified", "expires", "virtual_host", "doc_id");
81  
82      // ===================================================================================
83      // Attribute
84      // =========
85  
86      /** Client for interacting with the search engine. */
87      @Resource
88      protected SearchEngineClient searchEngineClient;
89  
90      /** Helper for building and parsing search queries. */
91      @Resource
92      protected QueryHelper queryHelper;
93  
94      /** Helper for executing search operations. */
95      @Resource
96      protected SearchHelper searchHelper;
97  
98      /** HTTP servlet request for accessing request parameters. */
99      @Resource
100     protected HttpServletRequest request;
101 
102     /** Current form reference for rendering extra fields on the edit page. */
103     private CreateForm currentForm;
104 
105     /** List of document items returned from search */
106     public List<Map<String, Object>> documentItems;
107 
108     /** Number of results per page */
109     public String pageSize;
110 
111     /** Current page number for pagination */
112     public String currentPageNumber;
113 
114     /** Total number of records found */
115     public String allRecordCount;
116 
117     /** Total number of pages for pagination */
118     public String allPageCount;
119 
120     /** Flag indicating if there is a next page */
121     public boolean existNextPage;
122 
123     /** Flag indicating if there is a previous page */
124     public boolean existPrevPage;
125 
126     /** Starting record number for current page */
127     public String currentStartRecordNumber;
128 
129     /** Ending record number for current page */
130     public String currentEndRecordNumber;
131 
132     /** List of page numbers for pagination */
133     public List<String> pageNumberList;
134 
135     /** Search execution time in milliseconds */
136     public String execTime;
137 
138     // ===================================================================================
139     // Hook
140     // ======
141     @Override
142     protected void setupHtmlData(final ActionRuntime runtime) {
143         super.setupHtmlData(runtime);
144 
145         runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameSearchlist()));
146     }
147 
148     @Override
149     protected String getActionRole() {
150         return ROLE;
151     }
152 
153     // ===================================================================================
154     // Search Execute
155     // ==============
156     /**
157      * Displays the search list management index page.
158      *
159      * @param form the list form for filtering
160      * @return HTML response for the search list page
161      */
162     @Execute
163     @Secured({ ROLE, ROLE + VIEW })
164     public HtmlResponse index(final ListForm form) {
165         saveToken();
166         validate(form, messages -> {}, () -> asHtml(path_AdminError_AdminErrorJsp));
167         return asListHtml();
168     }
169 
170     /**
171      * Performs a search operation with the provided form data.
172      *
173      * @param form the list form containing search criteria
174      * @return HTML response with search results
175      */
176     protected HtmlResponse doSearch(final ListForm form) {
177         validate(form, messages -> {}, this::asListHtml);
178 
179         if (StringUtil.isBlank(form.q)) {
180             // query matches on all documents.
181             form.q = Constants.MATCHES_ALL_QUERY;
182         }
183         final WebRenderData renderData = new WebRenderData();
184         form.initialize();
185         request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN);
186         try {
187             searchHelper.search(form, renderData, getUserBean());
188             return asListHtml().renderWith(data -> {
189                 renderData.register(data);
190             });
191         } catch (final InvalidQueryException e) {
192             if (logger.isDebugEnabled()) {
193                 logger.debug("Invalid query: {}", form.q, e);
194             }
195             throwValidationError(e.getMessageCode(), this::asListHtml);
196         } catch (final ResultOffsetExceededException e) {
197             if (logger.isDebugEnabled()) {
198                 logger.debug("Invalid offset: {}", form.offset, e);
199             }
200             throwValidationError(messages -> messages.addErrorsResultSizeExceeded(GLOBAL), this::asListHtml);
201         }
202 
203         throwValidationError(messages -> messages.addErrorsInvalidQueryUnknown(GLOBAL), this::asListHtml);
204         return null; // ignore
205     }
206 
207     /**
208      * Executes a search based on the provided search criteria.
209      *
210      * @param form the list form containing search criteria
211      * @return HTML response with search results
212      */
213     @Execute
214     @Secured({ ROLE, ROLE + VIEW })
215     public HtmlResponse search(final ListForm form) {
216         saveToken();
217         return doSearch(form);
218     }
219 
220     /**
221      * Navigates to the previous page of search results.
222      *
223      * @param form the list form containing current search criteria
224      * @return HTML response with previous page results
225      */
226     @Execute
227     @Secured({ ROLE, ROLE + VIEW })
228     public HtmlResponse prev(final ListForm form) {
229         saveToken();
230         return doMove(form, -1);
231     }
232 
233     /**
234      * Navigates to the next page of search results.
235      *
236      * @param form the list form containing current search criteria
237      * @return HTML response with next page results
238      */
239     @Execute
240     @Secured({ ROLE, ROLE + VIEW })
241     public HtmlResponse next(final ListForm form) {
242         saveToken();
243         return doMove(form, 1);
244     }
245 
246     /**
247      * Navigates to a specific page of search results.
248      *
249      * @param form the list form containing target page information
250      * @return HTML response with specified page results
251      */
252     @Execute
253     @Secured({ ROLE, ROLE + VIEW })
254     public HtmlResponse move(final ListForm form) {
255         saveToken();
256         return doMove(form, 0);
257     }
258 
259     /**
260      * Handles pagination movement logic.
261      *
262      * @param form the list form containing current state
263      * @param move the direction to move (-1 for previous, 0 for specific page, 1 for next)
264      * @return HTML response with moved page results
265      */
266     protected HtmlResponse doMove(final ListForm form, final int move) {
267         form.initialize();
268         Integer pageNumber = form.pn;
269         if (pageNumber != null && pageNumber > 0) {
270             pageNumber = pageNumber + move;
271             if (pageNumber < 1) {
272                 pageNumber = 1;
273             }
274             form.start = (pageNumber - 1) * form.getPageSize();
275         }
276         return doSearch(form);
277     }
278 
279     // -----------------------------------------------------
280     // Confirm
281     // -------
282 
283     /**
284      * Deletes a single document from the search index.
285      *
286      * @param form the delete form containing document ID
287      * @return HTML response redirecting to the list page
288      */
289     @Execute
290     @Secured({ ROLE })
291     public HtmlResponse delete(final DeleteForm form) {
292         validate(form, messages -> {}, this::asListHtml);
293         verifyToken(this::asListHtml);
294         final String docId = form.docId;
295         try {
296             final QueryBuilder query = QueryBuilders.termQuery(fessConfig.getIndexFieldDocId(), docId);
297             searchEngineClient.deleteByQuery(fessConfig.getIndexDocumentUpdateIndex(), query);
298             saveInfo(messages -> messages.addSuccessDeleteDocFromIndex(GLOBAL));
299         } catch (final Exception e) {
300             logger.warn("Failed to process a request.", e);
301             throwValidationError(messages -> messages.addErrorsFailedToDeleteDocInAdmin(GLOBAL), this::asListHtml);
302         }
303         return asListHtml();
304     }
305 
306     /**
307      * Deletes all documents matching the current search query.
308      *
309      * @param form the list form containing search criteria
310      * @return HTML response redirecting to the list page
311      */
312     @Execute
313     @Secured({ ROLE, ROLE + VIEW })
314     public HtmlResponse deleteall(final ListForm form) {
315         validate(form, messages -> {}, this::asListHtml);
316         verifyToken(this::asListHtml);
317         try {
318             searchHelper.deleteByQuery(request, form);
319             saveInfo(messages -> messages.addSuccessDeleteDocFromIndex(GLOBAL));
320         } catch (final InvalidQueryException e) {
321             if (logger.isDebugEnabled()) {
322                 logger.debug("Invalid query: {}", form.q, e);
323             }
324             throwValidationError(e.getMessageCode(), this::asListHtml);
325         }
326         return asListHtml();
327     }
328 
329     /**
330      * Displays the form for creating a new document.
331      *
332      * @param form the create form
333      * @return HTML response for the document creation form
334      */
335     @Execute
336     @Secured({ ROLE })
337     public HtmlResponse createnew(final CreateForm form) {
338         saveToken();
339         form.initialize();
340         form.crudMode = CrudMode.CREATE;
341         getDoc(form).ifPresent(entity -> {
342             form.doc = fessConfig.convertToEditableDoc(entity);
343         });
344         currentForm = form;
345         return asEditHtml();
346     }
347 
348     /**
349      * Displays the form for editing an existing document.
350      *
351      * @param form the edit form containing document ID
352      * @return HTML response for the document edit form
353      */
354     @Execute
355     @Secured({ ROLE })
356     public HtmlResponse edit(final EditForm form) {
357         validate(form, messages -> {}, this::asListHtml);
358         getDoc(form).ifPresent(entity -> {
359             form.doc = fessConfig.convertToEditableDoc(entity);
360             form.id = (String) entity.remove(fessConfig.getIndexFieldId());
361             form.seqNo = (Long) entity.remove(fessConfig.getIndexFieldSeqNo());
362             form.primaryTerm = (Long) entity.remove(fessConfig.getIndexFieldPrimaryTerm());
363         }).orElse(() -> {
364             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.id), this::asListHtml);
365         });
366         currentForm = form;
367         saveToken();
368         return asEditHtml();
369     }
370 
371     /**
372      * Creates a new document in the search index.
373      *
374      * @param form the create form containing document data
375      * @return HTML response redirecting to the list page after creation
376      */
377     @Execute
378     @Secured({ ROLE })
379     public HtmlResponse create(final CreateForm form) {
380         currentForm = form;
381         verifyCrudMode(form.crudMode, CrudMode.CREATE, this::asListHtml);
382         validate(form, messages -> {}, this::asEditHtml);
383         validateFields(form.doc, v -> throwValidationError(v, this::asEditHtml));
384         verifyToken(this::asEditHtml);
385         getDoc(form).ifPresent(entity -> {
386             try {
387                 entity.putAll(fessConfig.convertToStorableDoc(form.doc));
388 
389                 final String newId = ComponentUtil.getCrawlingInfoHelper().generateId(entity);
390                 entity.put(fessConfig.getIndexFieldId(), newId);
391 
392                 final String index = fessConfig.getIndexDocumentUpdateIndex();
393                 searchEngineClient.store(index, entity);
394                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
395             } catch (final Exception e) {
396                 logger.warn("Failed to add {}", entity, e);
397                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
398                         this::asEditHtml);
399             }
400         }).orElse(() -> {
401             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
402         });
403         return redirect(getClass());
404     }
405 
406     /**
407      * Updates an existing document in the search index.
408      *
409      * @param form the edit form containing updated document data
410      * @return HTML response redirecting to the search results after update
411      */
412     @Execute
413     @Secured({ ROLE })
414     public HtmlResponse update(final EditForm form) {
415         currentForm = form;
416         verifyCrudMode(form.crudMode, CrudMode.EDIT, this::asListHtml);
417         validate(form, messages -> {}, this::asEditHtml);
418         validateFields(form.doc, v -> throwValidationError(v, this::asEditHtml));
419         verifyToken(this::asEditHtml);
420         getDoc(form).ifPresent(entity -> {
421             final String index = fessConfig.getIndexDocumentUpdateIndex();
422             try {
423                 entity.putAll(fessConfig.convertToStorableDoc(form.doc));
424 
425                 final String newId = ComponentUtil.getCrawlingInfoHelper().generateId(entity);
426                 final String oldId = (String) entity.get(fessConfig.getIndexFieldId());
427                 if (!newId.equals(oldId)) {
428                     entity.put(fessConfig.getIndexFieldId(), newId);
429                     entity.remove(fessConfig.getIndexFieldVersion());
430                     final Long seqNo = (Long) entity.remove(fessConfig.getIndexFieldSeqNo());
431                     final Long primaryTerm = (Long) entity.remove(fessConfig.getIndexFieldPrimaryTerm());
432                     if (seqNo != null && primaryTerm != null && oldId != null) {
433                         searchEngineClient.delete(index, oldId, seqNo, primaryTerm);
434                     }
435                 }
436 
437                 searchEngineClient.store(index, entity);
438                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
439             } catch (final Exception e) {
440                 logger.warn("Failed to update {}", entity, e);
441                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
442                         this::asEditHtml);
443             }
444         }).orElse(() -> {
445             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.id), this::asEditHtml);
446         });
447         return redirectWith(getClass(), moreUrl("search").params("q", URLUtil.encode(form.q, Constants.UTF_8)));
448     }
449 
450     // ===================================================================================
451     //                                                                       Validation
452     //                                                                           =========
453     /**
454      * Validates document fields according to index field requirements.
455      *
456      * @param doc the document to validate
457      * @param throwError consumer to handle validation errors
458      */
459     public static void validateFields(final Map<String, Object> doc, final Consumer<VaMessenger<FessMessages>> throwError) {
460         final FessConfig fessConfig = ComponentUtil.getFessConfig();
461 
462         try {
463             if (!fessConfig.validateIndexRequiredFields(doc)) {
464                 throwError.accept(messages -> fessConfig.invalidIndexRequiredFields(doc)
465                         .stream()
466                         .forEach(s -> messages.addErrorsPropertyRequired("doc." + s, s)));
467             }
468 
469             if (!fessConfig.validateIndexArrayFields(doc)) {
470                 throwError.accept(messages -> fessConfig.invalidIndexArrayFields(doc)
471                         .stream()
472                         .forEach(s -> messages.addErrorsPropertyTypeArray("doc." + s, s)));
473             }
474             if (!fessConfig.validateIndexDateFields(doc)) {
475                 throwError.accept(messages -> fessConfig.invalidIndexDateFields(doc)
476                         .stream()
477                         .forEach(s -> messages.addErrorsPropertyTypeDate("doc." + s, s)));
478             }
479             if (!fessConfig.validateIndexIntegerFields(doc)) {
480                 throwError.accept(messages -> fessConfig.invalidIndexIntegerFields(doc)
481                         .stream()
482                         .forEach(s -> messages.addErrorsPropertyTypeInteger("doc." + s, s)));
483             }
484             if (!fessConfig.validateIndexLongFields(doc)) {
485                 throwError.accept(messages -> fessConfig.invalidIndexLongFields(doc)
486                         .stream()
487                         .forEach(s -> messages.addErrorsPropertyTypeLong("doc." + s, s)));
488             }
489             if (!fessConfig.validateIndexFloatFields(doc)) {
490                 throwError.accept(messages -> fessConfig.invalidIndexFloatFields(doc)
491                         .stream()
492                         .forEach(s -> messages.addErrorsPropertyTypeFloat("doc." + s, s)));
493             }
494             if (!fessConfig.validateIndexDoubleFields(doc)) {
495                 throwError.accept(messages -> fessConfig.invalidIndexDoubleFields(doc)
496                         .stream()
497                         .forEach(s -> messages.addErrorsPropertyTypeDouble("doc." + s, s)));
498             }
499         } catch (final Exception e) {
500             throwError.accept(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, e.getMessage()));
501         }
502     }
503 
504     // ===================================================================================
505     //                                                                              JSP
506     //                                                                           =========
507     /**
508      * Retrieves or creates a document entity based on the form data.
509      *
510      * @param form the form containing document information
511      * @return optional entity containing the document data, or empty if not found
512      */
513     public static OptionalEntity<Map<String, Object>> getDoc(final CreateForm form) {
514         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
515         final FessConfig fessConfig = ComponentUtil.getFessConfig();
516         final SearchEngineClient searchEngineClient = ComponentUtil.getSearchEngineClient();
517         switch (form.crudMode) {
518         case CrudMode.CREATE:
519             final Map<String, Object> entity = new HashMap<>();
520             entity.put(fessConfig.getIndexFieldDocId(), systemHelper.generateDocId(entity));
521             return OptionalEntity.of(entity);
522         case CrudMode.EDIT:
523             final String docId;
524             if (form.doc != null) {
525                 docId = (String) form.doc.get(fessConfig.getIndexFieldDocId());
526             } else {
527                 docId = null;
528             }
529             if (StringUtil.isNotBlank(docId)) {
530                 return searchEngineClient.getDocument(fessConfig.getIndexDocumentUpdateIndex(), builder -> {
531                     builder.setQuery(QueryBuilders.termQuery(fessConfig.getIndexFieldDocId(), docId));
532                     return true;
533                 });
534             }
535             break;
536         default:
537             break;
538         }
539         return OptionalEntity.empty();
540     }
541 
542     private HtmlResponse asListHtml() {
543         return asHtml(path_AdminSearchlist_AdminSearchlistJsp);
544     }
545 
546     private HtmlResponse asEditHtml() {
547         return asHtml(path_AdminSearchlist_AdminSearchlistEditJsp).renderWith(data -> {
548             registerExtraFields(data);
549         });
550     }
551 
552     private void registerExtraFields(final RenderData data) {
553         final Set<String> arrayFieldSet = fessConfig.getIndexAdminArrayFieldSet();
554         final Set<String> dateFieldSet = fessConfig.getIndexAdminDateFieldSet();
555         final Set<String> integerFieldSet = fessConfig.getIndexAdminIntegerFieldSet();
556         final Set<String> longFieldSet = fessConfig.getIndexAdminLongFieldSet();
557         final Set<String> floatFieldSet = fessConfig.getIndexAdminFloatFieldSet();
558         final Set<String> doubleFieldSet = fessConfig.getIndexAdminDoubleFieldSet();
559 
560         final Set<String> reservedFields = Set.of(fessConfig.getIndexFieldId(), fessConfig.getIndexFieldVersion(),
561                 fessConfig.getIndexFieldSeqNo(), fessConfig.getIndexFieldPrimaryTerm());
562 
563         // Collect candidate fields from config definitions and document keys
564         final Set<String> candidateFields = new java.util.TreeSet<>();
565         candidateFields.addAll(arrayFieldSet);
566         candidateFields.addAll(dateFieldSet);
567         candidateFields.addAll(integerFieldSet);
568         candidateFields.addAll(longFieldSet);
569         candidateFields.addAll(floatFieldSet);
570         candidateFields.addAll(doubleFieldSet);
571         final Map<String, Object> doc = currentForm != null ? currentForm.doc : null;
572         if (doc != null) {
573             candidateFields.addAll(doc.keySet());
574         }
575 
576         final List<String> extraFieldNames = new ArrayList<>();
577         final Map<String, String> extraFieldTypes = new TreeMap<>();
578         candidateFields.stream().filter(key -> !STANDARD_EDIT_FIELDS.contains(key) && !reservedFields.contains(key)).forEach(key -> {
579             final String type;
580             if (arrayFieldSet.contains(key)) {
581                 type = "array";
582             } else if (dateFieldSet.contains(key)) {
583                 type = "date";
584             } else if (integerFieldSet.contains(key) || longFieldSet.contains(key) || floatFieldSet.contains(key)
585                     || doubleFieldSet.contains(key)) {
586                 type = "number";
587             } else {
588                 type = "text";
589             }
590             extraFieldNames.add(key);
591             extraFieldTypes.put(key, type);
592         });
593         RenderDataUtil.register(data, "extraFieldNames", extraFieldNames);
594         RenderDataUtil.register(data, "extraFieldTypes", extraFieldTypes);
595     }
596 
597     /**
598      * Web-specific implementation of SearchRenderData for rendering search results in the admin interface.
599      */
600     protected static class WebRenderData extends SearchRenderData {
601 
602         /**
603          * Default constructor.
604          */
605         protected WebRenderData() {
606             super();
607         }
608 
609         /**
610          * Registers all search-related data for rendering in the web interface.
611          *
612          * @param data the render data to populate
613          */
614         public void register(final RenderData data) {
615             RenderDataUtil.register(data, "queryId", queryId);
616             RenderDataUtil.register(data, "documentItems", documentItems);
617             RenderDataUtil.register(data, "facetResponse", facetResponse);
618             RenderDataUtil.register(data, "appendHighlightParams", appendHighlightParams);
619             RenderDataUtil.register(data, "execTime", execTime);
620             RenderDataUtil.register(data, "pageSize", pageSize);
621             RenderDataUtil.register(data, "currentPageNumber", currentPageNumber);
622             RenderDataUtil.register(data, "allRecordCount", allRecordCount);
623             RenderDataUtil.register(data, "allRecordCountRelation", allRecordCountRelation);
624             RenderDataUtil.register(data, "allPageCount", allPageCount);
625             RenderDataUtil.register(data, "existNextPage", existNextPage);
626             RenderDataUtil.register(data, "existPrevPage", existPrevPage);
627             RenderDataUtil.register(data, "currentStartRecordNumber", currentStartRecordNumber);
628             RenderDataUtil.register(data, "currentEndRecordNumber", currentEndRecordNumber);
629             RenderDataUtil.register(data, "pageNumberList", pageNumberList);
630             RenderDataUtil.register(data, "partialResults", partialResults);
631             RenderDataUtil.register(data, "queryTime", queryTime);
632             RenderDataUtil.register(data, "searchQuery", searchQuery);
633             RenderDataUtil.register(data, "requestedTime", requestedTime);
634         }
635     }
636 }