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.backup;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.io.BufferedReader;
21  import java.io.BufferedWriter;
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.InputStreamReader;
30  import java.io.OutputStream;
31  import java.io.OutputStreamWriter;
32  import java.io.Writer;
33  import java.nio.file.Files;
34  import java.nio.file.Path;
35  import java.time.LocalDateTime;
36  import java.time.ZoneId;
37  import java.time.format.DateTimeFormatter;
38  import java.util.Arrays;
39  import java.util.HashMap;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.concurrent.atomic.AtomicBoolean;
43  import java.util.function.Consumer;
44  import java.util.stream.Collectors;
45  
46  import org.apache.commons.text.StringEscapeUtils;
47  import org.apache.logging.log4j.LogManager;
48  import org.apache.logging.log4j.Logger;
49  import org.codelibs.core.exception.IORuntimeException;
50  import org.codelibs.core.io.CopyUtil;
51  import org.codelibs.core.lang.StringUtil;
52  import org.codelibs.core.misc.Pair;
53  import org.codelibs.curl.CurlResponse;
54  import org.codelibs.fess.Constants;
55  import org.codelibs.fess.annotation.Secured;
56  import org.codelibs.fess.app.web.base.FessAdminAction;
57  import org.codelibs.fess.helper.SystemHelper;
58  import org.codelibs.fess.mylasta.direction.FessConfig;
59  import org.codelibs.fess.opensearch.config.exbhv.FileConfigBhv;
60  import org.codelibs.fess.opensearch.config.exbhv.LabelTypeBhv;
61  import org.codelibs.fess.opensearch.config.exbhv.WebConfigBhv;
62  import org.codelibs.fess.opensearch.log.exbhv.ClickLogBhv;
63  import org.codelibs.fess.opensearch.log.exbhv.FavoriteLogBhv;
64  import org.codelibs.fess.opensearch.log.exbhv.SearchLogBhv;
65  import org.codelibs.fess.opensearch.log.exbhv.UserInfoBhv;
66  import org.codelibs.fess.opensearch.log.exentity.ClickLog;
67  import org.codelibs.fess.opensearch.log.exentity.FavoriteLog;
68  import org.codelibs.fess.opensearch.log.exentity.SearchLog;
69  import org.codelibs.fess.opensearch.log.exentity.UserInfo;
70  import org.codelibs.fess.util.ComponentUtil;
71  import org.codelibs.fess.util.GsaConfigParser;
72  import org.codelibs.fess.util.RenderDataUtil;
73  import org.codelibs.fess.util.ResourceUtil;
74  import org.codelibs.fess.util.SearchEngineUtil;
75  import org.dbflute.bhv.readable.EntityRowHandler;
76  import org.lastaflute.core.magic.async.AsyncManager;
77  import org.lastaflute.web.Execute;
78  import org.lastaflute.web.response.ActionResponse;
79  import org.lastaflute.web.response.HtmlResponse;
80  import org.lastaflute.web.response.StreamResponse;
81  import org.lastaflute.web.ruts.process.ActionRuntime;
82  import org.xml.sax.InputSource;
83  
84  import com.fasterxml.jackson.core.type.TypeReference;
85  import com.fasterxml.jackson.databind.ObjectMapper;
86  
87  import jakarta.annotation.Resource;
88  
89  /**
90   * Admin action for Backup management.
91   *
92   */
93  public class AdminBackupAction extends FessAdminAction {
94  
95      /**
96       * Default constructor.
97       */
98      public AdminBackupAction() {
99          super();
100     }
101 
102     /**
103      * The role for this action.
104      */
105     public static final String ROLE = "admin-backup";
106 
107     private static final Logger logger = LogManager.getLogger(AdminBackupAction.class);
108 
109     /**
110      * The ndjson extension.
111      */
112     public static final String NDJSON_EXTENTION = ".ndjson";
113 
114     private static final DateTimeFormatter ISO_8601_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
115 
116     @Resource
117     private AsyncManager asyncManager;
118 
119     @Resource
120     private WebConfigBhv webConfigBhv;
121 
122     @Resource
123     private FileConfigBhv fileConfigBhv;
124 
125     @Resource
126     private LabelTypeBhv labelTypeBhv;
127 
128     @Override
129     protected void setupHtmlData(final ActionRuntime runtime) {
130         super.setupHtmlData(runtime);
131         runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameBackup()));
132     }
133 
134     @Override
135     protected String getActionRole() {
136         return ROLE;
137     }
138 
139     /**
140      * Show the index page.
141      * @return The HTML response.
142      */
143     @Execute
144     @Secured({ ROLE, ROLE + VIEW })
145     public HtmlResponse index() {
146         saveToken();
147         return asListHtml();
148     }
149 
150     /**
151      * Upload a file.
152      * @param form The upload form.
153      * @return The HTML response.
154      */
155     @Execute
156     @Secured({ ROLE })
157     public HtmlResponse upload(final UploadForm form) {
158         validate(form, messages -> {}, this::asListHtml);
159         verifyToken(this::asListHtml);
160         final String fileName = form.bulkFile.getFileName();
161 
162         if (logger.isDebugEnabled()) {
163             logger.debug("Backup file upload initiated: fileName={}", fileName);
164         }
165 
166         final File tempFile = ComponentUtil.getSystemHelper().createTempFile("fess_restore_", ".tmp");
167         try (final InputStream in = form.bulkFile.getInputStream(); final OutputStream out = new FileOutputStream(tempFile)) {
168             CopyUtil.copy(in, out);
169             asyncImport(fileName, tempFile);
170 
171             if (logger.isInfoEnabled()) {
172                 logger.info("Backup file uploaded successfully and queued for import: fileName={}, tempFile={}", fileName,
173                         tempFile.getAbsolutePath());
174             }
175         } catch (final IOException e) {
176             logger.warn("Failed to upload backup file: fileName={}, error={}", fileName, e.getMessage(), e);
177             if (tempFile.exists() && !tempFile.delete()) {
178                 logger.warn("Failed to delete temporary file: {}", tempFile.getAbsolutePath());
179             }
180             throwValidationError(messages -> messages.addErrorsFileIsNotSupported(GLOBAL, fileName), this::asListHtml);
181         }
182         saveInfo(messages -> messages.addSuccessBulkProcessStarted(GLOBAL));
183         return redirect(getClass()); // no-op
184     }
185 
186     /**
187      * Import the file asynchronously.
188      * @param fileName The file name.
189      * @param tempFile The temporary file.
190      */
191     protected void asyncImport(final String fileName, final File tempFile) {
192         final int fileType;
193         if (fileName.startsWith("system") && fileName.endsWith(".properties")) {
194             fileType = 1;
195         } else if (fileName.startsWith("gsa") && fileName.endsWith(".xml")) {
196             fileType = 2;
197         } else if (fileName.endsWith(".bulk")) {
198             fileType = 3;
199         } else if (fileName.startsWith("fess") && fileName.endsWith(".json")) {
200             fileType = 4;
201         } else if (fileName.startsWith("doc") && fileName.endsWith(".json")) {
202             fileType = 5;
203         } else {
204             throwValidationError(messages -> messages.addErrorsFileIsNotSupported(GLOBAL, fileName), this::asListHtml);
205             return;
206         }
207 
208         asyncManager.async(() -> {
209             switch (fileType) {
210             case 1:
211                 importSystemProperties(fileName, tempFile);
212                 break;
213             case 2:
214                 importGsaXml(fileName, tempFile);
215                 break;
216             case 3:
217                 importBulk(fileName, tempFile);
218                 break;
219             case 4:
220                 importFessJson(fileName, tempFile);
221                 break;
222             case 5:
223                 importDocJson(fileName, tempFile);
224                 break;
225             default:
226                 break;
227             }
228         });
229     }
230 
231     private void importBulk(final String fileName, final File tempFile) {
232         if (logger.isDebugEnabled()) {
233             logger.debug("Bulk data import started: fileName={}", fileName);
234         }
235 
236         final ObjectMapper mapper = new ObjectMapper();
237         final AtomicBoolean resetJobs = new AtomicBoolean(false);
238         try (CurlResponse response = ComponentUtil.getCurlHelper().post("/_bulk").onConnect((req, con) -> {
239             con.setDoOutput(true);
240             try (final BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(tempFile)));
241                     final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(con.getOutputStream(), Constants.CHARSET_UTF_8))) {
242                 String line;
243                 while ((line = br.readLine()) != null) {
244                     if (StringUtil.isNotBlank(line)) {
245                         final Map<String, Map<String, String>> dataObj;
246                         if (line.contains("\"_index\"") || line.contains("\"_type\"")) {
247                             dataObj = parseObject(mapper, line);
248                         } else {
249                             dataObj = null;
250                         }
251                         if (dataObj != null) {
252                             final Map<String, String> indexObj = dataObj.get("index");
253                             if (indexObj != null) {
254                                 if (indexObj.containsKey("_type")) {
255                                     indexObj.remove("_type");
256                                 }
257                                 final String index = indexObj.get("_index");
258                                 if (index != null) {
259                                     if (index.startsWith(".fess")) {
260                                         indexObj.put("_index", index.substring(1));
261                                     }
262                                     if (index.endsWith("scheduled_job")) {
263                                         resetJobs.set(true);
264                                     }
265                                 }
266                                 bw.write(mapper.writeValueAsString(dataObj));
267                             } else {
268                                 bw.write(line);
269                             }
270                         } else {
271                             bw.write(line);
272                         }
273                     }
274                     bw.write("\n");
275                 }
276                 bw.flush();
277             } catch (IOException e) {
278                 throw new IORuntimeException(e);
279             }
280         }).execute()) {
281             if (logger.isDebugEnabled()) {
282                 logger.debug("Bulk Response:\n{}", response.getContentAsString());
283             }
284             systemHelper.reloadConfiguration(resetJobs.get());
285 
286             if (logger.isInfoEnabled()) {
287                 logger.info("Bulk data import completed successfully: fileName={}, resetJobs={}", fileName, resetJobs.get());
288             }
289         } catch (final Exception e) {
290             logger.warn("Failed to import bulk file: fileName={}, error={}", fileName, e.getMessage(), e);
291         } finally {
292             deleteTempFile(tempFile);
293         }
294     }
295 
296     private void importGsaXml(final String fileName, final File tempFile) {
297         if (logger.isDebugEnabled()) {
298             logger.debug("GSA XML import started: fileName={}", fileName);
299         }
300 
301         final GsaConfigParser configParser = ComponentUtil.getComponent(GsaConfigParser.class);
302         try (final InputStream in = new FileInputStream(tempFile)) {
303             configParser.parse(new InputSource(in));
304         } catch (final IOException e) {
305             logger.warn("Failed to read GSA XML file: fileName={}, error={}", fileName, e.getMessage(), e);
306             deleteTempFile(tempFile);
307             return;
308         }
309 
310         try {
311             configParser.getWebConfig().ifPresent(c -> webConfigBhv.insert(c));
312             configParser.getFileConfig().ifPresent(c -> fileConfigBhv.insert(c));
313             labelTypeBhv.batchInsert(Arrays.stream(configParser.getLabelTypes()).collect(Collectors.toList()));
314 
315             if (logger.isInfoEnabled()) {
316                 logger.info("GSA XML import completed successfully: fileName={}", fileName);
317             }
318         } catch (final Exception e) {
319             logger.warn("Failed to insert GSA XML data into database: fileName={}, error={}", fileName, e.getMessage(), e);
320         } finally {
321             deleteTempFile(tempFile);
322         }
323     }
324 
325     private void importSystemProperties(final String fileName, final File tempFile) {
326         if (logger.isDebugEnabled()) {
327             logger.debug("System properties import started: fileName={}", fileName);
328         }
329 
330         try (final InputStream in = new FileInputStream(tempFile)) {
331             ComponentUtil.getSystemProperties().load(in);
332 
333             if (logger.isInfoEnabled()) {
334                 logger.info("System properties import completed successfully: fileName={}", fileName);
335             }
336         } catch (final IOException e) {
337             logger.warn("Failed to import system.properties file: fileName={}, error={}", fileName, e.getMessage(), e);
338         } finally {
339             deleteTempFile(tempFile);
340         }
341     }
342 
343     private void importFessJson(final String fileName, final File tempFile) {
344         if (logger.isDebugEnabled()) {
345             logger.debug("Fess JSON import started: fileName={}", fileName);
346         }
347 
348         try (final InputStream in = new FileInputStream(tempFile); final OutputStream out = Files.newOutputStream(getFessJsonPath())) {
349             CopyUtil.copy(in, out);
350 
351             if (logger.isInfoEnabled()) {
352                 logger.info("Fess JSON import completed successfully: fileName={}", fileName);
353             }
354         } catch (final IOException e) {
355             logger.warn("Failed to import fess.json file: fileName={}, error={}", fileName, e.getMessage(), e);
356         } finally {
357             deleteTempFile(tempFile);
358         }
359     }
360 
361     private void importDocJson(final String fileName, final File tempFile) {
362         if (logger.isDebugEnabled()) {
363             logger.debug("Doc JSON import started: fileName={}", fileName);
364         }
365 
366         try (final InputStream in = new FileInputStream(tempFile); final OutputStream out = Files.newOutputStream(getDocJsonPath())) {
367             CopyUtil.copy(in, out);
368 
369             if (logger.isInfoEnabled()) {
370                 logger.info("Doc JSON import completed successfully: fileName={}", fileName);
371             }
372         } catch (final IOException e) {
373             logger.warn("Failed to import doc.json file: fileName={}, error={}", fileName, e.getMessage(), e);
374         } finally {
375             deleteTempFile(tempFile);
376         }
377     }
378 
379     private Map<String, Map<String, String>> parseObject(final ObjectMapper mapper, final String line) {
380         try {
381             return mapper.readValue(line, new TypeReference<Map<String, Map<String, String>>>() {
382             });
383         } catch (final Exception e) {
384             if (logger.isDebugEnabled()) {
385                 logger.debug("Failed to parse {}", line, e);
386             }
387             return null;
388         }
389     }
390 
391     /**
392      * Download a file.
393      * @param id The ID of the file.
394      * @return The action response.
395      */
396     @Execute
397     @Secured({ ROLE, ROLE + VIEW })
398     public ActionResponse download(final String id) {
399         if (logger.isDebugEnabled()) {
400             logger.debug("Backup download requested: id={}", id);
401         }
402 
403         if (stream(fessConfig.getIndexBackupAllTargets()).get(stream -> stream.anyMatch(s -> s.equals(id)))) {
404             if ("system.properties".equals(id)) {
405                 return asStream(id).contentTypeOctetStream().stream(out -> {
406                     try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
407                         ComponentUtil.getSystemProperties().store(baos, id);
408                         try (final InputStream in = new ByteArrayInputStream(baos.toByteArray())) {
409                             out.write(in);
410                         }
411                     }
412                 });
413             }
414             if (id.endsWith(NDJSON_EXTENTION)) {
415                 final String name = id.substring(0, id.length() - NDJSON_EXTENTION.length());
416                 if ("search_log".equals(name)) {
417                     return writeNdjsonResponse(id, getSearchLogNdjsonWriteCall());
418                 }
419                 if ("user_info".equals(name)) {
420                     return writeNdjsonResponse(id, getUserInfoNdjsonWriteCall());
421                 }
422                 if ("click_log".equals(name)) {
423                     return writeNdjsonResponse(id, getClickLogNdjsonWriteCall());
424                 }
425                 if ("favorite_log".equals(name)) {
426                     return writeNdjsonResponse(id, getFavoriteLogNdjsonWriteCall());
427                 }
428             } else if ("fess.json".equals(id)) {
429                 return asStream(id).contentTypeOctetStream().stream(out -> {
430                     final Path fessJsonPath = getFessJsonPath();
431                     try (final InputStream in = Files.newInputStream(fessJsonPath)) {
432                         out.write(in);
433                     }
434                 });
435             } else if ("doc.json".equals(id)) {
436                 return asStream(id).contentTypeOctetStream().stream(out -> {
437                     final Path fessJsonPath = getDocJsonPath();
438                     try (final InputStream in = Files.newInputStream(fessJsonPath)) {
439                         out.write(in);
440                     }
441                 });
442             } else {
443                 String index;
444                 final String filename;
445                 if (id.endsWith(".bulk")) {
446                     index = id.substring(0, id.length() - 5);
447                     filename = id;
448                 } else {
449                     index = id;
450                     filename = id + ".bulk";
451                 }
452                 if ("fess_config".equals(index)) {
453                     index = fessConfig.getIndexConfigIndex();
454                 } else if ("fess_user".equals(index)) {
455                     index = fessConfig.getIndexUserIndex();
456                 } else if ("fess_basic_config".equals(index) && !"fess_config".equals(fessConfig.getIndexConfigIndex())) {
457                     index = "basic_" + fessConfig.getIndexConfigIndex();
458                 }
459                 final String alias = index;
460                 return asStream(filename).contentTypeOctetStream().stream(out -> {
461                     try (final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out.stream(), Constants.CHARSET_UTF_8))) {
462                         SearchEngineUtil.scroll(alias, hit -> {
463                             try {
464                                 writer.write("{\"index\":{\"_index\":\"" + hit.getIndex() + "\",\"_id\":\""
465                                         + StringEscapeUtils.escapeJson(hit.getId()) + "\"}}\n");
466                                 writer.write(hit.getSourceAsString());
467                                 writer.write("\n");
468                             } catch (final IOException e) {
469                                 throw new IORuntimeException(e);
470                             }
471                             return true;
472                         });
473                         writer.flush();
474                     }
475                 });
476             }
477         }
478         throwValidationError(messages -> messages.addErrorsCouldNotFindBackupIndex(GLOBAL), this::asListHtml);
479         return redirect(getClass()); // no-op
480     }
481 
482     private Path getDocJsonPath() {
483         return ResourceUtil.getClassesPath("fess_indices", "fess", "doc.json");
484     }
485 
486     private Path getFessJsonPath() {
487         return ResourceUtil.getClassesPath("fess_indices", "fess.json");
488     }
489 
490     private StreamResponse writeNdjsonResponse(final String id, final Consumer<Writer> writeCall) {
491         return asStream(id)//
492                 .header("Pragma", "no-cache")//
493                 .header("Cache-Control", "no-cache")//
494                 .header("Expires", "Thu, 01 Dec 1994 16:00:00 GMT")//
495                 .header("Content-Type", "application/x-ndjson")//
496                 .stream(out -> {
497                     try (final Writer writer = new BufferedWriter(new OutputStreamWriter(out.stream(), Constants.CHARSET_UTF_8))) {
498                         writeCall.accept(writer);
499                         writer.flush();
500                     } catch (final Exception e) {
501                         logger.warn("Failed to write {} to response.", id, e);
502                     }
503                 });
504     }
505 
506     private static StringBuilder appendJson(final String field, final Object value, final StringBuilder buf) {
507         buf.append('"').append(StringEscapeUtils.escapeJson(field)).append('"').append(':');
508         if (value == null) {
509             buf.append("null");
510         } else if (value instanceof LocalDateTime) {
511             final String format =
512                     ((LocalDateTime) value).atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC")).format(ISO_8601_FORMATTER);
513             buf.append('"').append(StringEscapeUtils.escapeJson(format)).append('"');
514         } else if (value instanceof String[]) {
515             final String json = Arrays.stream((String[]) value)
516                     .map(s -> "\"" + StringEscapeUtils.escapeJson(s) + "\"")
517                     .collect(Collectors.joining(","));
518             buf.append('[').append(json).append(']');
519         } else if (value instanceof List) {
520             final String json = ((List<?>) value).stream()
521                     .map(s -> "\"" + StringEscapeUtils.escapeJson(s.toString()) + "\"")
522                     .collect(Collectors.joining(","));
523             buf.append('[').append(json).append(']');
524         } else if (value instanceof Map) {
525             buf.append('{');
526             final String json = ((Map<?, ?>) value).entrySet().stream().map(e -> {
527                 final StringBuilder tempBuf = new StringBuilder();
528                 appendJson(e.getKey().toString(), e.getValue(), tempBuf);
529                 return tempBuf.toString();
530             }).collect(Collectors.joining(","));
531             buf.append(json);
532             buf.append('}');
533         } else if (value instanceof Long || value instanceof Integer) {
534             buf.append(((Number) value).longValue());
535         } else if (value instanceof Number) {
536             buf.append(((Number) value).doubleValue());
537         } else {
538             buf.append('"').append(StringEscapeUtils.escapeJson(value.toString())).append('"');
539         }
540         return buf;
541     }
542 
543     /**
544      * Get the write call for search log ndjson.
545      * @return The write call.
546      */
547     public static Consumer<Writer> getSearchLogNdjsonWriteCall() {
548         final FessConfig fessConfig = ComponentUtil.getFessConfig();
549         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
550         final long timeout = fessConfig.getIndexBackupLogLoadTimeoutAsInteger().longValue();
551         return writer -> {
552             final SearchLogBhv bhv = ComponentUtil.getComponent(SearchLogBhv.class);
553             bhv.selectCursor(cb -> {
554                 cb.query().matchAll();
555                 cb.query().addOrderBy_RequestedAt_Asc();
556             }, new LogEntityRowHandler<SearchLog>() {
557                 @Override
558                 public void handle(final SearchLog entity) {
559                     final StringBuilder buf = new StringBuilder();
560                     buf.append('{');
561                     appendJson("id", entity.getId(), buf).append(',');
562                     appendJson("query-id", entity.getQueryId(), buf).append(',');
563                     appendJson("user-info-id", entity.getUserInfoId(), buf).append(',');
564                     appendJson("user-session-id", entity.getUserSessionId(), buf).append(',');
565                     appendJson("user", entity.getUser(), buf).append(',');
566                     appendJson("search-word", entity.getSearchWord(), buf).append(',');
567                     appendJson("hit-count", entity.getHitCount(), buf).append(',');
568                     appendJson("query-page-size", entity.getQueryPageSize(), buf).append(',');
569                     appendJson("query-offset", entity.getQueryOffset(), buf).append(',');
570                     appendJson("referer", entity.getReferer(), buf).append(',');
571                     appendJson("languages", entity.getLanguages(), buf).append(',');
572                     appendJson("roles", entity.getRoles(), buf).append(',');
573                     appendJson("user-agent", entity.getUserAgent(), buf).append(',');
574                     appendJson("client-ip", entity.getClientIp(), buf).append(',');
575                     appendJson("access-type", entity.getAccessType(), buf).append(',');
576                     appendJson("query-time", entity.getQueryTime(), buf).append(',');
577                     appendJson("response-time", entity.getResponseTime(), buf).append(',');
578                     appendJson("requested-at", entity.getRequestedAt(), buf).append(',');
579                     final Map<String, List<String>> searchFieldMap = entity.getSearchFieldLogList()
580                             .stream()
581                             .collect(Collectors.groupingBy(Pair::getFirst, Collectors.mapping(Pair::getSecond, Collectors.toList())));
582                     appendJson("search-field", searchFieldMap, buf).append(',');
583                     final Map<String, List<String>> requestHeaderMap = entity.getRequestHeaderList()
584                             .stream()
585                             .collect(Collectors.groupingBy(Pair::getFirst, Collectors.mapping(Pair::getSecond, Collectors.toList())));
586                     appendJson("headers", requestHeaderMap, buf);
587                     buf.append('}');
588                     buf.append('\n');
589                     try {
590                         writer.write(buf.toString());
591                     } catch (final IOException e) {
592                         throw new IORuntimeException(e);
593                     }
594                     if (!systemHelper.calibrateCpuLoad(timeout)) {
595                         breakCursor = true;
596                     }
597                 }
598             });
599         };
600     }
601 
602     /**
603      * Get the write call for user info ndjson.
604      * @return The write call.
605      */
606     public static Consumer<Writer> getUserInfoNdjsonWriteCall() {
607         final FessConfig fessConfig = ComponentUtil.getFessConfig();
608         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
609         final long timeout = fessConfig.getIndexBackupLogLoadTimeoutAsInteger().longValue();
610         return writer -> {
611             final UserInfoBhv bhv = ComponentUtil.getComponent(UserInfoBhv.class);
612             bhv.selectCursor(cb -> {
613                 cb.query().matchAll();
614                 cb.query().addOrderBy_CreatedAt_Asc();
615             }, new LogEntityRowHandler<UserInfo>() {
616                 @Override
617                 public void handle(final UserInfo entity) {
618                     final StringBuilder buf = new StringBuilder();
619                     buf.append('{');
620                     appendJson("id", entity.getId(), buf).append(',');
621                     appendJson("created-at", entity.getCreatedAt(), buf).append(',');
622                     appendJson("updated-at", entity.getUpdatedAt(), buf);
623                     buf.append('}');
624                     buf.append('\n');
625                     try {
626                         writer.write(buf.toString());
627                     } catch (final IOException e) {
628                         throw new IORuntimeException(e);
629                     }
630                     if (!systemHelper.calibrateCpuLoad(timeout)) {
631                         breakCursor = true;
632                     }
633                 }
634             });
635         };
636     }
637 
638     /**
639      * Get the write call for favorite log ndjson.
640      * @return The write call.
641      */
642     public static Consumer<Writer> getFavoriteLogNdjsonWriteCall() {
643         final FessConfig fessConfig = ComponentUtil.getFessConfig();
644         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
645         final long timeout = fessConfig.getIndexBackupLogLoadTimeoutAsInteger().longValue();
646         return writer -> {
647             final FavoriteLogBhv bhv = ComponentUtil.getComponent(FavoriteLogBhv.class);
648             bhv.selectCursor(cb -> {
649                 cb.query().matchAll();
650                 cb.query().addOrderBy_CreatedAt_Asc();
651             }, new LogEntityRowHandler<FavoriteLog>() {
652                 @Override
653                 public void handle(final FavoriteLog entity) {
654                     final StringBuilder buf = new StringBuilder();
655                     buf.append('{');
656                     appendJson("id", entity.getId(), buf).append(',');
657                     appendJson("created-at", entity.getCreatedAt(), buf).append(',');
658                     appendJson("query-id", entity.getQueryId(), buf).append(',');
659                     appendJson("user-info-id", entity.getUserInfoId(), buf).append(',');
660                     appendJson("doc-id", entity.getDocId(), buf).append(',');
661                     appendJson("url", entity.getUrl(), buf);
662                     buf.append('}');
663                     buf.append('\n');
664                     try {
665                         writer.write(buf.toString());
666                     } catch (final IOException e) {
667                         throw new IORuntimeException(e);
668                     }
669                     if (!systemHelper.calibrateCpuLoad(timeout)) {
670                         breakCursor = true;
671                     }
672                 }
673             });
674         };
675     }
676 
677     /**
678      * Get the write call for click log ndjson.
679      * @return The write call.
680      */
681     public static Consumer<Writer> getClickLogNdjsonWriteCall() {
682         final FessConfig fessConfig = ComponentUtil.getFessConfig();
683         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
684         final long timeout = fessConfig.getIndexBackupLogLoadTimeoutAsInteger().longValue();
685         return writer -> {
686             final ClickLogBhv bhv = ComponentUtil.getComponent(ClickLogBhv.class);
687             bhv.selectCursor(cb -> {
688                 cb.query().matchAll();
689                 cb.query().addOrderBy_RequestedAt_Asc();
690             }, new LogEntityRowHandler<ClickLog>() {
691                 @Override
692                 public void handle(final ClickLog entity) {
693                     final StringBuilder buf = new StringBuilder();
694                     buf.append('{');
695                     appendJson("id", entity.getId(), buf).append(',');
696                     appendJson("query-id", entity.getQueryId(), buf).append(',');
697                     appendJson("user-session-id", entity.getUserSessionId(), buf).append(',');
698                     appendJson("doc-id", entity.getDocId(), buf).append(',');
699                     appendJson("url", entity.getUrl(), buf).append(',');
700                     appendJson("order", entity.getOrder(), buf).append(',');
701                     appendJson("query-requested-at", entity.getQueryRequestedAt(), buf).append(',');
702                     appendJson("requested-at", entity.getRequestedAt(), buf);
703                     buf.append('}');
704                     buf.append('\n');
705                     try {
706                         writer.write(buf.toString());
707                     } catch (final IOException e) {
708                         throw new IORuntimeException(e);
709                     }
710                     if (!systemHelper.calibrateCpuLoad(timeout)) {
711                         breakCursor = true;
712                     }
713                 }
714             });
715         };
716     }
717 
718     /**
719      * Get the backup items.
720      * @return The backup items.
721      */
722     public static List<Map<String, String>> getBackupItems() {
723         final FessConfig fessConfig = ComponentUtil.getFessConfig();
724         return stream(fessConfig.getIndexBackupAllTargets()).get(stream -> stream.map(name -> {
725             final Map<String, String> map = new HashMap<>();
726             map.put("id", name);
727             map.put("name", name);
728             return map;
729         }).collect(Collectors.toList()));
730     }
731 
732     private HtmlResponse asListHtml() {
733         return asHtml(path_AdminBackup_AdminBackupJsp).useForm(UploadForm.class)
734                 .renderWith(data -> RenderDataUtil.register(data, "backupItems", getBackupItems()));
735     }
736 
737     private void deleteTempFile(final File tempFile) {
738         if (tempFile != null && !tempFile.delete()) {
739             logger.warn("Failed to delete {}", tempFile.getAbsolutePath());
740         }
741     }
742 
743     private static abstract class LogEntityRowHandler<ENTITY> implements EntityRowHandler<ENTITY> {
744         protected boolean breakCursor = false;
745 
746         @Override
747         public boolean isBreakCursor() {
748             return breakCursor;
749         }
750     }
751 }