1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.helper;
17
18 import java.io.BufferedReader;
19 import java.io.ByteArrayInputStream;
20 import java.io.ByteArrayOutputStream;
21 import java.io.IOException;
22 import java.io.InputStreamReader;
23 import java.io.Reader;
24 import java.io.StringReader;
25 import java.util.Base64;
26 import java.util.HashSet;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.zip.GZIPInputStream;
30 import java.util.zip.GZIPOutputStream;
31
32 import org.apache.commons.lang3.StringUtils;
33 import org.apache.logging.log4j.LogManager;
34 import org.apache.logging.log4j.Logger;
35 import org.codelibs.core.io.ReaderUtil;
36 import org.codelibs.core.lang.StringUtil;
37 import org.codelibs.fess.Constants;
38 import org.codelibs.fess.crawler.builder.RequestDataBuilder;
39 import org.codelibs.fess.crawler.client.CrawlerClient;
40 import org.codelibs.fess.crawler.client.CrawlerClientFactory;
41 import org.codelibs.fess.crawler.entity.RequestData;
42 import org.codelibs.fess.crawler.entity.ResponseData;
43 import org.codelibs.fess.crawler.entity.ResultData;
44 import org.codelibs.fess.crawler.exception.ChildUrlsException;
45 import org.codelibs.fess.crawler.exception.CrawlerSystemException;
46 import org.codelibs.fess.crawler.exception.CrawlingAccessException;
47 import org.codelibs.fess.crawler.extractor.Extractor;
48 import org.codelibs.fess.crawler.extractor.impl.TikaExtractor;
49 import org.codelibs.fess.crawler.processor.ResponseProcessor;
50 import org.codelibs.fess.crawler.processor.impl.DefaultResponseProcessor;
51 import org.codelibs.fess.crawler.rule.Rule;
52 import org.codelibs.fess.crawler.rule.RuleManager;
53 import org.codelibs.fess.crawler.serializer.DataSerializer;
54 import org.codelibs.fess.crawler.transformer.Transformer;
55 import org.codelibs.fess.crawler.util.TextUtil;
56 import org.codelibs.fess.mylasta.direction.FessConfig;
57 import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig;
58 import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig.ConfigName;
59 import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig.Param;
60 import org.codelibs.fess.util.ComponentUtil;
61 import org.lastaflute.di.core.SingletonLaContainer;
62 import org.lastaflute.di.core.exception.ComponentNotFoundException;
63
64 import jakarta.annotation.PostConstruct;
65
66
67
68
69
70
71
72
73 public class DocumentHelper {
74 private static final Logger logger = LogManager.getLogger(DocumentHelper.class);
75
76
77 protected static final String SIMILAR_DOC_HASH_PREFIX = "$";
78
79
80
81
82
83 public DocumentHelper() {
84
85 }
86
87
88
89
90
91 @PostConstruct
92 public void init() {
93 if (logger.isDebugEnabled()) {
94 logger.debug("Initializing {}", this.getClass().getSimpleName());
95 }
96 try {
97 final TikaExtractor tikaExtractor = ComponentUtil.getComponent("tikaExtractor");
98 if (tikaExtractor != null) {
99 tikaExtractor.setMaxAlphanumTermSize(getMaxAlphanumTermSize());
100 tikaExtractor.setMaxSymbolTermSize(getMaxSymbolTermSize());
101 tikaExtractor.setReplaceDuplication(isDuplicateTermRemoved());
102 tikaExtractor.setSpaceChars(getSpaceChars());
103 }
104 } catch (final ComponentNotFoundException e) {
105 if (logger.isDebugEnabled()) {
106 logger.debug("tikaExtractor is not found: {}", e.getMessage().replace('\n', ' '));
107 }
108 } catch (final Exception e) {
109 logger.warn("Failed to initialize TikaExtractor.", e);
110 }
111 }
112
113
114
115
116
117
118
119
120
121
122
123 public String getTitle(final ResponseData responseData, final String title, final Map<String, Object> dataMap) {
124 if (title == null) {
125 return StringUtil.EMPTY;
126 }
127
128 final int[] spaceChars = getSpaceChars();
129 try (final Reader reader = new StringReader(title)) {
130 return TextUtil.normalizeText(reader).initialCapacity(title.length()).spaceChars(spaceChars).execute();
131 } catch (final IOException e) {
132 return StringUtil.EMPTY;
133 }
134 }
135
136
137
138
139
140
141
142
143
144
145
146
147 public String getContent(final CrawlingConfig crawlingConfig, final ResponseData responseData, final String content,
148 final Map<String, Object> dataMap) {
149 if (content == null) {
150 return StringUtil.EMPTY;
151 }
152
153 if (crawlingConfig != null) {
154 final Map<String, String> configParam = crawlingConfig.getConfigParameterMap(ConfigName.CONFIG);
155 if (configParam != null && Constants.TRUE.equalsIgnoreCase(configParam.get(Param.Config.KEEP_ORIGINAL_BODY))) {
156 return content;
157 }
158 }
159
160 if (responseData.getMetaDataMap().get(Extractor.class.getSimpleName()) instanceof TikaExtractor) {
161 return content;
162 }
163
164 final int maxAlphanumTermSize = getMaxAlphanumTermSize();
165 final int maxSymbolTermSize = getMaxSymbolTermSize();
166 final boolean duplicateTermRemoved = isDuplicateTermRemoved();
167 final int[] spaceChars = getSpaceChars();
168 try (final Reader reader = new StringReader(content)) {
169 return TextUtil.normalizeText(reader)
170 .initialCapacity(content.length())
171 .maxAlphanumTermSize(maxAlphanumTermSize)
172 .maxSymbolTermSize(maxSymbolTermSize)
173 .duplicateTermRemoved(duplicateTermRemoved)
174 .spaceChars(spaceChars)
175 .execute();
176 } catch (final IOException e) {
177 return StringUtil.EMPTY;
178 }
179 }
180
181
182
183
184
185
186 protected int getMaxAlphanumTermSize() {
187 final FessConfig fessConfig = ComponentUtil.getFessConfig();
188 return fessConfig.getCrawlerDocumentMaxAlphanumTermSizeAsInteger();
189 }
190
191
192
193
194
195
196 protected int getMaxSymbolTermSize() {
197 final FessConfig fessConfig = ComponentUtil.getFessConfig();
198 return fessConfig.getCrawlerDocumentMaxSymbolTermSizeAsInteger();
199 }
200
201
202
203
204
205
206 protected boolean isDuplicateTermRemoved() {
207 final FessConfig fessConfig = ComponentUtil.getFessConfig();
208 return fessConfig.isCrawlerDocumentDuplicateTermRemoved();
209 }
210
211
212
213
214
215
216 protected int[] getSpaceChars() {
217 final FessConfig fessConfig = ComponentUtil.getFessConfig();
218 return fessConfig.getCrawlerDocumentSpaceCharsAsArray();
219 }
220
221
222
223
224
225
226
227
228
229
230
231 public String getDigest(final ResponseData responseData, final String content, final Map<String, Object> dataMap, final int maxWidth) {
232 if (content == null) {
233 return StringUtil.EMPTY;
234 }
235
236 String subContent;
237 if (content.length() < maxWidth * 2) {
238 subContent = content;
239 } else {
240 subContent = content.substring(0, maxWidth * 2);
241 }
242
243 final int[] spaceChars = getSpaceChars();
244 try (final Reader reader = new StringReader(subContent)) {
245 final String originalStr = TextUtil.normalizeText(reader).initialCapacity(content.length()).spaceChars(spaceChars).execute();
246 return StringUtils.abbreviate(originalStr, maxWidth);
247 } catch (final IOException e) {
248 return StringUtil.EMPTY;
249 }
250 }
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265 public Map<String, Object> processRequest(final CrawlingConfig crawlingConfig, final String crawlingInfoId, final String url) {
266 if (StringUtil.isBlank(crawlingInfoId)) {
267 throw new CrawlingAccessException("sessionId is null. Cannot access document without a valid session ID.");
268 }
269
270 final CrawlerClientFactory crawlerClientFactory = crawlingConfig.initializeClientFactory(ComponentUtil::getCrawlerClientFactory);
271 final CrawlerClient client = crawlerClientFactory.getClient(url);
272 if (client == null) {
273 throw new CrawlingAccessException(
274 "CrawlerClient is null for URL: " + url + ". Unable to access the document without a crawler client.");
275 }
276
277 final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
278 final long startTime = systemHelper.getCurrentTimeAsLong();
279 try (final ResponseData responseData = client.execute(RequestDataBuilder.newRequestData().get().url(url).build())) {
280 if (responseData.getRedirectLocation() != null) {
281 final Set<RequestData> childUrlList = new HashSet<>();
282 childUrlList.add(RequestDataBuilder.newRequestData().get().url(responseData.getRedirectLocation()).build());
283 throw new ChildUrlsException(childUrlList, this.getClass().getName() + "#RedirectedFrom:" + url);
284 }
285 responseData.setExecutionTime(systemHelper.getCurrentTimeAsLong() - startTime);
286 responseData.setSessionId(crawlingInfoId);
287
288 final RuleManager ruleManager = SingletonLaContainer.getComponent(RuleManager.class);
289 final Rule rule = ruleManager.getRule(responseData);
290 if (rule == null) {
291 throw new CrawlingAccessException("No url rule for " + url);
292 }
293 responseData.setRuleId(rule.getRuleId());
294 final ResponseProcessor responseProcessor = rule.getResponseProcessor();
295 if (!(responseProcessor instanceof DefaultResponseProcessor)) {
296 throw new CrawlingAccessException("The response processor is not DefaultResponseProcessor. responseProcessor: "
297 + responseProcessor + ", url: " + url);
298 }
299 final Transformer transformer = ((DefaultResponseProcessor) responseProcessor).getTransformer();
300 final ResultData resultData = transformer.transform(responseData);
301 final Object rawData = resultData.getRawData();
302 if (rawData != null) {
303 @SuppressWarnings("unchecked")
304 final Map<String, Object> responseDataMap = (Map<String, Object>) rawData;
305 return responseDataMap;
306 } else {
307 final byte[] data = resultData.getData();
308 if (data != null) {
309 try {
310 final DataSerializer dataSerializer = ComponentUtil.getComponent("dataSerializer");
311 @SuppressWarnings("unchecked")
312 final Map<String, Object> responseDataMap = (Map<String, Object>) dataSerializer.fromBinaryToObject(data);
313 return responseDataMap;
314 } catch (final Exception e) {
315 throw new CrawlerSystemException("Could not create an instance from bytes.", e);
316 }
317 }
318 }
319 return null;
320 } catch (final Exception e) {
321 throw new CrawlingAccessException("Failed to parse " + url, e);
322 }
323 }
324
325
326
327
328
329
330
331
332 public String decodeSimilarDocHash(final String hash) {
333 if (hash != null && hash.startsWith(SIMILAR_DOC_HASH_PREFIX) && hash.length() > SIMILAR_DOC_HASH_PREFIX.length()) {
334 try (BufferedReader reader = new BufferedReader(new InputStreamReader(
335 new GZIPInputStream(
336 new ByteArrayInputStream(Base64.getUrlDecoder().decode(hash.substring(SIMILAR_DOC_HASH_PREFIX.length())))),
337 Constants.UTF_8))) {
338 return ReaderUtil.readText(reader);
339 } catch (final Exception e) {
340 if (logger.isDebugEnabled()) {
341 logger.debug("Failed to decode similar document hash: hash={}", hash, e);
342 }
343 }
344 }
345 return hash;
346 }
347
348
349
350
351
352
353
354
355 public String encodeSimilarDocHash(final String hash) {
356 if (hash != null && !hash.startsWith(SIMILAR_DOC_HASH_PREFIX)) {
357 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
358 try (GZIPOutputStream gos = new GZIPOutputStream(baos)) {
359 gos.write(hash.getBytes(Constants.UTF_8));
360 }
361 return SIMILAR_DOC_HASH_PREFIX + Base64.getUrlEncoder().withoutPadding().encodeToString(baos.toByteArray());
362 } catch (final IOException e) {
363 logger.warn("Failed to encode similar document hash: hash={}", hash, e);
364 }
365 }
366 return hash;
367 }
368
369
370
371
372
373
374
375
376
377 public String appendLineNumber(final String prefix, final String content) {
378 if (StringUtil.isBlank(content)) {
379 return StringUtil.EMPTY;
380 }
381 final String[] values = content.split("\n");
382 final StringBuilder buf = new StringBuilder((int) (content.length() * 1.3));
383 buf.append(prefix).append(1).append(':').append(values[0]);
384 for (int i = 1; i < values.length; i++) {
385 buf.append('\n').append(prefix).append(i + 1).append(':').append(values[i]);
386 }
387 return buf.toString();
388 }
389 }