1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
59
60
61 public class AdminSearchlistAction extends FessAdminAction {
62
63
64
65
66 public AdminSearchlistAction() {
67 super();
68 }
69
70
71 public static final String ROLE = "admin-searchlist";
72
73
74
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
84
85
86
87 @Resource
88 protected SearchEngineClient searchEngineClient;
89
90
91 @Resource
92 protected QueryHelper queryHelper;
93
94
95 @Resource
96 protected SearchHelper searchHelper;
97
98
99 @Resource
100 protected HttpServletRequest request;
101
102
103 private CreateForm currentForm;
104
105
106 public List<Map<String, Object>> documentItems;
107
108
109 public String pageSize;
110
111
112 public String currentPageNumber;
113
114
115 public String allRecordCount;
116
117
118 public String allPageCount;
119
120
121 public boolean existNextPage;
122
123
124 public boolean existPrevPage;
125
126
127 public String currentStartRecordNumber;
128
129
130 public String currentEndRecordNumber;
131
132
133 public List<String> pageNumberList;
134
135
136 public String execTime;
137
138
139
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
155
156
157
158
159
160
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
172
173
174
175
176 protected HtmlResponse doSearch(final ListForm form) {
177 validate(form, messages -> {}, this::asListHtml);
178
179 if (StringUtil.isBlank(form.q)) {
180
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;
205 }
206
207
208
209
210
211
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
222
223
224
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
235
236
237
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
248
249
250
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
261
262
263
264
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
281
282
283
284
285
286
287
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
308
309
310
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
331
332
333
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
350
351
352
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
373
374
375
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
408
409
410
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
452
453
454
455
456
457
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
506
507
508
509
510
511
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
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
599
600 protected static class WebRenderData extends SearchRenderData {
601
602
603
604
605 protected WebRenderData() {
606 super();
607 }
608
609
610
611
612
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 }