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.api.admin.searchlist;
17  
18  import static org.codelibs.fess.app.web.admin.searchlist.AdminSearchlistAction.getDoc;
19  import static org.codelibs.fess.app.web.admin.searchlist.AdminSearchlistAction.validateFields;
20  
21  import java.util.Map;
22  
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.fess.Constants;
27  import org.codelibs.fess.app.web.CrudMode;
28  import org.codelibs.fess.app.web.api.ApiResult;
29  import org.codelibs.fess.app.web.api.ApiResult.ApiDeleteResponse;
30  import org.codelibs.fess.app.web.api.ApiResult.ApiDocResponse;
31  import org.codelibs.fess.app.web.api.ApiResult.ApiDocsResponse;
32  import org.codelibs.fess.app.web.api.ApiResult.ApiResponse;
33  import org.codelibs.fess.app.web.api.ApiResult.ApiUpdateResponse;
34  import org.codelibs.fess.app.web.api.ApiResult.Status;
35  import org.codelibs.fess.app.web.api.admin.FessApiAdminAction;
36  import org.codelibs.fess.entity.SearchRenderData;
37  import org.codelibs.fess.exception.InvalidQueryException;
38  import org.codelibs.fess.exception.ResultOffsetExceededException;
39  import org.codelibs.fess.helper.SearchHelper;
40  import org.codelibs.fess.opensearch.client.SearchEngineClient;
41  import org.codelibs.fess.util.ComponentUtil;
42  import org.lastaflute.web.Execute;
43  import org.lastaflute.web.response.JsonResponse;
44  import org.opensearch.index.query.QueryBuilder;
45  import org.opensearch.index.query.QueryBuilders;
46  
47  import jakarta.annotation.Resource;
48  
49  /**
50   * API action for admin search list management.
51   *
52   */
53  public class ApiAdminSearchlistAction extends FessApiAdminAction {
54  
55      // ===================================================================================
56      // Constant
57      //
58      /** The logger for this class. */
59      private static final Logger logger = LogManager.getLogger(ApiAdminSearchlistAction.class);
60  
61      // ===================================================================================
62      //                                                                         Constructor
63      //                                                                         ===========
64      /**
65       * Default constructor.
66       */
67      public ApiAdminSearchlistAction() {
68          super();
69      }
70  
71      // ===================================================================================
72      //                                                                           Attribute
73      //                                                                           =========
74  
75      // ===================================================================================
76      // Attribute
77      // =========
78      /** The search helper for performing search operations. */
79      @Resource
80      protected SearchHelper searchHelper;
81  
82      /** The search engine client for interacting with OpenSearch. */
83      @Resource
84      protected SearchEngineClient searchEngineClient;
85  
86      // ===================================================================================
87      //                                                                      Search Execute
88      //                                                                      ==============
89  
90      /**
91       * Searches for documents in the search index.
92       *
93       * @param body the search parameters for querying documents
94       * @return JSON response containing search results
95       */
96      // GET /api/admin/searchlist/docs
97      // PUT /api/admin/searchlist/docs
98      @Execute
99      public JsonResponse<ApiResult> docs(final SearchBody body) {
100         validateApi(body, messages -> {});
101 
102         if (StringUtil.isBlank(body.q)) {
103             // query matches on all documents.
104             body.q = Constants.MATCHES_ALL_QUERY;
105         }
106 
107         final SearchRenderData renderData = new SearchRenderData();
108         body.initialize();
109         try {
110             searchHelper.search(body, renderData, getUserBean());
111             return asJson(new ApiDocsResponse().renderData(renderData).status(Status.OK).result());
112         } catch (final InvalidQueryException e) {
113             if (logger.isDebugEnabled()) {
114                 logger.debug("Invalid query: {}", body.q, e);
115             }
116             throwValidationErrorApi(e.getMessageCode());
117         } catch (final ResultOffsetExceededException e) {
118             if (logger.isDebugEnabled()) {
119                 logger.debug("Invalid offset: {}", body.offset, e);
120             }
121             throwValidationErrorApi(messages -> messages.addErrorsResultSizeExceeded(GLOBAL));
122         }
123 
124         throwValidationErrorApi(messages -> messages.addErrorsInvalidQueryUnknown(GLOBAL));
125         return null; // ignore
126     }
127 
128     /**
129      * Retrieves a specific document by ID.
130      *
131      * @param id the document ID to retrieve
132      * @return JSON response containing the document
133      */
134     // GET /api/admin/searchlist/doc/{doc_id}
135     @Execute
136     public JsonResponse<ApiResult> get$doc(final String id) {
137         return asJson(new ApiDocResponse().doc(searchEngineClient.getDocument(fessConfig.getIndexDocumentUpdateIndex(), builder -> {
138             builder.setQuery(QueryBuilders.termQuery(fessConfig.getIndexFieldDocId(), id));
139             return true;
140         }).orElseGet(() -> {
141             throwValidationErrorApi(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id));
142             return null;
143         })).status(Status.OK).result());
144     }
145 
146     /**
147      * Creates a new document in the search index.
148      *
149      * @param body the document data to create
150      * @return JSON response containing the created document ID
151      */
152     // POST /api/admin/searchlist/doc
153     @Execute
154     public JsonResponse<ApiResult> post$doc(final CreateBody body) {
155         validateApi(body, messages -> {});
156         if (body.doc == null) {
157             throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, "doc is required"));
158         }
159         validateFields(body.doc, this::throwValidationErrorApi);
160         body.crudMode = CrudMode.CREATE;
161         final Map<String, Object> doc = getDoc(body).map(entity -> {
162             try {
163                 entity.putAll(fessConfig.convertToStorableDoc(body.doc));
164 
165                 final String newId = ComponentUtil.getCrawlingInfoHelper().generateId(entity);
166                 entity.put(fessConfig.getIndexFieldId(), newId);
167 
168                 final String index = fessConfig.getIndexDocumentUpdateIndex();
169                 searchEngineClient.store(index, entity);
170                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
171             } catch (final Exception e) {
172                 logger.warn("Failed to add {}", entity, e);
173                 throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)));
174             }
175             return entity;
176         }).orElseGet(() -> {
177             throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL));
178             return null;
179         });
180         return asJson(
181                 new ApiUpdateResponse().id(doc.get(fessConfig.getIndexFieldDocId()).toString()).created(true).status(Status.OK).result());
182     }
183 
184     /**
185      * Updates an existing document in the search index.
186      *
187      * @param body the document data to update
188      * @return JSON response containing the updated document ID
189      */
190     // PUT /api/admin/searchlist/doc
191     @Execute
192     public JsonResponse<ApiResult> put$doc(final EditBody body) {
193         validateApi(body, messages -> {});
194         if (body.doc == null) {
195             throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, "doc is required"));
196         }
197         validateFields(body.doc, this::throwValidationErrorApi);
198         body.crudMode = CrudMode.EDIT;
199         final Map<String, Object> doc = getDoc(body).map(entity -> {
200             final String index = fessConfig.getIndexDocumentUpdateIndex();
201             try {
202                 entity.putAll(fessConfig.convertToStorableDoc(body.doc));
203 
204                 final String newId = ComponentUtil.getCrawlingInfoHelper().generateId(entity);
205                 final String oldId = (String) entity.get(fessConfig.getIndexFieldId());
206                 if (!newId.equals(oldId)) {
207                     entity.put(fessConfig.getIndexFieldId(), newId);
208                     entity.remove(fessConfig.getIndexFieldVersion());
209                     final Number seqNo = (Number) entity.remove(fessConfig.getIndexFieldSeqNo());
210                     final Number primaryTerm = (Number) entity.remove(fessConfig.getIndexFieldPrimaryTerm());
211                     if (seqNo != null && primaryTerm != null && oldId != null) {
212                         searchEngineClient.delete(index, oldId, seqNo, primaryTerm);
213                     }
214                 }
215 
216                 searchEngineClient.store(index, entity);
217                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
218             } catch (final Exception e) {
219                 logger.warn("Failed to update {}", entity, e);
220                 throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)));
221             }
222             return entity;
223         }).orElseGet(() -> {
224             throwValidationErrorApi(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, body.doc.toString()));
225             return null;
226         });
227         return asJson(
228                 new ApiUpdateResponse().id(doc.get(fessConfig.getIndexFieldDocId()).toString()).created(false).status(Status.OK).result());
229     }
230 
231     /**
232      * Deletes a document by ID from the search index.
233      *
234      * @param id the document ID to delete
235      * @return JSON response indicating success or failure
236      */
237     // DELETE /api/admin/searchlist/doc/{doc_id}
238     @Execute
239     public JsonResponse<ApiResult> delete$doc(final String id) {
240         try {
241             final QueryBuilder query = QueryBuilders.termQuery(fessConfig.getIndexFieldDocId(), id);
242             searchEngineClient.deleteByQuery(fessConfig.getIndexDocumentUpdateIndex(), query);
243             saveInfo(messages -> messages.addSuccessDeleteDocFromIndex(GLOBAL));
244         } catch (final Exception e) {
245             logger.warn("Failed to process a request.", e);
246             throwValidationErrorApi(messages -> messages.addErrorsFailedToDeleteDocInAdmin(GLOBAL));
247         }
248         return asJson(new ApiResponse().status(Status.OK).result());
249     }
250 
251     /**
252      * Deletes documents matching the given query from the search index.
253      *
254      * @param body the search parameters defining which documents to delete
255      * @return JSON response containing the count of deleted documents
256      */
257     // DELETE /api/admin/searchlist/query
258     @Execute
259     public JsonResponse<ApiResult> delete$query(final SearchBody body) {
260         validateApi(body, messages -> {});
261 
262         if (StringUtil.isBlank(body.q)) {
263             throwValidationErrorApi(messages -> messages.addErrorsInvalidQueryUnknown(GLOBAL));
264         }
265         try {
266             final long count = searchHelper.deleteByQuery(request, body);
267             return asJson(new ApiDeleteResponse().count(count).status(Status.OK).result());
268         } catch (final InvalidQueryException e) {
269             if (logger.isDebugEnabled()) {
270                 logger.debug(e.getMessage(), e);
271             }
272             throwValidationErrorApi(e.getMessageCode());
273         } catch (final ResultOffsetExceededException e) {
274             if (logger.isDebugEnabled()) {
275                 logger.debug(e.getMessage(), e);
276             }
277             throwValidationErrorApi(messages -> messages.addErrorsResultSizeExceeded(GLOBAL));
278         }
279 
280         throwValidationErrorApi(messages -> messages.addErrorsInvalidQueryUnknown(GLOBAL));
281         return null; // ignore
282     }
283 }