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.ByteArrayInputStream;
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.net.Proxy;
26 import java.nio.file.Files;
27 import java.nio.file.Path;
28 import java.nio.file.Paths;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.Comparator;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.concurrent.TimeUnit;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37 import java.util.stream.Collectors;
38
39 import javax.xml.XMLConstants;
40 import javax.xml.parsers.DocumentBuilder;
41 import javax.xml.parsers.DocumentBuilderFactory;
42
43 import org.apache.commons.lang3.StringUtils;
44 import org.apache.logging.log4j.LogManager;
45 import org.apache.logging.log4j.Logger;
46 import org.codelibs.core.io.CopyUtil;
47 import org.codelibs.core.lang.StringUtil;
48 import org.codelibs.curl.Curl;
49 import org.codelibs.curl.CurlRequest;
50 import org.codelibs.curl.CurlResponse;
51 import org.codelibs.fess.crawler.Constants;
52 import org.codelibs.fess.exception.PluginException;
53 import org.codelibs.fess.util.ComponentUtil;
54 import org.codelibs.fess.util.ResourceUtil;
55 import org.lastaflute.di.exception.IORuntimeException;
56 import org.w3c.dom.Document;
57 import org.w3c.dom.Node;
58 import org.w3c.dom.NodeList;
59 import org.xml.sax.SAXException;
60
61 import com.fasterxml.jackson.databind.ObjectMapper;
62 import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
63 import com.google.common.cache.CacheBuilder;
64 import com.google.common.cache.CacheLoader;
65 import com.google.common.cache.LoadingCache;
66
67
68
69
70
71
72 public class PluginHelper {
73
74 private static final Logger logger = LogManager.getLogger(PluginHelper.class);
75
76
77
78
79
80 protected LoadingCache<ArtifactType, Artifact[]> availableArtifacts = CacheBuilder.newBuilder()
81 .maximumSize(10)
82 .expireAfterWrite(5, TimeUnit.MINUTES)
83 .build(new CacheLoader<ArtifactType, Artifact[]>() {
84 @Override
85 public Artifact[] load(final ArtifactType key) {
86 final List<Artifact> list = new ArrayList<>();
87 for (final String url : getRepositories()) {
88 if (url.endsWith(".yaml")) {
89 if (key == ArtifactType.UNKNOWN) {
90 list.addAll(loadArtifactsFromRepository(url));
91 }
92 } else {
93 list.addAll(processRepository(key, url));
94 }
95 }
96 return list.toArray(new Artifact[list.size()]);
97 }
98 });
99
100
101
102
103
104 public PluginHelper() {
105
106 }
107
108
109
110
111
112
113
114
115 public Artifact[] getAvailableArtifacts(final ArtifactType artifactType) {
116 try {
117 return availableArtifacts.get(artifactType);
118 } catch (final Exception e) {
119 throw new PluginException("Failed to access " + artifactType, e);
120 }
121 }
122
123
124
125
126
127
128 protected String[] getRepositories() {
129 return split(ComponentUtil.getFessConfig().getPluginRepositories(), ",")
130 .get(stream -> stream.map(String::trim).toArray(n -> new String[n]));
131 }
132
133
134
135
136
137
138
139
140 protected List<Artifact> loadArtifactsFromRepository(final String url) {
141 final String content = getRepositoryContent(url);
142 final ObjectMapper objectMapper = new YAMLMapper();
143 try {
144 @SuppressWarnings("unchecked")
145 final List<Map<?, ?>> result = objectMapper.readValue(content, List.class);
146 if (result != null) {
147 return result.stream()
148 .map(o -> new Artifact((String) o.get("name"), (String) o.get("version"), (String) o.get("url")))
149 .collect(Collectors.toList());
150 }
151 return Collections.emptyList();
152 } catch (final Exception e) {
153 throw new PluginException("Failed to access " + url, e);
154 }
155 }
156
157
158
159
160
161
162
163
164 protected List<Artifact> processRepository(final ArtifactType artifactType, final String url) {
165 final List<Artifact> list = new ArrayList<>();
166 final String repoContent = getRepositoryContent(url);
167 final Matcher matcher = Pattern.compile("href=\"[^\"]*(" + artifactType.getId() + "[a-zA-Z0-9\\-]+)/?\"").matcher(repoContent);
168 while (matcher.find()) {
169 final String name = matcher.group(1);
170 if (isExcludedName(artifactType, name)) {
171 continue;
172 }
173 final String pluginUrl = url + (url.endsWith("/") ? name + "/" : "/" + name + "/");
174 try {
175 final String pluginMetaContent = getRepositoryContent(pluginUrl + "maven-metadata.xml");
176 try (final InputStream is = new ByteArrayInputStream(pluginMetaContent.getBytes(Constants.UTF_8_CHARSET))) {
177 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
178 factory.setFeature(Constants.FEATURE_SECURE_PROCESSING, true);
179 factory.setFeature(Constants.FEATURE_EXTERNAL_GENERAL_ENTITIES, false);
180 factory.setFeature(Constants.FEATURE_EXTERNAL_PARAMETER_ENTITIES, false);
181 factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, StringUtil.EMPTY);
182 factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, StringUtil.EMPTY);
183 final DocumentBuilder builder = factory.newDocumentBuilder();
184 final Document document = builder.parse(is);
185 final NodeList nodeList = document.getElementsByTagName("version");
186 for (int i = 0; i < nodeList.getLength(); i++) {
187 final String version = nodeList.item(i).getTextContent();
188 if (isTargetPluginVersion(version)) {
189 if (version.endsWith("SNAPSHOT")) {
190 final String snapshotVersion = getSnapshotActualVersion(builder, pluginUrl, version);
191 if (StringUtil.isNotBlank(snapshotVersion)) {
192 final String actualVersion = version.replace("SNAPSHOT", snapshotVersion);
193 list.add(new Artifact(name, actualVersion,
194 pluginUrl + version + "/" + name + "-" + actualVersion + ".jar"));
195 } else if (logger.isDebugEnabled()) {
196 logger.debug("Snapshot name not found: name={}, version={}", name, version);
197 }
198 } else {
199 list.add(new Artifact(name, version, pluginUrl + version + "/" + name + "-" + version + ".jar"));
200 }
201 } else if (logger.isDebugEnabled()) {
202 logger.debug("Artifact ignored: name={}, version={}", name, version);
203 }
204 }
205 }
206 } catch (final Exception e) {
207 logger.warn("Failed to parse maven-metadata.xml: url={}", pluginUrl, e);
208 }
209 }
210 return list;
211 }
212
213
214
215
216
217
218
219
220 protected boolean isExcludedName(final ArtifactType artifactType, final String name) {
221 if (artifactType != ArtifactType.CRAWLER) {
222 return false;
223 }
224
225 if ("fess-crawler".equals(name)
226 || "fess-crawler-db".equals(name)
227 || "fess-crawler-db-h2".equals(name)
228 || "fess-crawler-db-mysql".equals(name)
229 || "fess-crawler-es".equals(name)
230 || "fess-crawler-opensearch".equals(name)
231 || "fess-crawler-lasta".equals(name)
232 || "fess-crawler-parent".equals(name)
233 || "fess-crawler-playwright".equals(name)
234 || "fess-crawler-webdriver".equals(name)) {
235 return true;
236 }
237
238 return false;
239 }
240
241
242
243
244
245
246
247 protected boolean isTargetPluginVersion(final String version) {
248 return ComponentUtil.getFessConfig().isTargetPluginVersion(version);
249 }
250
251
252
253
254
255
256
257
258
259
260
261 protected String getSnapshotActualVersion(final DocumentBuilder builder, final String pluginUrl, final String version)
262 throws SAXException, IOException {
263 String timestamp = null;
264 String buildNumber = null;
265 final String versionMetaContent = getRepositoryContent(pluginUrl + version + "/maven-metadata.xml");
266 try (final InputStream is = new ByteArrayInputStream(versionMetaContent.getBytes(Constants.UTF_8_CHARSET))) {
267 final Document doc = builder.parse(is);
268 final NodeList snapshotNodeList = doc.getElementsByTagName("snapshot");
269 if (snapshotNodeList.getLength() > 0) {
270 final NodeList nodeList = snapshotNodeList.item(0).getChildNodes();
271 for (int i = 0; i < nodeList.getLength(); i++) {
272 final Node node = nodeList.item(i);
273 if ("timestamp".equalsIgnoreCase(node.getNodeName())) {
274 timestamp = node.getTextContent();
275 } else if ("buildNumber".equalsIgnoreCase(node.getNodeName())) {
276 buildNumber = node.getTextContent();
277 }
278 }
279 }
280 }
281 if (StringUtil.isNotBlank(timestamp) && StringUtil.isNotBlank(buildNumber)) {
282 return timestamp + "-" + buildNumber;
283 }
284 return null;
285 }
286
287
288
289
290
291
292
293
294 protected String getRepositoryContent(final String url) {
295 if (logger.isDebugEnabled()) {
296 logger.debug("Loading: url={}", url);
297 }
298 try (final CurlResponse response = createCurlRequest(url).execute()) {
299 return response.getContentAsString();
300 } catch (final IOException e) {
301 throw new IORuntimeException(e);
302 }
303 }
304
305
306
307
308
309
310
311 public Artifact[] getInstalledArtifacts(final ArtifactType artifactType) {
312 if (artifactType == ArtifactType.UNKNOWN) {
313 final File[] jarFiles = ResourceUtil.getPluginJarFiles((d, n) -> {
314 for (final ArtifactType type : ArtifactType.values()) {
315 if (n.startsWith(type.getId())) {
316 return false;
317 }
318 }
319 return n.endsWith(".jar");
320 });
321 final List<Artifact> list = new ArrayList<>(jarFiles.length);
322 for (final File file : jarFiles) {
323 list.add(getArtifactFromFileName(artifactType, file.getName()));
324 }
325 list.sort(Comparator.comparing(Artifact::getName));
326 return list.toArray(new Artifact[list.size()]);
327 }
328
329 final File[] jarFiles = ResourceUtil.getPluginJarFiles(artifactType.getId());
330 final List<Artifact> list = new ArrayList<>(jarFiles.length);
331 for (final File file : jarFiles) {
332 list.add(getArtifactFromFileName(artifactType, file.getName()));
333 }
334 list.sort(Comparator.comparing(Artifact::getName));
335 return list.toArray(new Artifact[list.size()]);
336 }
337
338
339
340
341
342
343
344
345 protected Artifact getArtifactFromFileName(final ArtifactType artifactType, final String filename) {
346 return getArtifactFromFileName(artifactType, filename, null);
347 }
348
349
350
351
352
353
354
355
356
357 public Artifact getArtifactFromFileName(final ArtifactType artifactType, final String filename, final String url) {
358 final String baseName = StringUtils.removeEndIgnoreCase(filename, ".jar");
359 final List<String> nameList = new ArrayList<>();
360 final List<String> versionList = new ArrayList<>();
361 boolean isName = true;
362 for (final String value : baseName.split("-")) {
363 if (isName && value.length() > 0 && value.charAt(0) >= '0' && value.charAt(0) <= '9') {
364 isName = false;
365 }
366 if (isName) {
367 nameList.add(value);
368 } else {
369 versionList.add(value);
370 }
371 }
372 return new Artifact(nameList.stream().collect(Collectors.joining("-")), versionList.stream().collect(Collectors.joining("-")), url);
373 }
374
375
376
377
378
379
380 public void installArtifact(final Artifact artifact) {
381 switch (artifact.getType()) {
382 case THEME:
383 install(artifact);
384 ComponentUtil.getThemeHelper().install(artifact);
385 break;
386 default:
387 install(artifact);
388 break;
389 }
390 }
391
392
393
394
395
396
397
398 protected void install(final Artifact artifact) {
399 final String fileName = artifact.getFileName();
400 final String url = artifact.getUrl();
401 if (StringUtil.isBlank(url)) {
402 throw new PluginException("url is blank: " + artifact.getName());
403 }
404 if (url.startsWith("http:") || url.startsWith("https:")) {
405 try (final CurlResponse response = createCurlRequest(url).execute()) {
406 if (response.getHttpStatusCode() != 200) {
407 throw new PluginException("HTTP Status " + response.getHttpStatusCode() + " : failed to get the artifact from " + url);
408 }
409 try (final InputStream in = response.getContentAsStream()) {
410 CopyUtil.copy(in, ResourceUtil.getPluginPath(fileName).toFile());
411 }
412 } catch (final Exception e) {
413 throw new PluginException("Failed to install the artifact " + artifact.getName(), e);
414 }
415 } else {
416 try (final InputStream in = new FileInputStream(url)) {
417 CopyUtil.copy(in, ResourceUtil.getPluginPath(fileName).toFile());
418 } catch (final Exception e) {
419 throw new PluginException("Failed to install the artifact " + artifact.getName(), e);
420 }
421 }
422 }
423
424
425
426
427
428
429
430 protected CurlRequest createCurlRequest(final String url) {
431 final CurlRequest request = Curl.get(url);
432 final Proxy proxy = ComponentUtil.getFessConfig().getHttpProxy();
433 if (proxy != null && !Proxy.NO_PROXY.equals(proxy)) {
434 request.proxy(proxy);
435 }
436 return request;
437 }
438
439
440
441
442
443
444
445 public void deleteInstalledArtifact(final Artifact artifact) {
446 final String fileName = artifact.getFileName();
447 final Path jarPath = Paths.get(ResourceUtil.getPluginPath().toString(), fileName);
448 if (!Files.exists(jarPath)) {
449 throw new PluginException(fileName + " does not exist.");
450 }
451
452 switch (artifact.getType()) {
453 case THEME:
454 ComponentUtil.getThemeHelper().uninstall(artifact);
455 uninstall(fileName, jarPath);
456 break;
457 default:
458 uninstall(fileName, jarPath);
459 break;
460 }
461
462 }
463
464
465
466
467
468
469
470
471 protected void uninstall(final String fileName, final Path jarPath) {
472 try {
473 Files.delete(jarPath);
474 } catch (final IOException e) {
475 throw new PluginException("Failed to delete the artifact " + fileName, e);
476 }
477 }
478
479
480
481
482
483
484
485
486 public Artifact getArtifact(final String name, final String version) {
487 if (StringUtil.isBlank(name) || StringUtil.isBlank(version)) {
488 return null;
489 }
490 for (final Artifact artifact : getAvailableArtifacts(ArtifactType.getType(name))) {
491 if (name.equals(artifact.getName()) && version.equals(artifact.getVersion())) {
492 return artifact;
493 }
494 }
495 return null;
496 }
497
498
499
500
501 public static class Artifact {
502
503 protected final String name;
504
505 protected final String version;
506
507 protected final String url;
508
509
510
511
512
513
514
515
516 public Artifact(final String name, final String version, final String url) {
517 this.name = name;
518 this.version = version;
519 this.url = url;
520 }
521
522
523
524
525
526
527
528 public Artifact(final String name, final String version) {
529 this(name, version, null);
530 }
531
532
533
534
535
536
537 public String getName() {
538 return name;
539 }
540
541
542
543
544
545
546 public String getVersion() {
547 return version;
548 }
549
550
551
552
553
554
555 public String getFileName() {
556 return name + "-" + version + ".jar";
557 }
558
559
560
561
562
563
564 public String getUrl() {
565 return url;
566 }
567
568
569
570
571
572
573 public ArtifactType getType() {
574 return ArtifactType.getType(name);
575 }
576
577
578
579
580
581
582 @Override
583 public String toString() {
584 return name + ":" + version;
585 }
586 }
587
588
589
590
591
592 public enum ArtifactType {
593
594 DATA_STORE("fess-ds"),
595
596 THEME("fess-theme"),
597
598 INGEST("fess-ingest"),
599
600 SCRIPT("fess-script"),
601
602 WEBAPP("fess-webapp"),
603
604 THUMBNAIL("fess-thumbnail"),
605
606 CRAWLER("fess-crawler"),
607
608 LLM("fess-llm"),
609
610 UNKNOWN("jar");
611
612
613 private final String id;
614
615
616
617
618
619
620 ArtifactType(final String id) {
621 this.id = id;
622 }
623
624
625
626
627
628
629 public String getId() {
630 return id;
631 }
632
633
634
635
636
637
638
639 public static ArtifactType getType(final String name) {
640 if (name.startsWith(DATA_STORE.getId())) {
641 return DATA_STORE;
642 }
643 if (name.startsWith(THEME.getId())) {
644 return THEME;
645 }
646 if (name.startsWith(INGEST.getId())) {
647 return INGEST;
648 }
649 if (name.startsWith(SCRIPT.getId())) {
650 return SCRIPT;
651 }
652 if (name.startsWith(WEBAPP.getId())) {
653 return WEBAPP;
654 }
655 if (name.startsWith(THUMBNAIL.getId())) {
656 return THUMBNAIL;
657 }
658 if (name.startsWith(CRAWLER.getId())) {
659 return CRAWLER;
660 }
661 if (name.startsWith(LLM.getId())) {
662 return LLM;
663 }
664 return UNKNOWN;
665 }
666 }
667
668 }