1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.crawler;
17
18 import static org.codelibs.core.stream.StreamUtil.split;
19 import static org.codelibs.core.stream.StreamUtil.stream;
20
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.Date;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.LinkedHashSet;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.regex.Pattern;
32 import java.util.stream.Collectors;
33
34 import org.apache.logging.log4j.LogManager;
35 import org.apache.logging.log4j.Logger;
36 import org.codelibs.core.io.CloseableUtil;
37 import org.codelibs.core.lang.StringUtil;
38 import org.codelibs.core.misc.Pair;
39 import org.codelibs.fess.app.service.FailureUrlService;
40 import org.codelibs.fess.crawler.builder.RequestDataBuilder;
41 import org.codelibs.fess.crawler.client.CrawlerClient;
42 import org.codelibs.fess.crawler.entity.RequestData;
43 import org.codelibs.fess.crawler.entity.ResponseData;
44 import org.codelibs.fess.crawler.entity.UrlQueue;
45 import org.codelibs.fess.crawler.log.LogType;
46 import org.codelibs.fess.exception.ContainerNotAvailableException;
47 import org.codelibs.fess.exception.ContentNotFoundException;
48 import org.codelibs.fess.helper.CrawlingConfigHelper;
49 import org.codelibs.fess.helper.CrawlingInfoHelper;
50 import org.codelibs.fess.helper.DuplicateHostHelper;
51 import org.codelibs.fess.helper.IndexingHelper;
52 import org.codelibs.fess.helper.PermissionHelper;
53 import org.codelibs.fess.helper.SystemHelper;
54 import org.codelibs.fess.mylasta.direction.FessConfig;
55 import org.codelibs.fess.opensearch.client.SearchEngineClient;
56 import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig;
57 import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig.ConfigName;
58 import org.codelibs.fess.util.ComponentUtil;
59 import org.codelibs.fess.util.DocumentUtil;
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79 public class FessCrawlerThread extends CrawlerThread {
80
81
82
83
84 public FessCrawlerThread() {
85 super();
86 }
87
88 private static final Logger logger = LogManager.getLogger(FessCrawlerThread.class);
89
90
91 protected static final String CRAWLER_CLIENTS = "crawlerClients";
92
93
94 private static final int HTTP_STATUS_NOT_FOUND = 404;
95
96
97 private static final int HTTP_STATUS_OK = 200;
98
99
100
101
102
103
104 protected ConcurrentHashMap<String, Pair<String, Pattern>> clientRuleCache = new ConcurrentHashMap<>();
105
106
107
108
109
110
111
112
113
114
115 @Override
116 protected boolean isContentUpdated(final CrawlerClient client, final UrlQueue<?> urlQueue) {
117 if (ComponentUtil.getFessConfig().isIncrementalCrawling()) {
118
119 final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
120 final long startTime = systemHelper.getCurrentTimeAsLong();
121
122 final FessConfig fessConfig = ComponentUtil.getFessConfig();
123 final CrawlingConfigHelper crawlingConfigHelper = ComponentUtil.getCrawlingConfigHelper();
124 final CrawlingInfoHelper crawlingInfoHelper = ComponentUtil.getCrawlingInfoHelper();
125 final IndexingHelper indexingHelper = ComponentUtil.getIndexingHelper();
126 final SearchEngineClient searchEngineClient = ComponentUtil.getSearchEngineClient();
127
128 final String url = urlQueue.getUrl();
129 ResponseData responseData = null;
130 try {
131 final CrawlingConfig crawlingConfig = crawlingConfigHelper.get(crawlerContext.getSessionId());
132 final Map<String, Object> dataMap = new HashMap<>();
133 dataMap.put(fessConfig.getIndexFieldUrl(), url);
134 final List<String> roleTypeList = new ArrayList<>();
135 stream(crawlingConfig.getPermissions()).of(stream -> stream.forEach(p -> roleTypeList.add(p)));
136 if (ComponentUtil.getProtocolHelper().isFilePathProtocol(url)) {
137 if (url.endsWith("/")) {
138
139 return true;
140 }
141 final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
142 if (fessConfig.isSmbRoleFromFile() || fessConfig.isFileRoleFromFile() || fessConfig.isFtpRoleFromFile()) {
143
144 responseData =
145 client.execute(RequestDataBuilder.newRequestData().head().url(url).weight(urlQueue.getWeight()).build());
146 if (responseData == null) {
147 return true;
148 }
149
150 roleTypeList.addAll(permissionHelper.getSmbRoleTypeList(responseData));
151 roleTypeList.addAll(permissionHelper.getFileRoleTypeList(responseData));
152 roleTypeList.addAll(permissionHelper.getFtpRoleTypeList(responseData));
153 }
154 }
155 dataMap.put(fessConfig.getIndexFieldRole(), roleTypeList);
156 final String id = crawlingInfoHelper.generateId(dataMap);
157
158 if (logger.isDebugEnabled()) {
159 logger.debug("Searching indexed document: {}", id);
160 }
161 final Map<String, Object> document = indexingHelper.getDocument(searchEngineClient, id,
162 new String[] { fessConfig.getIndexFieldId(), fessConfig.getIndexFieldLastModified(),
163 fessConfig.getIndexFieldAnchor(), fessConfig.getIndexFieldSegment(), fessConfig.getIndexFieldExpires(),
164 fessConfig.getIndexFieldClickCount(), fessConfig.getIndexFieldFavoriteCount() });
165 if (document == null) {
166 storeChildUrlsToQueue(urlQueue, getChildUrlSet(searchEngineClient, id));
167 return true;
168 }
169
170 final Date expires = DocumentUtil.getValue(document, fessConfig.getIndexFieldExpires(), Date.class);
171 if (expires != null && expires.getTime() < systemHelper.getCurrentTimeAsLong()) {
172 final Object idValue = document.get(fessConfig.getIndexFieldId());
173 if (idValue != null && !indexingHelper.deleteDocument(searchEngineClient, idValue.toString())) {
174 logger.debug("Failed to delete expired document: {}", url);
175 }
176 return true;
177 }
178
179 final Date lastModified = DocumentUtil.getValue(document, fessConfig.getIndexFieldLastModified(), Date.class);
180 if (lastModified == null) {
181 return true;
182 }
183 urlQueue.setLastModified(lastModified.getTime());
184 log(logHelper, LogType.CHECK_LAST_MODIFIED, crawlerContext, urlQueue);
185
186 if (responseData == null) {
187
188 responseData = client.execute(RequestDataBuilder.newRequestData().head().url(url).build());
189 if (responseData == null) {
190 return true;
191 }
192 }
193
194 final int httpStatusCode = responseData.getHttpStatusCode();
195 if (logger.isDebugEnabled()) {
196 logger.debug("Accessing document: url={}, status={}", url, httpStatusCode);
197 }
198 if (httpStatusCode == HTTP_STATUS_NOT_FOUND) {
199 storeChildUrlsToQueue(urlQueue, getAnchorSet(document.get(fessConfig.getIndexFieldAnchor())));
200 if (!indexingHelper.deleteDocument(searchEngineClient, id)) {
201 logger.debug("Failed to delete document: status={}, url={}", HTTP_STATUS_NOT_FOUND, url);
202 }
203 return false;
204 }
205 if (responseData.getLastModified() == null) {
206 return true;
207 }
208 if (responseData.getLastModified().getTime() <= lastModified.getTime() && httpStatusCode == HTTP_STATUS_OK) {
209
210 log(logHelper, LogType.NOT_MODIFIED, crawlerContext, urlQueue);
211
212 responseData.setExecutionTime(systemHelper.getCurrentTimeAsLong() - startTime);
213 responseData.setParentUrl(urlQueue.getParentUrl());
214 responseData.setSessionId(crawlerContext.getSessionId());
215 responseData.setHttpStatusCode(org.codelibs.fess.crawler.Constants.NOT_MODIFIED_STATUS);
216 processResponse(urlQueue, responseData);
217
218 storeChildUrlsToQueue(urlQueue, getAnchorSet(document.get(fessConfig.getIndexFieldAnchor())));
219
220 final Date documentExpires = crawlingInfoHelper.getDocumentExpires(crawlingConfig);
221 if (documentExpires != null
222 && !indexingHelper.updateDocument(searchEngineClient, id, fessConfig.getIndexFieldExpires(), documentExpires)) {
223 logger.debug("Failed to update field: field={}, url={}", fessConfig.getIndexFieldExpires(), url);
224 }
225
226 return false;
227 }
228 } finally {
229 if (responseData != null) {
230 CloseableUtil.closeQuietly(responseData);
231 }
232 }
233 }
234 return true;
235 }
236
237
238
239
240
241
242
243
244 protected void storeChildUrlsToQueue(final UrlQueue<?> urlQueue, final Set<RequestData> childUrlSet) {
245 if (childUrlSet != null) {
246
247 try {
248 storeChildUrls(childUrlSet.stream().filter(rd -> StringUtil.isNotBlank(rd.getUrl())).collect(Collectors.toSet()),
249 urlQueue.getUrl(), urlQueue.getDepth() != null ? urlQueue.getDepth() + 1 : 1);
250 } catch (final Throwable t) {
251 if (!ComponentUtil.available()) {
252 throw new ContainerNotAvailableException(t);
253 }
254 throw t;
255 }
256 }
257 }
258
259
260
261
262
263
264
265
266 protected Set<RequestData> getAnchorSet(final Object obj) {
267 if (obj == null) {
268 return null;
269 }
270
271 List<String> anchorList;
272 if (obj instanceof final String s) {
273 anchorList = List.of(s);
274 } else if (obj instanceof final List<?> l) {
275 anchorList = l.stream().filter(item -> item != null).map(String::valueOf).toList();
276 } else {
277 return null;
278 }
279
280 if (anchorList.isEmpty()) {
281 return null;
282 }
283
284 final Set<RequestData> childUrlSet = new LinkedHashSet<>();
285 for (final String anchor : anchorList) {
286 if (StringUtil.isNotBlank(anchor)) {
287 childUrlSet.add(RequestDataBuilder.newRequestData().get().url(anchor).build());
288 }
289 }
290 return childUrlSet.isEmpty() ? null : childUrlSet;
291 }
292
293
294
295
296
297
298
299
300
301 protected Set<RequestData> getChildUrlSet(final SearchEngineClient searchEngineClient, final String id) {
302 final FessConfig fessConfig = ComponentUtil.getFessConfig();
303 final IndexingHelper indexingHelper = ComponentUtil.getIndexingHelper();
304 final List<Map<String, Object>> docList =
305 indexingHelper.getChildDocumentList(searchEngineClient, id, new String[] { fessConfig.getIndexFieldUrl() });
306 if (docList.isEmpty()) {
307 return null;
308 }
309 if (logger.isDebugEnabled()) {
310 logger.debug("Found documents: {}", docList);
311 }
312 final Set<RequestData> urlSet = new HashSet<>(docList.size());
313 for (final Map<String, Object> doc : docList) {
314 final String url = DocumentUtil.getValue(doc, fessConfig.getIndexFieldUrl(), String.class);
315 if (StringUtil.isNotBlank(url)) {
316 urlSet.add(RequestDataBuilder.newRequestData().get().url(url).build());
317 }
318 }
319 return urlSet;
320 }
321
322
323
324
325
326
327
328
329
330 @Override
331 protected void processResponse(final UrlQueue<?> urlQueue, final ResponseData responseData) {
332 super.processResponse(urlQueue, responseData);
333
334 final FessConfig fessConfig = ComponentUtil.getFessConfig();
335 if (fessConfig.isCrawlerFailureUrlStatusCodes(responseData.getHttpStatusCode())) {
336 final String sessionId = crawlerContext.getSessionId();
337 final CrawlingConfig crawlingConfig = ComponentUtil.getCrawlingConfigHelper().get(sessionId);
338 final String url = urlQueue.getUrl();
339
340 final FailureUrlService failureUrlService = ComponentUtil.getComponent(FailureUrlService.class);
341 failureUrlService.store(crawlingConfig, ContentNotFoundException.class.getCanonicalName(), url,
342 new ContentNotFoundException(urlQueue.getParentUrl(), url));
343 }
344 }
345
346
347
348
349
350
351
352
353
354
355 @Override
356 protected void storeChildUrl(final String childUrl, final String parentUrl, final float weight, final int depth) {
357 if (StringUtil.isNotBlank(childUrl)) {
358 final DuplicateHostHelper duplicateHostHelper = ComponentUtil.getDuplicateHostHelper();
359 final String url = duplicateHostHelper.convert(childUrl);
360 super.storeChildUrl(url, parentUrl, weight, depth);
361 }
362 }
363
364
365
366
367
368
369
370
371
372
373 @Override
374 protected CrawlerClient getClient(final String url) {
375 final CrawlingConfigHelper crawlingConfigHelper = ComponentUtil.getCrawlingConfigHelper();
376 final CrawlingConfig crawlingConfig = crawlingConfigHelper.get(crawlerContext.getSessionId());
377 final Map<String, String> clientConfigMap = crawlingConfig.getConfigParameterMap(ConfigName.CLIENT);
378 final String value = clientConfigMap.get(CRAWLER_CLIENTS);
379 final CrawlerClient client = getClientRuleList(value).stream().map(e -> {
380 if (e.getSecond().matcher(url).matches()) {
381 return e.getFirst();
382 }
383 return null;
384 })
385 .filter(StringUtil::isNotBlank)
386 .findFirst()
387 .map(s -> clientFactory.getClient(s + ":" + url))
388 .orElseGet(() -> clientFactory.getClient(url));
389 if (logger.isDebugEnabled()) {
390 logger.debug("CrawlerClient: class={}", client.getClass().getCanonicalName());
391 }
392 return client;
393 }
394
395
396
397
398
399
400
401
402
403 protected List<Pair<String, Pattern>> getClientRuleList(final String value) {
404 if (StringUtil.isBlank(value)) {
405 return Collections.emptyList();
406 }
407 return split(value, ",").get(stream -> stream.map(String::trim)
408 .map(s -> clientRuleCache.computeIfAbsent(s, t -> {
409 final String[] values = t.split(":", 2);
410 if (values.length != 2) {
411 return null;
412 }
413 return new Pair<>(values[0], Pattern.compile(values[1]));
414 }))
415 .toList());
416 }
417 }