View Javadoc
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 }