1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.helper;
17
18 import static org.codelibs.core.stream.StreamUtil.split;
19
20 import java.io.BufferedInputStream;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.UnsupportedEncodingException;
25 import java.net.URLDecoder;
26 import java.net.URLEncoder;
27 import java.util.ArrayList;
28 import java.util.Date;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.concurrent.ConcurrentHashMap;
36 import java.util.concurrent.ExecutionException;
37 import java.util.concurrent.TimeUnit;
38 import java.util.function.Consumer;
39 import java.util.function.Function;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42 import java.util.stream.Collectors;
43
44 import org.apache.catalina.connector.ClientAbortException;
45 import org.apache.commons.lang3.StringUtils;
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.CoreLibConstants;
50 import org.codelibs.core.io.CloseableUtil;
51 import org.codelibs.core.lang.StringUtil;
52 import org.codelibs.core.misc.DynamicProperties;
53 import org.codelibs.core.stream.StreamUtil;
54 import org.codelibs.fess.Constants;
55 import org.codelibs.fess.app.web.base.SearchForm;
56 import org.codelibs.fess.app.web.base.login.FessLoginAssist;
57 import org.codelibs.fess.crawler.builder.RequestDataBuilder;
58 import org.codelibs.fess.crawler.client.CrawlerClient;
59 import org.codelibs.fess.crawler.client.CrawlerClientFactory;
60 import org.codelibs.fess.crawler.entity.ResponseData;
61 import org.codelibs.fess.crawler.util.CharUtil;
62 import org.codelibs.fess.entity.FacetQueryView;
63 import org.codelibs.fess.entity.HighlightInfo;
64 import org.codelibs.fess.entity.SearchRenderData;
65 import org.codelibs.fess.exception.FessSystemException;
66 import org.codelibs.fess.helper.UserAgentHelper.UserAgentType;
67 import org.codelibs.fess.mylasta.action.FessUserBean;
68 import org.codelibs.fess.mylasta.direction.FessConfig;
69 import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig;
70 import org.codelibs.fess.util.ComponentUtil;
71 import org.codelibs.fess.util.DocumentUtil;
72 import org.codelibs.fess.util.FacetResponse;
73 import org.codelibs.fess.util.ResourceUtil;
74 import org.dbflute.optional.OptionalThing;
75 import org.lastaflute.taglib.function.LaFunctions;
76 import org.lastaflute.web.response.ActionResponse;
77 import org.lastaflute.web.response.StreamResponse;
78 import org.lastaflute.web.ruts.process.ActionRuntime;
79 import org.lastaflute.web.util.LaRequestUtil;
80 import org.lastaflute.web.util.LaServletContextUtil;
81 import org.opensearch.core.common.text.Text;
82 import org.opensearch.search.fetch.subphase.highlight.HighlightField;
83
84 import com.github.jknack.handlebars.Context;
85 import com.github.jknack.handlebars.Handlebars;
86 import com.github.jknack.handlebars.Template;
87 import com.github.jknack.handlebars.io.FileTemplateLoader;
88 import com.google.common.cache.Cache;
89 import com.google.common.cache.CacheBuilder;
90 import com.ibm.icu.text.SimpleDateFormat;
91
92 import jakarta.annotation.PostConstruct;
93 import jakarta.servlet.ServletContext;
94 import jakarta.servlet.SessionTrackingMode;
95 import jakarta.servlet.http.HttpServletRequest;
96 import jakarta.servlet.http.HttpSession;
97
98
99
100
101
102
103
104 public class ViewHelper {
105
106
107
108
109 public ViewHelper() {
110
111 }
112
113 private static final Logger logger = LogManager.getLogger(ViewHelper.class);
114
115
116 protected static final String SCREEN_WIDTH = "screen_width";
117
118
119 protected static final int TABLET_WIDTH = 768;
120
121
122 protected static final String CONTENT_DISPOSITION = "Content-Disposition";
123
124
125 protected static final String HL_CACHE = "hl_cache";
126
127
128 protected static final String QUERIES = "queries";
129
130
131 protected static final String CACHE_MSG = "cache_msg";
132
133
134 protected static final Pattern LOCAL_PATH_PATTERN = Pattern.compile("^file:/+[a-zA-Z]:");
135
136
137 protected static final Pattern SHARED_FOLDER_PATTERN = Pattern.compile("^file:/+[^/]\\.");
138
139
140 protected static final String ELLIPSIS = "...";
141
142
143 protected boolean encodeUrlLink = false;
144
145
146 protected String urlLinkEncoding = Constants.UTF_8;
147
148
149 protected String[] highlightedFields;
150
151
152 protected String originalHighlightTagPre = "<em>";
153
154
155 protected String originalHighlightTagPost = "</em>";
156
157
158 protected String highlightTagPre;
159
160
161 protected String highlightTagPost;
162
163
164 protected boolean useSession = true;
165
166
167 protected final Map<String, String> pageCacheMap = new ConcurrentHashMap<>();
168
169
170 protected final Map<String, String> initFacetParamMap = new HashMap<>();
171
172
173 protected final Map<String, String> initGeoParamMap = new HashMap<>();
174
175
176 protected final List<FacetQueryView> facetQueryViewList = new ArrayList<>();
177
178
179 protected String cacheTemplateName = "cache";
180
181
182 protected String escapedHighlightPre = null;
183
184
185 protected String escapedHighlightPost = null;
186
187
188 protected Set<Integer> highlightTerminalCharSet = new HashSet<>();
189
190
191 protected ActionHook actionHook = new ActionHook();
192
193
194 protected final Set<String> inlineMimeTypeSet = new HashSet<>();
195
196
197 protected Cache<String, FacetResponse> facetCache;
198
199
200 protected long facetCacheDuration = 60 * 10L;
201
202
203 protected int textFragmentPrefixLength;
204
205
206 protected int textFragmentSuffixLength;
207
208
209 protected int textFragmentSize;
210
211
212
213
214
215 @PostConstruct
216 public void init() {
217 if (logger.isDebugEnabled()) {
218 logger.debug("Initializing {}", this.getClass().getSimpleName());
219 }
220 final FessConfig fessConfig = ComponentUtil.getFessConfig();
221 escapedHighlightPre = LaFunctions.h(originalHighlightTagPre);
222 escapedHighlightPost = LaFunctions.h(originalHighlightTagPost);
223 highlightTagPre = fessConfig.getQueryHighlightTagPre();
224 highlightTagPost = fessConfig.getQueryHighlightTagPost();
225 highlightedFields = fessConfig.getQueryHighlightContentDescriptionFieldsAsArray();
226 for (final int v : fessConfig.getQueryHighlightTerminalCharsAsArray()) {
227 highlightTerminalCharSet.add(v);
228 }
229 try {
230 final ServletContext servletContext = ComponentUtil.getComponent(ServletContext.class);
231 servletContext.setSessionTrackingModes(
232 fessConfig.getSessionTrackingModesAsSet().stream().map(SessionTrackingMode::valueOf).collect(Collectors.toSet()));
233 } catch (final Throwable t) {
234 logger.warn("Failed to set SessionTrackingMode.", t);
235 }
236
237 split(fessConfig.getQueryFacetQueries(), "\n").of(stream -> stream.map(String::trim).filter(StringUtil::isNotEmpty).forEach(s -> {
238 final String[] values = StringUtils.split(s, ":", 2);
239 if (values.length != 2) {
240 return;
241 }
242 final FacetQueryView facetQueryView = new FacetQueryView();
243 facetQueryView.setTitle(values[0]);
244 split(values[1], "\t").of(subStream -> subStream.map(String::trim).filter(StringUtil::isNotEmpty).forEach(v -> {
245 final String[] facet = StringUtils.split(v, "=", 2);
246 if (facet.length == 2) {
247 facetQueryView.addQuery(facet[0], facet[1]);
248 }
249 }));
250 facetQueryView.init();
251 facetQueryViewList.add(facetQueryView);
252 if (logger.isDebugEnabled()) {
253 logger.debug("loaded {}", facetQueryView);
254 }
255 }));
256
257 facetCache = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(facetCacheDuration, TimeUnit.SECONDS).build();
258
259 textFragmentPrefixLength = fessConfig.getQueryHighlightTextFragmentPrefixLengthAsInteger();
260 textFragmentSuffixLength = fessConfig.getQueryHighlightTextFragmentSuffixLengthAsInteger();
261 textFragmentSize = fessConfig.getQueryHighlightTextFragmentSizeAsInteger();
262
263 split(fessConfig.getResponseInlineMimetypes(), ",")
264 .of(stream -> stream.map(String::trim).filter(StringUtil::isNotEmpty).forEach(inlineMimeTypeSet::add));
265 }
266
267
268
269
270
271
272
273
274
275 public String getContentTitle(final Map<String, Object> document) {
276 final FessConfig fessConfig = ComponentUtil.getFessConfig();
277 String title = DocumentUtil.getValue(document, fessConfig.getIndexFieldTitle(), String.class);
278 if (StringUtil.isBlank(title)) {
279 title = DocumentUtil.getValue(document, fessConfig.getIndexFieldFilename(), String.class);
280 if (StringUtil.isBlank(title)) {
281 title = DocumentUtil.getValue(document, fessConfig.getIndexFieldUrl(), String.class);
282 }
283 }
284 final int size = fessConfig.getResponseMaxTitleLengthAsInteger();
285 if (size > -1) {
286 title = StringUtils.abbreviate(title, size);
287 }
288 final String value = LaFunctions.h(title);
289 if (!fessConfig.isResponseHighlightContentTitleEnabled()) {
290 return value;
291 }
292 return getQuerySet().map(querySet -> {
293 final String pattern = querySet.stream().map(LaFunctions::h).map(Pattern::quote).collect(Collectors.joining("|"));
294 if (StringUtil.isBlank(pattern)) {
295 return null;
296 }
297 final Matcher matcher = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE).matcher(value);
298 final StringBuffer buf = new StringBuffer(value.length() + 100);
299 while (matcher.find()) {
300 matcher.appendReplacement(buf, Matcher.quoteReplacement(highlightTagPre + matcher.group(0) + highlightTagPost));
301 }
302 matcher.appendTail(buf);
303 return buf.toString();
304 }).orElse(value);
305 }
306
307
308
309
310
311
312 protected OptionalThing<Set<String>> getQuerySet() {
313 return LaRequestUtil.getOptionalRequest()
314 .map(req -> ((Set<String>) req.getAttribute(Constants.HIGHLIGHT_QUERIES)))
315 .filter(s -> s != null);
316 }
317
318
319
320
321
322
323
324
325 public String getContentDescription(final Map<String, Object> document) {
326 for (final String field : highlightedFields) {
327 final String text = DocumentUtil.getValue(document, field, String.class);
328 if (StringUtil.isNotBlank(text)) {
329 return escapeHighlight(text);
330 }
331 }
332
333 return StringUtil.EMPTY;
334 }
335
336
337
338
339
340
341
342
343 protected String escapeHighlight(final String text) {
344 final String escaped = LaFunctions.h(text);
345 final String value;
346 if (ComponentUtil.getFessConfig().isQueryHighlightBoundaryPositionDetect()) {
347 int pos = escaped.indexOf(escapedHighlightPre);
348 while (pos >= 0) {
349 final int c = escaped.codePointAt(pos);
350 if (Character.isISOControl(c) || highlightTerminalCharSet.contains(c)) {
351 break;
352 }
353 pos--;
354 }
355
356 value = escaped.substring(pos + 1);
357 } else {
358 value = escaped;
359 }
360 return value.replaceAll(escapedHighlightPre, highlightTagPre).replaceAll(escapedHighlightPost, highlightTagPost);
361 }
362
363
364
365
366
367
368
369 protected String removeHighlightTag(final String str) {
370 return str.replaceAll(originalHighlightTagPre, StringUtil.EMPTY).replaceAll(originalHighlightTagPost, StringUtil.EMPTY);
371 }
372
373
374
375
376
377
378
379 public HighlightInfo createHighlightInfo() {
380 return LaRequestUtil.getOptionalRequest().map(req -> {
381 final HighlightInfo highlightInfo = new HighlightInfo();
382 final String widthStr = req.getParameter(SCREEN_WIDTH);
383 if (StringUtil.isNotBlank(widthStr)) {
384 final int width = Integer.parseInt(widthStr);
385 updateHighlightInfo(highlightInfo, width);
386 final HttpSession session = req.getSession(false);
387 if (session != null) {
388 session.setAttribute(SCREEN_WIDTH, width);
389 }
390 } else {
391 final HttpSession session = req.getSession(false);
392 if (session != null) {
393 final Integer width = (Integer) session.getAttribute(SCREEN_WIDTH);
394 if (width != null) {
395 updateHighlightInfo(highlightInfo, width);
396 }
397 }
398 }
399 return highlightInfo;
400 }).orElse(new HighlightInfo());
401 }
402
403
404
405
406
407
408
409
410 protected void updateHighlightInfo(final HighlightInfo highlightInfo, final int width) {
411 if (width < TABLET_WIDTH) {
412 float ratio = (float) width / (float) TABLET_WIDTH;
413 if (ratio < 0.5) {
414 ratio = 0.5f;
415 }
416 highlightInfo.fragmentSize((int) (highlightInfo.getFragmentSize() * ratio));
417 }
418 }
419
420
421
422
423
424
425
426
427 public String getUrlLink(final Map<String, Object> document) {
428 final FessConfig fessConfig = ComponentUtil.getFessConfig();
429 String url = DocumentUtil.getValue(document, fessConfig.getIndexFieldUrl(), String.class);
430
431 if (StringUtil.isBlank(url)) {
432 return "#not-found-" + DocumentUtil.getValue(document, fessConfig.getIndexFieldDocId(), String.class);
433 }
434
435 final boolean isSmbUrl = url.startsWith("smb:") || url.startsWith("smb1:");
436 final boolean isFtpUrl = url.startsWith("ftp:");
437 final boolean isSmbOrFtpUrl = isSmbUrl || isFtpUrl;
438
439
440 url = ComponentUtil.getPathMappingHelper().replaceUrl(url);
441
442 final boolean isHttpUrl = url.startsWith("http:") || url.startsWith("https:");
443
444 if (isSmbUrl) {
445 url = url.replace("smb:", "file:");
446 url = url.replace("smb1:", "file:");
447 }
448
449 if (isHttpUrl && isSmbOrFtpUrl) {
450
451
452 final StringBuilder buf = new StringBuilder(url.length() + 100);
453 for (final char c : url.toCharArray()) {
454 if (CharUtil.isUrlChar(c)) {
455 buf.append(c);
456 } else {
457 try {
458 buf.append(URLEncoder.encode(String.valueOf(c), urlLinkEncoding));
459 } catch (final UnsupportedEncodingException e) {
460 buf.append(c);
461 }
462 }
463 }
464 url = buf.toString();
465 } else if (url.startsWith("file:")) {
466
467 url = updateFileProtocol(url);
468
469 if (encodeUrlLink) {
470 return appendQueryParameter(document, url);
471 }
472
473
474 if (!isSmbOrFtpUrl) {
475
476 try {
477 url = URLDecoder.decode(url.replace("+", "%2B"), urlLinkEncoding);
478 } catch (final Exception e) {
479 if (logger.isDebugEnabled()) {
480 logger.warn("Failed to decode url: url={}", url, e);
481 }
482 }
483 }
484 }
485
486
487
488 return appendQueryParameter(document, url);
489 }
490
491
492
493
494
495
496
497
498 protected String updateFileProtocol(String url) {
499 final int pos = url.indexOf(':', 5);
500 final boolean isLocalFile = pos > 0 && pos < 12;
501
502 final UserAgentType ua = ComponentUtil.getUserAgentHelper().getUserAgentType();
503 final DynamicProperties systemProperties = ComponentUtil.getSystemProperties();
504 switch (ua) {
505 case IE:
506 if (isLocalFile) {
507 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.winlocal.ie", "file://"));
508 } else {
509 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.ie", "file://"));
510 }
511 break;
512 case FIREFOX:
513 if (isLocalFile) {
514 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.winlocal.firefox", "file://"));
515 } else {
516 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.firefox", "file://///"));
517 }
518 break;
519 case CHROME:
520 if (isLocalFile) {
521 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.winlocal.chrome", "file://"));
522 } else {
523 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.chrome", "file://"));
524 }
525 break;
526 case SAFARI:
527 if (isLocalFile) {
528 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.winlocal.safari", "file://"));
529 } else {
530 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.safari", "file:////"));
531 }
532 break;
533 case OPERA:
534 if (isLocalFile) {
535 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.winlocal.opera", "file://"));
536 } else {
537 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.opera", "file://"));
538 }
539 break;
540 default:
541 if (isLocalFile) {
542 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.winlocal.other", "file://"));
543 } else {
544 url = url.replaceFirst("file:/+", systemProperties.getProperty("file.protocol.other", "file://"));
545 }
546 break;
547 }
548 return url;
549 }
550
551
552
553
554
555
556
557
558
559 protected String appendQueryParameter(final Map<String, Object> document, final String url) {
560 final FessConfig fessConfig = ComponentUtil.getFessConfig();
561 if (fessConfig.isAppendQueryParameter()) {
562 if (url.indexOf('#') >= 0) {
563 return url;
564 }
565
566 final String mimetype = DocumentUtil.getValue(document, fessConfig.getIndexFieldMimetype(), String.class);
567 if (StringUtil.isNotBlank(mimetype)) {
568 switch (mimetype) {
569 case "text/html":
570 return appendHTMLSearchWord(document, url);
571 case "application/pdf":
572 return appendPDFSearchWord(document, url);
573 default:
574 break;
575 }
576 }
577 }
578 return url;
579 }
580
581
582
583
584
585
586
587
588 protected String appendHTMLSearchWord(final Map<String, Object> document, final String url) {
589 final TextFragment[] textFragments = (TextFragment[]) document.get(Constants.TEXT_FRAGMENTS);
590 if (textFragments != null) {
591 final StringBuilder buf = new StringBuilder(1000);
592 buf.append(url).append("#:~:");
593 for (int i = 0; i < textFragmentSize && i < textFragments.length; i++) {
594 buf.append(textFragments[i].toURLString()).append('&');
595 }
596 return buf.toString();
597 }
598 return url;
599 }
600
601
602
603
604
605
606
607
608 protected String appendPDFSearchWord(final Map<String, Object> document, final String url) {
609 return LaRequestUtil.getOptionalRequest().map(req -> (String) req.getAttribute(Constants.REQUEST_QUERIES)).map(queries -> {
610 try {
611 final StringBuilder buf = new StringBuilder(url.length() + 100);
612 buf.append(url).append("#search=%22");
613 buf.append(URLEncoder.encode(queries.trim(), Constants.UTF_8));
614 buf.append("%22");
615 return buf.toString();
616 } catch (final UnsupportedEncodingException e) {
617 logger.warn("Unsupported encoding.", e);
618 }
619 return null;
620 }).filter(StringUtil::isNotBlank).orElse(url);
621 }
622
623
624
625
626
627
628
629
630 public String getPagePath(final String page) {
631 final Locale locale = ComponentUtil.getRequestManager().getUserLocale();
632 final String lang = locale.getLanguage();
633 final String country = locale.getCountry();
634
635 final String pathLC = getLocalizedPagePath(page, lang, country);
636 final String pLC = pageCacheMap.get(pathLC);
637 if (pLC != null) {
638 return pLC;
639 }
640 if (existsPage(pathLC)) {
641 pageCacheMap.put(pathLC, pathLC);
642 return pathLC;
643 }
644
645 final String pathL = getLocalizedPagePath(page, lang, null);
646 final String pL = pageCacheMap.get(pathL);
647 if (pL != null) {
648 return pL;
649 }
650 if (existsPage(pathL)) {
651 pageCacheMap.put(pathLC, pathL);
652 return pathL;
653 }
654
655 final String path = getLocalizedPagePath(page, null, null);
656 final String p = pageCacheMap.get(path);
657 if (p != null) {
658 return p;
659 }
660 if (existsPage(path)) {
661 pageCacheMap.put(pathLC, path);
662 return path;
663 }
664
665 return "index.jsp";
666 }
667
668
669
670
671
672
673
674
675
676 private String getLocalizedPagePath(final String page, final String lang, final String country) {
677 final StringBuilder buf = new StringBuilder(100);
678 buf.append("/WEB-INF/view/").append(page);
679 if (StringUtil.isNotBlank(lang)) {
680 buf.append('_').append(lang);
681 if (StringUtil.isNotBlank(country)) {
682 buf.append('_').append(country);
683 }
684 }
685 buf.append(".jsp");
686 return buf.toString();
687 }
688
689
690
691
692
693
694
695 private boolean existsPage(final String path) {
696 final String realPath = LaServletContextUtil.getServletContext().getRealPath(path);
697 final File file = new File(realPath);
698 return file.isFile();
699 }
700
701
702
703
704
705
706
707
708
709 public String createCacheContent(final Map<String, Object> doc, final String[] queries) {
710 final FessConfig fessConfig = ComponentUtil.getFessConfig();
711 final FileTemplateLoader loader = new FileTemplateLoader(ResourceUtil.getViewTemplatePath().toFile());
712 final Handlebars handlebars = new Handlebars(loader);
713
714 Locale locale = ComponentUtil.getRequestManager().getUserLocale();
715 if (locale == null) {
716 locale = Locale.ENGLISH;
717 }
718 String url = DocumentUtil.getValue(doc, fessConfig.getIndexFieldUrl(), String.class);
719 if (url == null) {
720 url = ComponentUtil.getMessageManager().getMessage(locale, "labels.search_unknown");
721 }
722 doc.put(fessConfig.getResponseFieldUrlLink(), getUrlLink(doc));
723 String createdStr;
724 final Date created = DocumentUtil.getValue(doc, fessConfig.getIndexFieldCreated(), Date.class);
725 if (created != null) {
726 final SimpleDateFormat sdf = new SimpleDateFormat(CoreLibConstants.DATE_FORMAT_ISO_8601_EXTEND);
727 createdStr = sdf.format(created);
728 } else {
729 createdStr = ComponentUtil.getMessageManager().getMessage(locale, "labels.search_unknown");
730 }
731 doc.put(CACHE_MSG, ComponentUtil.getMessageManager().getMessage(locale, "labels.search_cache_msg", url, createdStr));
732
733 doc.put(QUERIES, queries);
734
735 String cache = DocumentUtil.getValue(doc, fessConfig.getIndexFieldCache(), String.class);
736 if (cache != null) {
737 final String mimetype = DocumentUtil.getValue(doc, fessConfig.getIndexFieldMimetype(), String.class);
738 if (!ComponentUtil.getFessConfig().isHtmlMimetypeForCache(mimetype)) {
739 cache = StringEscapeUtils.escapeHtml4(cache);
740 }
741 cache = ComponentUtil.getPathMappingHelper().replaceUrls(cache);
742 if (queries != null && queries.length > 0) {
743 doc.put(HL_CACHE, replaceHighlightQueries(cache, queries));
744 } else {
745 doc.put(HL_CACHE, cache);
746 }
747 } else {
748 doc.put(fessConfig.getIndexFieldCache(), StringUtil.EMPTY);
749 doc.put(HL_CACHE, StringUtil.EMPTY);
750 }
751
752 try {
753 final Template template = handlebars.compile(cacheTemplateName);
754 final Context hbsContext = Context.newContext(doc);
755 return template.apply(hbsContext);
756 } catch (final Exception e) {
757 logger.warn("Failed to create a cache response.", e);
758 }
759
760 return null;
761 }
762
763
764
765
766
767
768
769
770
771 protected String replaceHighlightQueries(final String cache, final String[] queries) {
772 final StringBuffer buf = new StringBuffer(cache.length() + 100);
773 final StringBuffer segBuf = new StringBuffer(1000);
774 final Pattern p = Pattern.compile("<[^>]+>");
775 final Matcher m = p.matcher(cache);
776 final String[] regexQueries = new String[queries.length];
777 final String[] hlQueries = new String[queries.length];
778 for (int i = 0; i < queries.length; i++) {
779 regexQueries[i] = Pattern.quote(queries[i]);
780 hlQueries[i] = highlightTagPre + queries[i] + highlightTagPost;
781 }
782 while (m.find()) {
783 segBuf.setLength(0);
784 m.appendReplacement(segBuf, StringUtil.EMPTY);
785 String segment = segBuf.toString();
786 for (int i = 0; i < queries.length; i++) {
787 segment = Pattern.compile(regexQueries[i], Pattern.CASE_INSENSITIVE).matcher(segment).replaceAll(hlQueries[i]);
788 }
789 buf.append(segment);
790 buf.append(m.group(0));
791 }
792 segBuf.setLength(0);
793 m.appendTail(segBuf);
794 String segment = segBuf.toString();
795 for (int i = 0; i < queries.length; i++) {
796 segment = Pattern.compile(regexQueries[i], Pattern.CASE_INSENSITIVE).matcher(segment).replaceAll(hlQueries[i]);
797 }
798 buf.append(segment);
799 return buf.toString();
800 }
801
802
803
804
805
806
807
808
809 public Object getSitePath(final Map<String, Object> docMap) {
810 final FessConfig fessConfig = ComponentUtil.getFessConfig();
811 final Object siteValue = docMap.get(fessConfig.getIndexFieldSite());
812 if (siteValue != null) {
813 final String site = siteValue.toString();
814 final int size = fessConfig.getResponseMaxSitePathLengthAsInteger();
815 if (size > 3) {
816 return StringUtils.abbreviate(site, size);
817 }
818 if (size >= 0) {
819 return site;
820 }
821 }
822 final Object urlLink = docMap.get(fessConfig.getResponseFieldUrlLink());
823 if (urlLink != null) {
824 final String returnUrl;
825 final String url = urlLink.toString();
826 if (LOCAL_PATH_PATTERN.matcher(url).find() || SHARED_FOLDER_PATTERN.matcher(url).find()) {
827 returnUrl = url.replaceFirst("^file:/+", "");
828 } else if (url.startsWith("file:")) {
829 returnUrl = url.replaceFirst("^file:/+", "/");
830 } else {
831 returnUrl = url.replaceFirst("^[a-zA-Z0-9]*:/+", "");
832 }
833 final int size = fessConfig.getResponseMaxSitePathLengthAsInteger();
834 if (size > 3) {
835 return StringUtils.abbreviate(returnUrl, size);
836 }
837 return returnUrl;
838 }
839 return null;
840 }
841
842
843
844
845
846
847
848
849
850 public StreamResponse asContentResponse(final Map<String, Object> doc) {
851 if (logger.isDebugEnabled()) {
852 logger.debug("writing the content of: {}", doc);
853 }
854 final FessConfig fessConfig = ComponentUtil.getFessConfig();
855 final CrawlingConfigHelper crawlingConfigHelper = ComponentUtil.getCrawlingConfigHelper();
856 final String configId = DocumentUtil.getValue(doc, fessConfig.getIndexFieldConfigId(), String.class);
857 if (configId == null) {
858 final String docId = DocumentUtil.getValue(doc, fessConfig.getIndexFieldId(), String.class);
859 throw new FessSystemException("configId is null in document. docId: " + docId);
860 }
861 if (configId.length() < 2) {
862 throw new FessSystemException("Invalid configId length: " + configId + ". ConfigId must be at least 2 characters long.");
863 }
864 final CrawlingConfig config = crawlingConfigHelper.getCrawlingConfig(configId);
865 if (config == null) {
866 throw new FessSystemException("No crawlingConfig: " + configId);
867 }
868 final String url = DocumentUtil.getValue(doc, fessConfig.getIndexFieldUrl(), String.class);
869 final CrawlerClientFactory crawlerClientFactory =
870 config.initializeClientFactory(() -> ComponentUtil.getComponent(CrawlerClientFactory.class));
871 final CrawlerClient client = crawlerClientFactory.getClient(url);
872 if (client == null) {
873 throw new FessSystemException("No CrawlerClient: " + configId + ", url: " + url);
874 }
875 return writeContent(configId, url, client);
876 }
877
878
879
880
881
882
883
884
885
886 protected StreamResponse writeContent(final String configId, final String url, final CrawlerClient client) {
887 final StreamResponse response = new StreamResponse(StringUtil.EMPTY);
888 final ResponseData responseData = client.execute(RequestDataBuilder.newRequestData().get().url(url).build());
889 if (responseData.getHttpStatusCode() == 404) {
890 response.httpStatus(responseData.getHttpStatusCode());
891 CloseableUtil.closeQuietly(responseData);
892 return response;
893 }
894 writeFileName(response, responseData);
895 writeContentType(response, responseData);
896 writeNoCache(response, responseData);
897 response.stream(out -> {
898 try (final InputStream is = new BufferedInputStream(responseData.getResponseBody())) {
899 out.write(is);
900 } catch (final IOException e) {
901 if (!(e.getCause() instanceof ClientAbortException)) {
902 throw new FessSystemException("Failed to write a content. configId: " + configId + ", url: " + url, e);
903 }
904 } finally {
905 CloseableUtil.closeQuietly(responseData);
906 }
907 if (logger.isDebugEnabled()) {
908 logger.debug("Finished to write {}", url);
909 }
910 });
911 return response;
912 }
913
914
915
916
917
918
919
920 protected void writeNoCache(final StreamResponse response, final ResponseData responseData) {
921 response.header("Pragma", "no-cache");
922 response.header("Cache-Control", "no-cache");
923 response.header("Expires", "Thu, 01 Dec 1994 16:00:00 GMT");
924 }
925
926
927
928
929
930
931
932 protected void writeFileName(final StreamResponse response, final ResponseData responseData) {
933 String charset = responseData.getCharSet();
934 if (charset == null) {
935 charset = Constants.UTF_8;
936 }
937 final String name;
938 final String url = responseData.getUrl();
939 final int pos = url.lastIndexOf('/');
940 try {
941 if (pos >= 0 && pos + 1 < url.length()) {
942 name = URLDecoder.decode(url.substring(pos + 1), charset);
943 } else {
944 name = URLDecoder.decode(url, charset);
945 }
946
947 final String contentDispositionType;
948 if (inlineMimeTypeSet.contains(responseData.getMimeType())) {
949 contentDispositionType = "inline";
950 } else {
951 contentDispositionType = "attachment";
952 }
953
954 final String encodedName = URLEncoder.encode(name, Constants.UTF_8).replace("+", "%20");
955 final String contentDispositionValue;
956 if (name.equals(encodedName)) {
957 contentDispositionValue = contentDispositionType + "; filename=\"" + name + "\"";
958 } else {
959 contentDispositionValue = contentDispositionType + "; filename*=utf-8''" + encodedName;
960 }
961 if (logger.isDebugEnabled()) {
962 logger.debug("ResponseHeader: {}: {}", CONTENT_DISPOSITION, contentDispositionValue);
963 }
964 response.header(CONTENT_DISPOSITION, contentDispositionValue);
965 } catch (final Exception e) {
966 logger.warn("Failed to write a filename: {}", responseData, e);
967 }
968 }
969
970
971
972
973
974
975
976 protected void writeContentType(final StreamResponse response, final ResponseData responseData) {
977 final String mimeType = responseData.getMimeType();
978 if (logger.isDebugEnabled()) {
979 logger.debug("mimeType: {}", mimeType);
980 }
981 if (mimeType == null) {
982 response.contentTypeOctetStream();
983 return;
984 }
985 if (mimeType.startsWith("text/")) {
986 final String charset = responseData.getCharSet();
987 if (charset != null) {
988 response.contentType(mimeType + "; charset=" + charset);
989 return;
990 }
991 }
992 response.contentType(mimeType);
993 }
994
995
996
997
998
999
1000
1001
1002 public String getClientIp(final HttpServletRequest request) {
1003 final String value = request.getHeader("x-forwarded-for");
1004 if (StringUtil.isNotBlank(value)) {
1005 return value;
1006 }
1007 return request.getRemoteAddr();
1008 }
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018 public FacetResponse getCachedFacetResponse(final String query) {
1019 final OptionalThing<FessUserBean> userBean = ComponentUtil.getComponent(FessLoginAssist.class).getSavedUserBean();
1020 final String permissionKey = userBean.map(user -> StreamUtil.stream(user.getPermissions())
1021 .get(stream -> stream.sorted().distinct().collect(Collectors.joining("\n")))).orElse(StringUtil.EMPTY);
1022
1023 try {
1024 return facetCache.get(query + "\n" + permissionKey, () -> {
1025 final SearchHelper searchHelper = ComponentUtil.getSearchHelper();
1026 final SearchForm params = new SearchForm() {
1027 @Override
1028 public int getPageSize() {
1029 return 0;
1030 }
1031
1032 @Override
1033 public int getStartPosition() {
1034 return 0;
1035 }
1036 };
1037 params.q = query;
1038 final SearchRenderData data = new SearchRenderData();
1039 searchHelper.search(params, data, userBean);
1040 if (logger.isDebugEnabled()) {
1041 logger.debug("loaded facet data: {}", data);
1042 }
1043 return data.getFacetResponse();
1044 });
1045 } catch (final ExecutionException e) {
1046 throw new FessSystemException("Cannot load facet from cache.", e);
1047 }
1048 }
1049
1050
1051
1052
1053
1054
1055
1056 public String createHighlightText(final HighlightField highlightField) {
1057 final Text[] fragments = highlightField.fragments();
1058 if (fragments != null && fragments.length != 0) {
1059 final String[] texts = new String[fragments.length];
1060 for (int i = 0; i < fragments.length; i++) {
1061 texts[i] = fragments[i].string();
1062 }
1063 final String value = StringUtils.join(texts, ELLIPSIS);
1064 if (StringUtil.isNotBlank(value) && !ComponentUtil.getFessConfig().endsWithFullstop(value)) {
1065 return value + ELLIPSIS;
1066 }
1067 return value;
1068 }
1069 return null;
1070 }
1071
1072
1073
1074
1075
1076
1077
1078 public TextFragment[] createTextFragmentsByHighlight(final HighlightField[] fields) {
1079 final List<TextFragment> list = new ArrayList<>();
1080 for (final HighlightField field : fields) {
1081 final Text[] fragments = field.fragments();
1082 if (fragments != null) {
1083 for (final Text fragment : fragments) {
1084 final String text = fragment.string();
1085 if (text.length() > textFragmentPrefixLength + textFragmentSuffixLength) {
1086 final String target =
1087 text.replace(originalHighlightTagPre, StringUtil.EMPTY).replace(originalHighlightTagPost, StringUtil.EMPTY);
1088 if (target.length() > textFragmentPrefixLength + textFragmentSuffixLength) {
1089 list.add(new TextFragment(null, target.substring(0, textFragmentPrefixLength),
1090 target.substring(target.length() - textFragmentSuffixLength), null));
1091 }
1092 }
1093 }
1094 }
1095 }
1096 return list.toArray(n -> new TextFragment[n]);
1097 }
1098
1099
1100
1101
1102
1103
1104 public TextFragment[] createTextFragmentsByQuery() {
1105 return LaRequestUtil.getOptionalRequest().map(req -> {
1106 @SuppressWarnings("unchecked")
1107 final Set<String> querySet = (Set<String>) req.getAttribute(Constants.HIGHLIGHT_QUERIES);
1108 if (querySet != null) {
1109 return querySet.stream().map(s -> new TextFragment(null, s, null, null)).toArray(n -> new TextFragment[n]);
1110 }
1111 return new TextFragment[0];
1112 }).orElse(new TextFragment[0]);
1113 }
1114
1115
1116
1117
1118
1119
1120 public boolean isUseSession() {
1121 return useSession;
1122 }
1123
1124
1125
1126
1127
1128
1129 public void setUseSession(final boolean useSession) {
1130 this.useSession = useSession;
1131 }
1132
1133
1134
1135
1136
1137
1138
1139 public void addInitFacetParam(final String key, final String value) {
1140 initFacetParamMap.put(value, key);
1141 }
1142
1143
1144
1145
1146
1147
1148 public Map<String, String> getInitFacetParamMap() {
1149 return initFacetParamMap;
1150 }
1151
1152
1153
1154
1155
1156
1157
1158 public void addInitGeoParam(final String key, final String value) {
1159 initGeoParamMap.put(value, key);
1160 }
1161
1162
1163
1164
1165
1166
1167 public Map<String, String> getInitGeoParamMap() {
1168 return initGeoParamMap;
1169 }
1170
1171
1172
1173
1174
1175
1176 public void addFacetQueryView(final FacetQueryView facetQueryView) {
1177 facetQueryViewList.add(facetQueryView);
1178 }
1179
1180
1181
1182
1183
1184
1185 public List<FacetQueryView> getFacetQueryViewList() {
1186 return facetQueryViewList;
1187 }
1188
1189
1190
1191
1192
1193
1194 public void addInlineMimeType(final String mimeType) {
1195 inlineMimeTypeSet.add(mimeType);
1196 }
1197
1198
1199
1200
1201
1202
1203 public ActionHook getActionHook() {
1204 return actionHook;
1205 }
1206
1207
1208
1209
1210
1211
1212 public void setActionHook(final ActionHook actionHook) {
1213 this.actionHook = actionHook;
1214 }
1215
1216
1217
1218
1219
1220
1221 public void setEncodeUrlLink(final boolean encodeUrlLink) {
1222 this.encodeUrlLink = encodeUrlLink;
1223 }
1224
1225
1226
1227
1228
1229
1230 public void setUrlLinkEncoding(final String urlLinkEncoding) {
1231 this.urlLinkEncoding = urlLinkEncoding;
1232 }
1233
1234
1235
1236
1237
1238
1239 public void setOriginalHighlightTagPre(final String originalHighlightTagPre) {
1240 this.originalHighlightTagPre = originalHighlightTagPre;
1241 }
1242
1243
1244
1245
1246
1247
1248 public void setOriginalHighlightTagPost(final String originalHighlightTagPost) {
1249 this.originalHighlightTagPost = originalHighlightTagPost;
1250 }
1251
1252
1253
1254
1255
1256
1257 public void setCacheTemplateName(final String cacheTemplateName) {
1258 this.cacheTemplateName = cacheTemplateName;
1259 }
1260
1261
1262
1263
1264
1265
1266 public void setFacetCacheDuration(final long facetCacheDuration) {
1267 this.facetCacheDuration = facetCacheDuration;
1268 }
1269
1270
1271
1272
1273
1274 public static class ActionHook {
1275
1276
1277
1278
1279 public ActionHook() {
1280
1281 }
1282
1283
1284
1285
1286
1287
1288
1289
1290 public ActionResponse godHandPrologue(final ActionRuntime runtime, final Function<ActionRuntime, ActionResponse> func) {
1291 return func.apply(runtime);
1292 }
1293
1294
1295
1296
1297
1298
1299
1300
1301 public ActionResponse godHandMonologue(final ActionRuntime runtime, final Function<ActionRuntime, ActionResponse> func) {
1302 return func.apply(runtime);
1303 }
1304
1305
1306
1307
1308
1309
1310
1311 public void godHandEpilogue(final ActionRuntime runtime, final Consumer<ActionRuntime> consumer) {
1312 consumer.accept(runtime);
1313 }
1314
1315
1316
1317
1318
1319
1320
1321
1322 public ActionResponse hookBefore(final ActionRuntime runtime, final Function<ActionRuntime, ActionResponse> func) {
1323 return func.apply(runtime);
1324 }
1325
1326
1327
1328
1329
1330
1331
1332 public void hookFinally(final ActionRuntime runtime, final Consumer<ActionRuntime> consumer) {
1333 consumer.accept(runtime);
1334 }
1335 }
1336
1337
1338
1339
1340
1341 public static class TextFragment {
1342
1343 private final String prefix;
1344
1345 private final String textStart;
1346
1347 private final String textEnd;
1348
1349 private final String suffix;
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359 TextFragment(final String prefix, final String textStart, final String textEnd, final String suffix) {
1360 this.prefix = prefix;
1361 this.textStart = textStart == null ? StringUtil.EMPTY : textStart;
1362 this.textEnd = textEnd;
1363 this.suffix = suffix;
1364 }
1365
1366
1367
1368
1369
1370
1371 public String toURLString() {
1372 final StringBuilder buf = new StringBuilder();
1373 buf.append("text=");
1374 if (StringUtil.isNotBlank(prefix)) {
1375 buf.append(encodeToString(prefix)).append("-,");
1376 }
1377 buf.append(encodeToString(textStart));
1378 if (StringUtil.isNotBlank(textEnd)) {
1379 buf.append(',').append(encodeToString(textEnd));
1380 }
1381 if (StringUtil.isNotBlank(suffix)) {
1382 buf.append(",-").append(encodeToString(suffix));
1383 }
1384 return buf.toString();
1385 }
1386
1387
1388
1389
1390
1391
1392
1393 private String encodeToString(final String text) {
1394 return URLEncoder.encode(text, Constants.CHARSET_UTF_8);
1395 }
1396 }
1397 }