1 /*
2 * Copyright 2012-2025 CodeLibs Project and the Others.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 * either express or implied. See the License for the specific language
14 * governing permissions and limitations under the License.
15 */
16 package org.codelibs.fess.ds;
17
18 import java.io.File;
19 import java.io.InputStream;
20 import java.nio.file.FileSystem;
21 import java.nio.file.FileSystems;
22 import java.nio.file.Files;
23 import java.nio.file.Path;
24 import java.util.HashSet;
25 import java.util.LinkedHashMap;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.stream.Collectors;
31
32 import javax.xml.parsers.DocumentBuilder;
33 import javax.xml.parsers.DocumentBuilderFactory;
34
35 import org.apache.logging.log4j.LogManager;
36 import org.apache.logging.log4j.Logger;
37 import org.codelibs.core.lang.StringUtil;
38 import org.codelibs.fess.Constants;
39 import org.codelibs.fess.helper.PluginHelper;
40 import org.codelibs.fess.util.ComponentUtil;
41 import org.codelibs.fess.util.ResourceUtil;
42 import org.w3c.dom.Document;
43 import org.w3c.dom.NamedNodeMap;
44 import org.w3c.dom.Node;
45 import org.w3c.dom.NodeList;
46
47 /**
48 * Factory class responsible for managing and providing access to data store instances.
49 * This factory maintains a registry of data store implementations and provides methods
50 * to register, retrieve, and discover available data stores.
51 *
52 * <p>Data stores are registered by name and class name, allowing flexible lookup.
53 * The factory also supports dynamic discovery of data store plugins by scanning
54 * JAR files for data store configurations.</p>
55 *
56 * <p>Thread-safe operations are supported for registration and retrieval of data stores.
57 * The factory caches data store names with a time-based refresh mechanism to improve
58 * performance while ensuring up-to-date plugin discovery.</p>
59 */
60 public class DataStoreFactory {
61 /** Logger instance for this factory class. */
62 private static final Logger logger = LogManager.getLogger(DataStoreFactory.class);
63
64 /**
65 * Map containing registered data store instances indexed by their names and class simple names.
66 * All keys are stored in lowercase for case-insensitive lookup.
67 */
68 protected Map<String, DataStore> dataStoreMap = new LinkedHashMap<>();
69
70 /**
71 * Cached array of available data store names discovered from plugin JAR files.
72 * This cache is refreshed periodically based on the lastLoadedTime.
73 */
74 protected volatile String[] dataStoreNames = StringUtil.EMPTY_STRINGS;
75
76 /**
77 * Timestamp of the last time data store names were loaded from plugin files.
78 * Used to implement a time-based cache refresh mechanism.
79 * Volatile to ensure visibility across threads.
80 */
81 protected volatile long lastLoadedTime = 0;
82
83 /**
84 * Creates a new instance of DataStoreFactory.
85 * This constructor initializes the factory for managing data store instances
86 * and provides methods for registration, retrieval, and plugin discovery.
87 */
88 public DataStoreFactory() {
89 // Default constructor with explicit documentation
90 }
91
92 /**
93 * Registers a data store instance with the factory using the specified name.
94 * The data store will be accessible by both the provided name and its class simple name,
95 * both converted to lowercase for case-insensitive lookup.
96 *
97 * @param name the name to register the data store under, must not be null
98 * @param dataStore the data store instance to register, must not be null
99 * @throws IllegalArgumentException if either name or dataStore is null
100 */
101 public void add(final String name, final DataStore dataStore) {
102 if (name == null || dataStore == null) {
103 throw new IllegalArgumentException(
104 "Both name and dataStore parameters are required. name: " + name + ", dataStore: " + dataStore);
105 }
106 if (logger.isDebugEnabled()) {
107 logger.debug("Loaded DataStore: name={}", name);
108 }
109 dataStoreMap.put(name.toLowerCase(Locale.ROOT), dataStore);
110 dataStoreMap.put(dataStore.getClass().getSimpleName().toLowerCase(Locale.ROOT), dataStore);
111 }
112
113 /**
114 * Retrieves a data store instance by name.
115 * The lookup is case-insensitive and will match both registered names
116 * and class simple names.
117 *
118 * @param name the name of the data store to retrieve, may be null
119 * @return the data store instance if found, null if not found or name is null
120 */
121 public DataStore getDataStore(final String name) {
122 if (name == null) {
123 return null;
124 }
125 return dataStoreMap.get(name.toLowerCase(Locale.ROOT));
126 }
127
128 /**
129 * Returns an array of available data store names discovered from plugin JAR files.
130 * This method implements a time-based caching mechanism that refreshes the list
131 * every 60 seconds to balance performance with up-to-date plugin discovery.
132 *
133 * @return array of data store names sorted alphabetically, never null
134 */
135 public synchronized String[] getDataStoreNames() {
136 final long now = ComponentUtil.getSystemHelper().getCurrentTimeAsLong();
137 if (now - lastLoadedTime > 60000L) {
138 final List<String> nameList = loadDataStoreNameList();
139 dataStoreNames = nameList.toArray(n -> new String[nameList.size()]);
140 lastLoadedTime = now;
141 }
142 return dataStoreNames;
143 }
144
145 /**
146 * Loads the list of available data store names by scanning plugin JAR files.
147 * This method searches for 'fess_ds++.xml' configuration files within JAR files
148 * in the data store plugin directory and extracts component class names.
149 *
150 * <p>The method uses secure XML parsing features to prevent XXE attacks and
151 * other XML-based vulnerabilities. Component class names are extracted from
152 * the 'class' attribute of 'component' elements in the XML files.</p>
153 *
154 * @return sorted list of data store class simple names discovered from plugins
155 */
156 protected List<String> loadDataStoreNameList() {
157 final Set<String> nameSet = new HashSet<>();
158 final File[] jarFiles = ResourceUtil.getPluginJarFiles(PluginHelper.ArtifactType.DATA_STORE.getId());
159 if (jarFiles == null) {
160 return nameSet.stream().sorted().collect(Collectors.toList());
161 }
162 for (final File jarFile : jarFiles) {
163 try (FileSystem fs = FileSystems.newFileSystem(jarFile.toPath(), ClassLoader.getSystemClassLoader())) {
164 final Path xmlPath = fs.getPath("fess_ds++.xml");
165 if (!Files.exists(xmlPath)) {
166 if (logger.isDebugEnabled()) {
167 logger.debug("Configuration file (fess_ds++.xml) not found: path={}", jarFile.getAbsolutePath());
168 }
169 continue;
170 }
171 try (InputStream is = Files.newInputStream(xmlPath)) {
172 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
173 factory.setFeature(org.codelibs.fess.crawler.Constants.FEATURE_SECURE_PROCESSING, true);
174 factory.setFeature(org.codelibs.fess.crawler.Constants.FEATURE_EXTERNAL_GENERAL_ENTITIES, false);
175 factory.setFeature(org.codelibs.fess.crawler.Constants.FEATURE_EXTERNAL_PARAMETER_ENTITIES, false);
176 factory.setFeature(Constants.XERCES_FEATURE_PREFIX + Constants.LOAD_EXTERNAL_DTD_FEATURE, false);
177 final DocumentBuilder builder = factory.newDocumentBuilder();
178
179 final Document doc = builder.parse(is);
180 final NodeList nodeList = doc.getElementsByTagName("component");
181 for (int i = 0; i < nodeList.getLength(); i++) {
182 final Node node = nodeList.item(i);
183 final NamedNodeMap attributes = node.getAttributes();
184 if (attributes != null) {
185 final Node classAttr = attributes.getNamedItem("class");
186 if (classAttr != null) {
187 final String value = classAttr.getNodeValue();
188 if (StringUtil.isNotBlank(value)) {
189 final String[] values = value.split("\\.");
190 nameSet.add(values[values.length - 1]);
191 }
192 }
193 }
194 }
195 }
196 } catch (final Exception e) {
197 logger.warn("Failed to load DataStore plugin: path={}", jarFile.getAbsolutePath(), e);
198 }
199 }
200 return nameSet.stream().sorted().collect(Collectors.toList());
201 }
202
203 }