1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.taglib;
17
18 import static org.codelibs.core.stream.StreamUtil.stream;
19
20 import java.io.File;
21 import java.math.RoundingMode;
22 import java.nio.file.Files;
23 import java.nio.file.Path;
24 import java.nio.file.Paths;
25 import java.text.DecimalFormat;
26 import java.text.NumberFormat;
27 import java.text.SimpleDateFormat;
28 import java.time.LocalDateTime;
29 import java.time.ZonedDateTime;
30 import java.time.format.DateTimeFormatter;
31 import java.util.ArrayList;
32 import java.util.Base64;
33 import java.util.Calendar;
34 import java.util.Date;
35 import java.util.Enumeration;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Map;
39 import java.util.Objects;
40 import java.util.concurrent.ExecutionException;
41 import java.util.concurrent.TimeUnit;
42 import java.util.regex.Matcher;
43 import java.util.regex.Pattern;
44 import java.util.stream.Collectors;
45
46 import org.apache.commons.lang3.time.DurationFormatUtils;
47 import org.apache.commons.text.StringEscapeUtils;
48 import org.apache.logging.log4j.LogManager;
49 import org.apache.logging.log4j.Logger;
50 import org.apache.pdfbox.util.DateConverter;
51 import org.codelibs.core.lang.StringUtil;
52 import org.codelibs.fess.Constants;
53 import org.codelibs.fess.app.web.base.FessAdminAction;
54 import org.codelibs.fess.app.web.base.login.FessLoginAssist;
55 import org.codelibs.fess.entity.FacetQueryView;
56 import org.codelibs.fess.helper.ViewHelper;
57 import org.codelibs.fess.util.ComponentUtil;
58 import org.lastaflute.di.util.LdiURLUtil;
59 import org.lastaflute.web.LastaWebKey;
60 import org.lastaflute.web.util.LaRequestUtil;
61 import org.lastaflute.web.util.LaResponseUtil;
62 import org.lastaflute.web.util.LaServletContextUtil;
63 import org.opensearch.common.joda.Joda;
64
65 import com.google.common.cache.CacheBuilder;
66 import com.google.common.cache.CacheLoader;
67 import com.google.common.cache.LoadingCache;
68
69 import jakarta.servlet.http.HttpServletRequest;
70
71
72
73
74
75
76
77
78
79 public class FessFunctions {
80
81 private static final Logger logger = LogManager.getLogger(FessFunctions.class);
82
83
84 private static final String GEO_PREFIX = "geo.";
85
86
87 private static final String FACET_PREFIX = "facet.";
88
89
90 private static final String PDF_DATE = "pdf_date";
91
92
93 private static final Pattern EMAIL_ADDRESS_PATTERN =
94 Pattern.compile("[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}", Pattern.CASE_INSENSITIVE);
95
96
97
98
99
100 private static LoadingCache<String, Long> resourceHashCache =
101 CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES).build(new CacheLoader<String, Long>() {
102 @Override
103 public Long load(final String key) throws Exception {
104 try {
105 final Path path = Paths.get(LaServletContextUtil.getServletContext().getRealPath(key));
106 if (Files.isRegularFile(path)) {
107 return Files.getLastModifiedTime(path).toMillis();
108 }
109 } catch (final Exception e) {
110 logger.debug("Failed to access resource file: path={}", key, e);
111 }
112 return 0L;
113 }
114 });
115
116
117
118
119
120 protected FessFunctions() {
121
122 }
123
124
125
126
127
128
129
130 public static String html(final boolean isOpen) {
131 if (isOpen) {
132 return "<html lang=\"" + LaRequestUtil.getOptionalRequest().map(req -> {
133 if (req.getAttribute(LastaWebKey.USER_LOCALE_KEY) instanceof Locale locale) {
134 return locale;
135 }
136 return Locale.ENGLISH;
137 }).orElse(Locale.ENGLISH).getLanguage() + "\">";
138 }
139 return "</html>";
140 }
141
142
143
144
145
146
147
148 public static Boolean labelExists(final String value) {
149 return LaRequestUtil.getOptionalRequest().map(req -> {
150 @SuppressWarnings("unchecked")
151 final Map<String, String> labelValueMap = (Map<String, String>) req.getAttribute(Constants.LABEL_VALUE_MAP);
152 if (labelValueMap != null) {
153 return labelValueMap.get(value) != null;
154 }
155 return false;
156 }).orElse(false);
157 }
158
159
160
161
162
163
164
165 public static String label(final String value) {
166 return LaRequestUtil.getOptionalRequest().map(req -> {
167 @SuppressWarnings("unchecked")
168 final Map<String, String> labelValueMap = (Map<String, String>) req.getAttribute(Constants.LABEL_VALUE_MAP);
169 if (labelValueMap != null) {
170 return labelValueMap.get(value);
171 }
172 return null;
173 }).orElse(value);
174 }
175
176
177
178
179
180
181
182 public static Date date(final Long value) {
183 if (value == null) {
184 return null;
185 }
186 return new Date(value);
187 }
188
189
190
191
192
193
194
195 public static Date parseDate(final String value) {
196 return parseDate(value, Constants.DATE_OPTIONAL_TIME);
197 }
198
199
200
201
202
203
204
205
206 public static Date parseDate(final String value, final String format) {
207 if (value == null) {
208 return null;
209 }
210
211 try {
212 if (PDF_DATE.equals(format)) {
213 final Calendar cal = DateConverter.toCalendar(value);
214 return cal != null ? cal.getTime() : null;
215 }
216
217 final long time = Joda.forPattern(format).parseMillis(value);
218 return new Date(time);
219 } catch (final Exception e) {
220 return null;
221 }
222 }
223
224
225
226
227
228
229
230 public static String formatDate(final Date date) {
231 if (date == null) {
232 return StringUtil.EMPTY;
233 }
234 final SimpleDateFormat sdf = new SimpleDateFormat(Constants.ISO_DATETIME_FORMAT);
235 sdf.setTimeZone(Constants.TIMEZONE_UTC);
236 return sdf.format(date);
237 }
238
239
240
241
242
243
244
245 public static String formatDate(final LocalDateTime date) {
246 if (date == null) {
247 return StringUtil.EMPTY;
248 }
249 return date.format(DateTimeFormatter.ofPattern(Constants.ISO_DATETIME_FORMAT, Locale.ROOT));
250 }
251
252
253
254
255
256
257
258
259 public static String formatDate(final ZonedDateTime date, final String format) {
260 if (date == null) {
261 return StringUtil.EMPTY;
262 }
263 return date.format(DateTimeFormatter.ofPattern(format, Locale.ROOT));
264 }
265
266
267
268
269
270
271
272 public static String formatDuration(final long durationMillis) {
273 return DurationFormatUtils.formatDuration(durationMillis, "d 'days' HH:mm:ss.SSS").replace("0 days", StringUtil.EMPTY).trim();
274
275 }
276
277
278
279
280
281
282
283
284 public static String formatNumber(final long value, final String pattern) {
285 final DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(getUserLocale());
286 df.applyPattern(pattern);
287 return df.format(value);
288 }
289
290
291
292
293
294
295 private static Locale getUserLocale() {
296 if (ComponentUtil.hasRequestManager()) {
297 final Locale locale = ComponentUtil.getRequestManager().getUserLocale();
298 if (locale != null) {
299 return locale;
300 }
301 }
302 return Locale.ROOT;
303 }
304
305
306
307
308
309
310
311 public static String formatFileSize(final long value) {
312 double target = value;
313 String unit = "";
314 String format = "0.#";
315 if (value < 1024) {
316 format = "0";
317 } else if (value < 1024L * 1024L) {
318 target /= 1024;
319 unit = "K";
320 } else if (value < 1024L * 1024L * 1024L) {
321 target /= 1024;
322 target /= 1024;
323 unit = "M";
324 } else if (value < 1024L * 1024L * 1024L * 1024L) {
325 target /= 1024;
326 target /= 1024;
327 target /= 1024;
328 unit = "G";
329 } else {
330 target /= 1024;
331 target /= 1024;
332 target /= 1024;
333 target /= 1024;
334 unit = "T";
335 }
336 final DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(getUserLocale());
337 df.applyPattern(format);
338 df.setRoundingMode(RoundingMode.HALF_UP);
339 return df.format(target) + unit;
340 }
341
342
343
344
345
346
347
348 public static String pagingQuery(final String query) {
349 return LaRequestUtil.getOptionalRequest().map(req -> {
350 @SuppressWarnings("unchecked")
351 final List<String> pagingQueryList = (List<String>) req.getAttribute(Constants.PAGING_QUERY_LIST);
352 if (pagingQueryList != null) {
353 final String prefix;
354 if (query != null) {
355 prefix = "ex_q=" + query.split(":")[0] + "%3A";
356 } else {
357 prefix = null;
358 }
359 return pagingQueryList.stream()
360 .filter(s -> prefix == null || !s.startsWith(prefix))
361 .collect(Collectors.joining("&", "&", StringUtil.EMPTY));
362 }
363 return null;
364 }).orElse(StringUtil.EMPTY);
365 }
366
367
368
369
370
371
372 public static String facetQuery() {
373 return createQuery(Constants.FACET_QUERY, FACET_PREFIX);
374 }
375
376
377
378
379
380
381 public static String geoQuery() {
382 return createQuery(Constants.GEO_QUERY, GEO_PREFIX);
383 }
384
385
386
387
388
389
390 public static String facetForm() {
391 return createForm(Constants.FACET_FORM, FACET_PREFIX);
392 }
393
394
395
396
397
398
399 public static String geoForm() {
400 return createForm(Constants.GEO_FORM, GEO_PREFIX);
401 }
402
403
404
405
406
407
408 public static List<FacetQueryView> facetQueryViewList() {
409 final ViewHelper viewHelper = ComponentUtil.getViewHelper();
410 return viewHelper.getFacetQueryViewList();
411 }
412
413
414
415
416
417
418
419
420 private static String createQuery(final String key, final String prefix) {
421 return LaRequestUtil.getOptionalRequest().map(request -> {
422 String query = (String) request.getAttribute(key);
423 if (query == null) {
424 final StringBuilder buf = new StringBuilder(100);
425 final Enumeration<String> names = request.getParameterNames();
426 while (names.hasMoreElements()) {
427 final String name = names.nextElement();
428 if (name.startsWith(prefix)) {
429 final String[] values = request.getParameterValues(name);
430 if (values != null) {
431 for (final String value : values) {
432 buf.append('&');
433 buf.append(LdiURLUtil.encode(name, Constants.UTF_8));
434 buf.append('=');
435 buf.append(LdiURLUtil.encode(value, Constants.UTF_8));
436 }
437 }
438 }
439 }
440 query = buf.toString();
441 request.setAttribute(key, query);
442 }
443 return query;
444 }).orElse(null);
445 }
446
447
448
449
450
451
452
453
454 private static String createForm(final String key, final String prefix) {
455 return LaRequestUtil.getOptionalRequest().map(request -> {
456 String query = (String) request.getAttribute(key);
457 if (query == null) {
458 final StringBuilder buf = new StringBuilder(100);
459 final Enumeration<String> names = request.getParameterNames();
460 while (names.hasMoreElements()) {
461 final String name = names.nextElement();
462 if (name.startsWith(prefix)) {
463 final String[] values = request.getParameterValues(name);
464 if (values != null) {
465 for (final String value : values) {
466 buf.append("<input type=\"hidden\" name=\"");
467 buf.append(StringEscapeUtils.escapeHtml4(name));
468 buf.append("\" value=\"");
469 buf.append(StringEscapeUtils.escapeHtml4(value));
470 buf.append("\"/>");
471 }
472 }
473 }
474 }
475 query = buf.toString();
476 request.setAttribute(key, query);
477 }
478 return query;
479 }).orElse(null);
480 }
481
482
483
484
485
486
487
488 public static String base64(final String value) {
489 if (value == null) {
490 return StringUtil.EMPTY;
491 }
492 return Base64.getUrlEncoder().encodeToString(value.getBytes(Constants.CHARSET_UTF_8));
493 }
494
495
496
497
498
499
500
501 public static boolean fileExists(final String path) {
502 final File file = new File(LaServletContextUtil.getServletContext().getRealPath(path));
503 return file.exists();
504 }
505
506
507
508
509
510
511
512
513 public static String url(final String input) {
514 if (input == null) {
515 final String msg = "The argument 'input' should not be null.";
516 throw new IllegalArgumentException(msg);
517 }
518 if (!input.startsWith("/")) {
519 final String msg = "The argument 'input' should start with slash '/': " + input;
520 throw new IllegalArgumentException(msg);
521 }
522 final StringBuilder sb = new StringBuilder();
523 LaRequestUtil.getOptionalRequest().map(HttpServletRequest::getContextPath).filter(s -> s.length() > 1).ifPresent(s -> sb.append(s));
524 sb.append(input);
525 if (input.indexOf('?') == -1) {
526 try {
527 final Long value = resourceHashCache.get(input);
528 if (value.longValue() > 0) {
529 sb.append("?t=").append(value.toString());
530 }
531 } catch (final ExecutionException e) {
532 logger.debug("Failed to access resource hash cache: path={}", input, e);
533 }
534 }
535 return LaResponseUtil.getResponse().encodeURL(sb.toString());
536 }
537
538
539
540
541
542
543
544 public static String sdh(final String input) {
545 if (StringUtil.isBlank(input)) {
546 return input;
547 }
548 return ComponentUtil.getDocumentHelper().encodeSimilarDocHash(input);
549 }
550
551
552
553
554
555
556
557 public static String join(final Object input) {
558 String[] values = null;
559 if (input instanceof String[]) {
560 values = (String[]) input;
561 } else if (input instanceof List) {
562 values = ((List<?>) input).stream().filter(Objects::nonNull).map(Object::toString).toArray(n -> new String[n]);
563 } else if (input instanceof String) {
564 return input.toString();
565 }
566 if (values != null) {
567 return stream(values).get(stream -> stream.filter(StringUtil::isNotBlank).map(String::trim).collect(Collectors.joining(" ")));
568 }
569 return StringUtil.EMPTY;
570 }
571
572
573
574
575
576
577
578
579 public static String escapeJs(final String input) {
580 if (input == null) {
581 return StringUtil.EMPTY;
582 }
583 return StringEscapeUtils.escapeEcmaScript(input);
584 }
585
586
587
588
589
590
591
592
593
594 public static String replace(final Object input, final String regex, final String replacement) {
595 if (input == null) {
596 return StringUtil.EMPTY;
597 }
598 return input.toString().replaceAll(regex, replacement);
599 }
600
601
602
603
604
605
606
607
608
609
610 public static String formatCode(final String prefix, final String style, final String mimetype, final String input) {
611 if (input == null) {
612 return StringUtil.EMPTY;
613 }
614 final Pattern pattern = Pattern.compile("^" + prefix + "([0-9]+):(.*)$");
615 final String[] values = input.split("\n");
616 final List<String> list = new ArrayList<>(values.length);
617 int lineNum = 0;
618 for (final String line : values) {
619 final Matcher matcher = pattern.matcher(line);
620 if (matcher.matches()) {
621 if (lineNum == 0) {
622 lineNum = Integer.parseInt(matcher.group(1));
623 list.clear();
624 }
625 list.add(matcher.group(2));
626 } else {
627 list.add(line);
628 }
629 }
630 if (lineNum == 0 || list.isEmpty()) {
631 return "<pre class=\"" + style + "\">" + input + "</pre>";
632 }
633 int lastIndex = list.size();
634 if (list.get(list.size() - 1).endsWith("...")) {
635 lastIndex--;
636 }
637 if (lastIndex <= 0) {
638 lastIndex = 1;
639 }
640 final String content = list.subList(0, lastIndex).stream().collect(Collectors.joining("\n"));
641 if (StringUtil.isBlank(content)) {
642 return "<pre class=\"" + style + "\">" + input.replaceAll("L[0-9]+:", StringUtil.EMPTY).trim() + "</pre>";
643 }
644 return "<pre class=\"" + style + " linenums:" + lineNum + "\">" + content + "</pre>";
645 }
646
647
648
649
650
651
652
653
654 public static String getMessage(final String key, final String defaultValue) {
655 return ComponentUtil.getMessageManager().findMessage(getUserLocale(), key).orElse(defaultValue);
656 }
657
658
659
660
661
662
663
664 public static boolean hasActionRole(final String role) {
665 final String[] roles;
666 if (role.endsWith(FessAdminAction.VIEW)) {
667 roles = new String[] { role, role.substring(0, role.length() - FessAdminAction.VIEW.length()) };
668 } else {
669 roles = new String[] { role };
670 }
671 final FessLoginAssist loginAssist = ComponentUtil.getComponent(FessLoginAssist.class);
672 return loginAssist.getSavedUserBean()
673 .map(user -> user.hasRoles(roles) || user.hasRoles(ComponentUtil.getFessConfig().getAuthenticationAdminRolesAsArray()))
674 .orElse(false);
675 }
676
677
678
679
680
681
682
683 public static String maskEmail(final String value) {
684 if (value == null) {
685 return StringUtil.EMPTY;
686 }
687 return EMAIL_ADDRESS_PATTERN.matcher(value).replaceAll("******@****.***");
688 }
689 }