View Javadoc
1   /*
2    * Copyright 2012-2021 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.api.json;
17  
18  import java.io.IOException;
19  import java.net.URLDecoder;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Locale;
25  import java.util.Map;
26  
27  import javax.annotation.PostConstruct;
28  import javax.servlet.FilterChain;
29  import javax.servlet.ServletException;
30  import javax.servlet.http.HttpServletRequest;
31  import javax.servlet.http.HttpServletResponse;
32  
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.Logger;
35  import org.codelibs.core.exception.IORuntimeException;
36  import org.codelibs.core.lang.StringUtil;
37  import org.codelibs.fesen.script.Script;
38  import org.codelibs.fess.Constants;
39  import org.codelibs.fess.api.BaseJsonApiManager;
40  import org.codelibs.fess.app.service.FavoriteLogService;
41  import org.codelibs.fess.entity.FacetInfo;
42  import org.codelibs.fess.entity.GeoInfo;
43  import org.codelibs.fess.entity.HighlightInfo;
44  import org.codelibs.fess.entity.PingResponse;
45  import org.codelibs.fess.entity.SearchRenderData;
46  import org.codelibs.fess.entity.SearchRequestParams;
47  import org.codelibs.fess.entity.SearchRequestParams.SearchRequestType;
48  import org.codelibs.fess.es.client.SearchEngineClient;
49  import org.codelibs.fess.exception.WebApiException;
50  import org.codelibs.fess.helper.LabelTypeHelper;
51  import org.codelibs.fess.helper.PopularWordHelper;
52  import org.codelibs.fess.helper.RelatedContentHelper;
53  import org.codelibs.fess.helper.RelatedQueryHelper;
54  import org.codelibs.fess.helper.SearchHelper;
55  import org.codelibs.fess.helper.SystemHelper;
56  import org.codelibs.fess.helper.UserInfoHelper;
57  import org.codelibs.fess.mylasta.direction.FessConfig;
58  import org.codelibs.fess.util.ComponentUtil;
59  import org.codelibs.fess.util.DocumentUtil;
60  import org.codelibs.fess.util.FacetResponse;
61  import org.codelibs.fess.util.FacetResponse.Field;
62  import org.dbflute.optional.OptionalThing;
63  
64  public class JsonApiManager extends BaseJsonApiManager {
65  
66      private static final Logger logger = LogManager.getLogger(JsonApiManager.class);
67  
68      public JsonApiManager() {
69          setPathPrefix("/json");
70      }
71  
72      @PostConstruct
73      public void register() {
74          if (logger.isInfoEnabled()) {
75              logger.info("Load {}", this.getClass().getSimpleName());
76          }
77          ComponentUtil.getWebApiManagerFactory().add(this);
78      }
79  
80      @Override
81      public boolean matches(final HttpServletRequest request) {
82          final FessConfig fessConfig = ComponentUtil.getFessConfig();
83          if (!fessConfig.isWebApiJson()) {
84              switch (getFormatType(request)) {
85              case SEARCH:
86              case LABEL:
87              case POPULARWORD:
88                  return false;
89              default:
90                  break;
91              }
92          }
93  
94          final String servletPath = request.getServletPath();
95          return servletPath.startsWith(pathPrefix);
96      }
97  
98      @Override
99      public void process(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain)
100             throws IOException, ServletException {
101         switch (getFormatType(request)) {
102         case SEARCH:
103             processSearchRequest(request, response, chain);
104             break;
105         case LABEL:
106             processLabelRequest(request, response, chain);
107             break;
108         case POPULARWORD:
109             processPopularWordRequest(request, response, chain);
110             break;
111         case FAVORITE:
112             processFavoriteRequest(request, response, chain);
113             break;
114         case FAVORITES:
115             processFavoritesRequest(request, response, chain);
116             break;
117         case PING:
118             processPingRequest(request, response, chain);
119             break;
120         case SCROLL:
121             processScrollSearchRequest(request, response, chain);
122             break;
123         default:
124             writeJsonResponse(99, StringUtil.EMPTY, "Not found.");
125             break;
126         }
127     }
128 
129     protected void processScrollSearchRequest(final HttpServletRequest request, final HttpServletResponse response,
130             final FilterChain chain) {
131         final SearchHelper searchHelper = ComponentUtil.getSearchHelper();
132         final FessConfig fessConfig = ComponentUtil.getFessConfig();
133 
134         if (!fessConfig.isAcceptedSearchReferer(request.getHeader("referer"))) {
135             writeJsonResponse(99, StringUtil.EMPTY, "Referer is invalid.");
136             return;
137         }
138 
139         if (!fessConfig.isApiSearchScroll()) {
140             writeJsonResponse(99, StringUtil.EMPTY, "Scroll Search is not available.");
141             return;
142         }
143 
144         final StringBuilder buf = new StringBuilder(1000);
145         request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, Constants.SEARCH_LOG_ACCESS_TYPE_JSON);
146         final JsonRequestParams params = new JsonRequestParams(request, fessConfig);
147         try {
148             response.setContentType("application/x-ndjson; charset=UTF-8");
149             final long count = searchHelper.scrollSearch(params, doc -> {
150                 buf.setLength(0);
151                 buf.append('{');
152                 boolean first2 = true;
153                 for (final Map.Entry<String, Object> entry : doc.entrySet()) {
154                     final String name = entry.getKey();
155                     if (StringUtil.isNotBlank(name) && entry.getValue() != null) {
156                         if (!first2) {
157                             buf.append(',');
158                         } else {
159                             first2 = false;
160                         }
161                         buf.append(escapeJson(name));
162                         buf.append(':');
163                         buf.append(escapeJson(entry.getValue()));
164                     }
165                 }
166                 buf.append('}');
167                 buf.append('\n');
168                 try {
169                     response.getWriter().print(buf.toString());
170                 } catch (final IOException e) {
171                     throw new IORuntimeException(e);
172                 }
173                 return true;
174             }, OptionalThing.empty());
175             response.flushBuffer();
176             if (logger.isDebugEnabled()) {
177                 logger.debug("Loaded {} docs", count);
178             }
179         } catch (final Exception e) {
180             final int status = 9;
181             if (logger.isDebugEnabled()) {
182                 logger.debug("Failed to process a ping request.", e);
183             }
184             writeJsonResponse(status, null, e);
185         }
186 
187     }
188 
189     protected void processPingRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
190         final SearchEngineClient searchEngineClient = ComponentUtil.getSearchEngineClient();
191         int status;
192         Exception err = null;
193         try {
194             final PingResponse pingResponse = searchEngineClient.ping();
195             status = pingResponse.getStatus();
196             writeJsonResponse(status, "\"message\":" + pingResponse.getMessage());
197         } catch (final Exception e) {
198             status = 9;
199             err = e;
200             if (logger.isDebugEnabled()) {
201                 logger.debug("Failed to process a ping request.", e);
202             }
203             writeJsonResponse(status, null, err);
204         }
205     }
206 
207     protected void processSearchRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
208         final SearchHelper searchHelper = ComponentUtil.getSearchHelper();
209         final FessConfig fessConfig = ComponentUtil.getFessConfig();
210         final RelatedQueryHelper relatedQueryHelper = ComponentUtil.getRelatedQueryHelper();
211         final RelatedContentHelper relatedContentHelper = ComponentUtil.getRelatedContentHelper();
212 
213         int status = 0;
214         Exception err = null;
215         String query = null;
216         final StringBuilder buf = new StringBuilder(1000); // TODO replace response stream
217         request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, Constants.SEARCH_LOG_ACCESS_TYPE_JSON);
218         try {
219             final SearchRenderData data = new SearchRenderData();
220             final JsonRequestParams params = new JsonRequestParams(request, fessConfig);
221             query = params.getQuery();
222             searchHelper.search(params, data, OptionalThing.empty());
223             final String execTime = data.getExecTime();
224             final String queryTime = Long.toString(data.getQueryTime());
225             final String pageSize = Integer.toString(data.getPageSize());
226             final String currentPageNumber = Integer.toString(data.getCurrentPageNumber());
227             final String allRecordCount = Long.toString(data.getAllRecordCount());
228             final String allRecordCountRelation = data.getAllRecordCountRelation();
229             final String allPageCount = Integer.toString(data.getAllPageCount());
230             final List<Map<String, Object>> documentItems = data.getDocumentItems();
231             final FacetResponse facetResponse = data.getFacetResponse();
232             final String queryId = data.getQueryId();
233             final String highlightParams = data.getAppendHighlightParams();
234             final boolean nextPage = data.isExistNextPage();
235             final boolean prevPage = data.isExistPrevPage();
236             final long startRecordNumber = data.getCurrentStartRecordNumber();
237             final long endRecordNumber = data.getCurrentEndRecordNumber();
238             final List<String> pageNumbers = data.getPageNumberList();
239             final boolean partial = data.isPartialResults();
240             final String searchQuery = data.getSearchQuery();
241             final long requestedTime = data.getRequestedTime();
242 
243             buf.append("\"q\":");
244             buf.append(escapeJson(query));
245             buf.append(",\"query_id\":");
246             buf.append(escapeJson(queryId));
247             buf.append(",\"exec_time\":");
248             buf.append(execTime);
249             buf.append(",\"query_time\":");
250             buf.append(queryTime);
251             buf.append(',');
252             buf.append("\"page_size\":");
253             buf.append(pageSize);
254             buf.append(',');
255             buf.append("\"page_number\":");
256             buf.append(currentPageNumber);
257             buf.append(',');
258             buf.append("\"record_count\":");
259             buf.append(allRecordCount);
260             buf.append(',');
261             buf.append("\"record_count_relation\":");
262             buf.append(escapeJson(allRecordCountRelation));
263             buf.append(',');
264             buf.append("\"page_count\":");
265             buf.append(allPageCount);
266             buf.append(",\"highlight_params\":");
267             buf.append(escapeJson(highlightParams));
268             buf.append(",\"next_page\":");
269             buf.append(escapeJson(nextPage));
270             buf.append(",\"prev_page\":");
271             buf.append(escapeJson(prevPage));
272             buf.append(",\"start_record_number\":");
273             buf.append(startRecordNumber);
274             buf.append(",\"end_record_number\":");
275             buf.append(escapeJson(endRecordNumber));
276             buf.append(",\"page_numbers\":");
277             buf.append(escapeJson(pageNumbers));
278             buf.append(",\"partial\":");
279             buf.append(escapeJson(partial));
280             buf.append(",\"search_query\":");
281             buf.append(escapeJson(searchQuery));
282             buf.append(",\"requested_time\":");
283             buf.append(requestedTime);
284             final String[] relatedQueries = relatedQueryHelper.getRelatedQueries(params.getQuery());
285             buf.append(",\"related_query\":");
286             buf.append(escapeJson(relatedQueries));
287             final String[] relatedContents = relatedContentHelper.getRelatedContents(params.getQuery());
288             buf.append(",\"related_contents\":");
289             buf.append(escapeJson(relatedContents));
290             buf.append(',');
291             buf.append("\"result\":[");
292             if (!documentItems.isEmpty()) {
293                 boolean first1 = true;
294                 for (final Map<String, Object> document : documentItems) {
295                     if (!first1) {
296                         buf.append(',');
297                     } else {
298                         first1 = false;
299                     }
300                     buf.append('{');
301                     boolean first2 = true;
302                     for (final Map.Entry<String, Object> entry : document.entrySet()) {
303                         final String name = entry.getKey();
304                         if (StringUtil.isNotBlank(name) && entry.getValue() != null
305                                 && ComponentUtil.getQueryHelper().isApiResponseField(name)) {
306                             if (!first2) {
307                                 buf.append(',');
308                             } else {
309                                 first2 = false;
310                             }
311                             buf.append(escapeJson(name));
312                             buf.append(':');
313                             buf.append(escapeJson(entry.getValue()));
314                         }
315                     }
316                     buf.append('}');
317                 }
318             }
319             buf.append(']');
320             if (facetResponse != null && facetResponse.hasFacetResponse()) {
321                 // facet field
322                 buf.append(',');
323                 buf.append("\"facet_field\":[");
324                 if (facetResponse.getFieldList() != null) {
325                     boolean first1 = true;
326                     for (final Field field : facetResponse.getFieldList()) {
327                         if (!first1) {
328                             buf.append(',');
329                         } else {
330                             first1 = false;
331                         }
332                         buf.append("{\"name\":");
333                         buf.append(escapeJson(field.getName()));
334                         buf.append(",\"result\":[");
335                         boolean first2 = true;
336                         for (final Map.Entry<String, Long> entry : field.getValueCountMap().entrySet()) {
337                             if (!first2) {
338                                 buf.append(',');
339                             } else {
340                                 first2 = false;
341                             }
342                             buf.append("{\"value\":");
343                             buf.append(escapeJson(entry.getKey()));
344                             buf.append(",\"count\":");
345                             buf.append(entry.getValue());
346                             buf.append('}');
347                         }
348                         buf.append(']');
349                         buf.append('}');
350                     }
351                 }
352                 buf.append(']');
353                 // facet q
354                 buf.append(',');
355                 buf.append("\"facet_query\":[");
356                 if (facetResponse.getQueryCountMap() != null) {
357                     boolean first1 = true;
358                     for (final Map.Entry<String, Long> entry : facetResponse.getQueryCountMap().entrySet()) {
359                         if (!first1) {
360                             buf.append(',');
361                         } else {
362                             first1 = false;
363                         }
364                         buf.append("{\"value\":");
365                         buf.append(escapeJson(entry.getKey()));
366                         buf.append(",\"count\":");
367                         buf.append(entry.getValue());
368                         buf.append('}');
369                     }
370                 }
371                 buf.append(']');
372             }
373         } catch (final Exception e) {
374             status = 1;
375             err = e;
376             if (logger.isDebugEnabled()) {
377                 logger.debug("Failed to process a search request.", e);
378             }
379         }
380 
381         writeJsonResponse(status, buf.toString(), err);
382 
383     }
384 
385     protected String detailedMessage(final Throwable t) {
386         if (t == null) {
387             return "Unknown";
388         }
389         Throwable target = t;
390         if (target.getCause() == null) {
391             return target.getClass().getSimpleName() + "[" + target.getMessage() + "]";
392         }
393         final StringBuilder sb = new StringBuilder();
394         while (target != null) {
395             sb.append(target.getClass().getSimpleName());
396             if (target.getMessage() != null) {
397                 sb.append("[");
398                 sb.append(target.getMessage());
399                 sb.append("]");
400             }
401             sb.append("; ");
402             target = target.getCause();
403             if (target != null) {
404                 sb.append("nested: ");
405             }
406         }
407         return sb.toString();
408     }
409 
410     protected void processLabelRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
411         final LabelTypeHelper labelTypeHelper = ComponentUtil.getLabelTypeHelper();
412 
413         int status = 0;
414         Exception err = null;
415         final StringBuilder buf = new StringBuilder(255); // TODO replace response stream
416         try {
417             final List<Map<String, String>> labelTypeItems = labelTypeHelper.getLabelTypeItemList(SearchRequestType.JSON,
418                     request.getLocale() == null ? Locale.ROOT : request.getLocale());
419             buf.append("\"record_count\":");
420             buf.append(labelTypeItems.size());
421             if (!labelTypeItems.isEmpty()) {
422                 buf.append(',');
423                 buf.append("\"result\":[");
424                 boolean first1 = true;
425                 for (final Map<String, String> labelMap : labelTypeItems) {
426                     if (!first1) {
427                         buf.append(',');
428                     } else {
429                         first1 = false;
430                     }
431                     buf.append("{\"label\":");
432                     buf.append(escapeJson(labelMap.get(Constants.ITEM_LABEL)));
433                     buf.append(", \"value\":");
434                     buf.append(escapeJson(labelMap.get(Constants.ITEM_VALUE)));
435                     buf.append('}');
436                 }
437                 buf.append(']');
438             }
439         } catch (final Exception e) {
440             status = 1;
441             err = e;
442             if (logger.isDebugEnabled()) {
443                 logger.debug("Failed to process a label request.", e);
444             }
445         }
446 
447         writeJsonResponse(status, buf.toString(), err);
448 
449     }
450 
451     protected void processPopularWordRequest(final HttpServletRequest request, final HttpServletResponse response,
452             final FilterChain chain) {
453         if (!ComponentUtil.getFessConfig().isWebApiPopularWord()) {
454             writeJsonResponse(9, null, "Unsupported operation.");
455             return;
456         }
457 
458         final String seed = request.getParameter("seed");
459         final List<String> tagList = new ArrayList<>();
460         final String[] tags = request.getParameterValues("labels");
461         if (tags != null) {
462             tagList.addAll(Arrays.asList(tags));
463         }
464         final String key = ComponentUtil.getVirtualHostHelper().getVirtualHostKey();
465         if (StringUtil.isNotBlank(key)) {
466             tagList.add(key);
467         }
468         final String[] fields = request.getParameterValues("fields");
469         final String[] excludes = StringUtil.EMPTY_STRINGS;// TODO
470 
471         final PopularWordHelper popularWordHelper = ComponentUtil.getPopularWordHelper();
472 
473         int status = 0;
474         Exception err = null;
475         final StringBuilder buf = new StringBuilder(255); // TODO replace response stream
476         try {
477             final List<String> popularWordList = popularWordHelper.getWordList(SearchRequestType.JSON, seed,
478                     tagList.toArray(new String[tagList.size()]), null, fields, excludes);
479 
480             buf.append("\"result\":[");
481             boolean first1 = true;
482             for (final String word : popularWordList) {
483                 if (!first1) {
484                     buf.append(',');
485                 } else {
486                     first1 = false;
487                 }
488                 buf.append(escapeJson(word));
489             }
490             buf.append(']');
491         } catch (final Exception e) {
492             if (e instanceof WebApiException) {
493                 status = ((WebApiException) e).getStatusCode();
494             } else {
495                 status = 1;
496             }
497             err = e;
498             if (logger.isDebugEnabled()) {
499                 logger.debug("Failed to process a popularWord request.", e);
500             }
501         }
502 
503         writeJsonResponse(status, buf.toString(), err);
504 
505     }
506 
507     protected void processFavoriteRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
508         if (!ComponentUtil.getFessConfig().isUserFavorite()) {
509             writeJsonResponse(9, null, "Unsupported operation.");
510             return;
511         }
512 
513         final FessConfig fessConfig = ComponentUtil.getFessConfig();
514         final UserInfoHelper userInfoHelper = ComponentUtil.getUserInfoHelper();
515         final SearchHelper searchHelper = ComponentUtil.getSearchHelper();
516         final FavoriteLogService favoriteLogService = ComponentUtil.getComponent(FavoriteLogService.class);
517         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
518 
519         try {
520             final String docId = request.getParameter("docId");
521             final String queryId = request.getParameter("queryId");
522 
523             final String[] docIds = userInfoHelper.getResultDocIds(URLDecoder.decode(queryId, Constants.UTF_8));
524             if (docIds == null) {
525                 throw new WebApiException(6, "No searched urls.");
526             }
527 
528             searchHelper.getDocumentByDocId(docId, new String[] { fessConfig.getIndexFieldUrl(), fessConfig.getIndexFieldLang() },
529                     OptionalThing.empty()).ifPresent(doc -> {
530                         final String favoriteUrl = DocumentUtil.getValue(doc, fessConfig.getIndexFieldUrl(), String.class);
531                         final String userCode = userInfoHelper.getUserCode();
532 
533                         if (StringUtil.isBlank(userCode)) {
534                             throw new WebApiException(2, "No user session.");
535                         }
536                         if (StringUtil.isBlank(favoriteUrl)) {
537                             throw new WebApiException(2, "URL is null.");
538                         }
539 
540                         boolean found = false;
541                         for (final String id : docIds) {
542                             if (docId.equals(id)) {
543                                 found = true;
544                                 break;
545                             }
546                         }
547                         if (!found) {
548                             throw new WebApiException(5, "Not found: " + favoriteUrl);
549                         }
550 
551                         if (!favoriteLogService.addUrl(userCode, (userInfo, favoriteLog) -> {
552                             favoriteLog.setUserInfoId(userInfo.getId());
553                             favoriteLog.setUrl(favoriteUrl);
554                             favoriteLog.setDocId(docId);
555                             favoriteLog.setQueryId(queryId);
556                             favoriteLog.setCreatedAt(systemHelper.getCurrentTimeAsLocalDateTime());
557                         })) {
558                             throw new WebApiException(4, "Failed to add url: " + favoriteUrl);
559                         }
560 
561                         final String id = DocumentUtil.getValue(doc, fessConfig.getIndexFieldId(), String.class);
562                         searchHelper.update(id, builder -> {
563                             final Script script = ComponentUtil.getLanguageHelper().createScript(doc,
564                                     "ctx._source." + fessConfig.getIndexFieldFavoriteCount() + "+=1");
565                             builder.setScript(script);
566                             final Map<String, Object> upsertMap = new HashMap<>();
567                             upsertMap.put(fessConfig.getIndexFieldFavoriteCount(), 1);
568                             builder.setUpsert(upsertMap);
569                             builder.setRefreshPolicy(Constants.TRUE);
570                         });
571 
572                         writeJsonResponse(0, "\"result\":\"ok\"", (String) null);
573 
574                     }).orElse(() -> {
575                         throw new WebApiException(6, "Not found: " + docId);
576                     });
577 
578         } catch (final Exception e) {
579             int status;
580             if (e instanceof WebApiException) {
581                 status = ((WebApiException) e).getStatusCode();
582             } else {
583                 status = 1;
584             }
585             writeJsonResponse(status, null, e);
586             if (logger.isDebugEnabled()) {
587                 logger.debug("Failed to process a favorite request.", e);
588             }
589         }
590 
591     }
592 
593     protected void processFavoritesRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
594         if (!ComponentUtil.getFessConfig().isUserFavorite()) {
595             writeJsonResponse(9, null, "Unsupported operation.");
596             return;
597         }
598 
599         final UserInfoHelper userInfoHelper = ComponentUtil.getUserInfoHelper();
600         final FessConfig fessConfig = ComponentUtil.getFessConfig();
601         final SearchHelper searchHelper = ComponentUtil.getSearchHelper();
602         final FavoriteLogService favoriteLogService = ComponentUtil.getComponent(FavoriteLogService.class);
603 
604         int status = 0;
605         String body = null;
606         Exception err = null;
607 
608         try {
609             final String queryId = request.getParameter("queryId");
610             final String userCode = userInfoHelper.getUserCode();
611 
612             if (StringUtil.isBlank(userCode)) {
613                 throw new WebApiException(2, "No user session.");
614             }
615             if (StringUtil.isBlank(queryId)) {
616                 throw new WebApiException(3, "Query ID is null.");
617             }
618 
619             final String[] docIds = userInfoHelper.getResultDocIds(queryId);
620             final List<Map<String, Object>> docList = searchHelper.getDocumentListByDocIds(docIds, new String[] {
621                     fessConfig.getIndexFieldUrl(), fessConfig.getIndexFieldDocId(), fessConfig.getIndexFieldFavoriteCount() },
622                     OptionalThing.empty(), SearchRequestType.JSON);
623             List<String> urlList = new ArrayList<>(docList.size());
624             for (final Map<String, Object> doc : docList) {
625                 final String urlObj = DocumentUtil.getValue(doc, fessConfig.getIndexFieldUrl(), String.class);
626                 if (urlObj != null) {
627                     urlList.add(urlObj);
628                 }
629             }
630             urlList = favoriteLogService.getUrlList(userCode, urlList);
631             final List<String> docIdList = new ArrayList<>(urlList.size());
632             for (final Map<String, Object> doc : docList) {
633                 final String urlObj = DocumentUtil.getValue(doc, fessConfig.getIndexFieldUrl(), String.class);
634                 if (urlObj != null && urlList.contains(urlObj)) {
635                     final String docIdObj = DocumentUtil.getValue(doc, fessConfig.getIndexFieldDocId(), String.class);
636                     if (docIdObj != null) {
637                         docIdList.add(docIdObj);
638                     }
639                 }
640             }
641 
642             final StringBuilder buf = new StringBuilder(255); // TODO replace response stream
643             buf.append("\"num\":").append(docIdList.size());
644             buf.append(", \"doc_ids\":[");
645             if (!docIdList.isEmpty()) {
646                 for (int i = 0; i < docIdList.size(); i++) {
647                     if (i > 0) {
648                         buf.append(',');
649                     }
650                     buf.append(escapeJson(docIdList.get(i)));
651                 }
652             }
653             buf.append(']');
654             body = buf.toString();
655         } catch (final Exception e) {
656             if (e instanceof WebApiException) {
657                 status = ((WebApiException) e).getStatusCode();
658             } else {
659                 status = 1;
660             }
661 
662             err = e;
663             if (logger.isDebugEnabled()) {
664                 logger.debug("Failed to process a favorites request.", e);
665             }
666         }
667 
668         writeJsonResponse(status, body, err);
669 
670     }
671 
672     protected static class JsonRequestParams extends SearchRequestParams {
673 
674         private final HttpServletRequest request;
675 
676         private final FessConfig fessConfig;
677 
678         private int startPosition = -1;
679 
680         private int pageSize = -1;
681 
682         protected JsonRequestParams(final HttpServletRequest request, final FessConfig fessConfig) {
683             this.request = request;
684             this.fessConfig = fessConfig;
685         }
686 
687         @Override
688         public String getTrackTotalHits() {
689             return request.getParameter(Constants.TRACK_TOTAL_HITS);
690         }
691 
692         @Override
693         public String getQuery() {
694             return request.getParameter("q");
695         }
696 
697         @Override
698         public String[] getExtraQueries() {
699             return getParamValueArray(request, "ex_q");
700         }
701 
702         @Override
703         public Map<String, String[]> getFields() {
704             final Map<String, String[]> fields = new HashMap<>();
705             for (final Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
706                 final String key = entry.getKey();
707                 if (key.startsWith("fields.")) {
708                     final String[] value = simplifyArray(entry.getValue());
709                     fields.put(key.substring("fields.".length()), value);
710                 }
711             }
712             return fields;
713         }
714 
715         @Override
716         public Map<String, String[]> getConditions() {
717             final Map<String, String[]> conditions = new HashMap<>();
718             for (final Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
719                 final String key = entry.getKey();
720                 if (key.startsWith("as.")) {
721                     final String[] value = simplifyArray(entry.getValue());
722                     conditions.put(key.substring("as.".length()), value);
723                 }
724             }
725             return conditions;
726         }
727 
728         @Override
729         public String[] getLanguages() {
730             return getParamValueArray(request, "lang");
731         }
732 
733         @Override
734         public GeoInfo getGeoInfo() {
735             return createGeoInfo(request);
736         }
737 
738         @Override
739         public FacetInfo getFacetInfo() {
740             return createFacetInfo(request);
741         }
742 
743         @Override
744         public String getSort() {
745             return request.getParameter("sort");
746         }
747 
748         @Override
749         public int getStartPosition() {
750             if (startPosition != -1) {
751                 return startPosition;
752             }
753 
754             final String start = request.getParameter("start");
755             if (StringUtil.isBlank(start)) {
756                 startPosition = fessConfig.getPagingSearchPageStartAsInteger();
757             } else {
758                 try {
759                     startPosition = Integer.parseInt(start);
760                 } catch (final NumberFormatException e) {
761                     startPosition = fessConfig.getPagingSearchPageStartAsInteger();
762                 }
763             }
764             return startPosition;
765         }
766 
767         @Override
768         public int getPageSize() {
769             if (pageSize != -1) {
770                 return pageSize;
771             }
772 
773             final String num = request.getParameter("num");
774             if (StringUtil.isBlank(num)) {
775                 pageSize = fessConfig.getPagingSearchPageSizeAsInteger();
776             } else {
777                 try {
778                     pageSize = Integer.parseInt(num);
779                     if (pageSize > fessConfig.getPagingSearchPageMaxSizeAsInteger().intValue() || pageSize <= 0) {
780                         pageSize = fessConfig.getPagingSearchPageMaxSizeAsInteger();
781                     }
782                 } catch (final NumberFormatException e) {
783                     pageSize = fessConfig.getPagingSearchPageSizeAsInteger();
784                 }
785             }
786             return pageSize;
787         }
788 
789         @Override
790         public Object getAttribute(final String name) {
791             return request.getAttribute(name);
792         }
793 
794         @Override
795         public Locale getLocale() {
796             return Locale.ROOT;
797         }
798 
799         @Override
800         public SearchRequestType getType() {
801             return SearchRequestType.JSON;
802         }
803 
804         @Override
805         public String getSimilarDocHash() {
806             return request.getParameter("sdh");
807         }
808 
809         @Override
810         public HighlightInfo getHighlightInfo() {
811             return ComponentUtil.getViewHelper().createHighlightInfo();
812         }
813     }
814 
815     @Override
816     protected void writeHeaders(final HttpServletResponse response) {
817         ComponentUtil.getFessConfig().getApiJsonResponseHeaderList().forEach(e -> response.setHeader(e.getFirst(), e.getSecond()));
818     }
819 }