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.helper;
17  
18  import static org.codelibs.core.stream.StreamUtil.split;
19  import static org.codelibs.core.stream.StreamUtil.stream;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.lang.reflect.Field;
24  import java.net.JarURLConnection;
25  import java.net.URISyntaxException;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Enumeration;
29  import java.util.List;
30  import java.util.jar.JarEntry;
31  import java.util.jar.JarFile;
32  
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.Logger;
35  import org.codelibs.core.exception.ClassNotFoundRuntimeException;
36  import org.codelibs.core.exception.NoSuchFieldRuntimeException;
37  import org.codelibs.core.lang.ClassUtil;
38  import org.codelibs.core.lang.StringUtil;
39  import org.codelibs.fess.mylasta.direction.FessConfig;
40  import org.codelibs.fess.util.ComponentUtil;
41  
42  import jakarta.annotation.PostConstruct;
43  
44  /**
45   * Helper class for managing and validating URL protocols in Fess crawling system.
46   * This class handles the initialization and validation of web and file protocols
47   * used by the crawler to determine which URLs can be crawled.
48   */
49  public class ProtocolHelper {
50      private static final Logger logger = LogManager.getLogger(ProtocolHelper.class);
51  
52      /** Array of supported web protocols with colon suffix (e.g., "http:", "https:") */
53      protected String[] webProtocols = StringUtil.EMPTY_STRINGS;
54  
55      /** Array of supported file protocols with colon suffix (e.g., "file:", "ftp:") */
56      protected String[] fileProtocols = StringUtil.EMPTY_STRINGS;
57  
58      /**
59       * Default constructor for ProtocolHelper.
60       * Initializes the helper with empty protocol arrays that will be populated during init().
61       */
62      public ProtocolHelper() {
63          // Default constructor
64      }
65  
66      /**
67       * Initializes the protocol helper by loading configured protocols from FessConfig
68       * and scanning for available protocol handlers in the classpath.
69       * This method is called automatically after bean construction.
70       */
71      @PostConstruct
72      public void init() {
73          final FessConfig fessConfig = ComponentUtil.getFessConfig();
74          webProtocols = split(fessConfig.getCrawlerWebProtocols(), ",")
75                  .get(stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim() + ":").toArray(n -> new String[n]));
76          fileProtocols = split(fessConfig.getCrawlerFileProtocols(), ",")
77                  .get(stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim() + ":").toArray(n -> new String[n]));
78  
79          loadProtocols("org.codelibs.fess.net.protocol");
80  
81          if (logger.isDebugEnabled()) {
82              logger.debug("Web protocols: protocols={}", Arrays.toString(webProtocols));
83              logger.debug("File protocols: protocols={}", Arrays.toString(fileProtocols));
84          }
85      }
86  
87      /**
88       * Loads protocol handlers from the specified base package by scanning for
89       * Handler classes in subpackages and registering them as web or file protocols
90       * based on their PROTOCOL_TYPE field.
91       *
92       * @param basePackage the base package to scan for protocol handlers
93       */
94      protected void loadProtocols(final String basePackage) {
95          final List<String> subPackages = new ArrayList<>();
96          final String path = basePackage.replace('.', '/');
97          final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
98          try {
99              final Enumeration<java.net.URL> resources = classLoader.getResources(path);
100 
101             while (resources.hasMoreElements()) {
102                 final java.net.URL resource = resources.nextElement();
103                 logger.debug("Loading resource: url={}", resource);
104 
105                 if ("file".equals(resource.getProtocol())) {
106                     final File directory;
107                     try {
108                         directory = new File(resource.toURI());
109                     } catch (final URISyntaxException e) {
110                         logger.warn("Invalid URI for resource: url={}", resource, e);
111                         continue;
112                     }
113                     if (directory.exists() && directory.isDirectory()) {
114                         final File[] files = directory.listFiles(File::isDirectory);
115                         if (files != null) {
116                             for (final File file : files) {
117                                 final String name = file.getName();
118                                 subPackages.add(name);
119                                 logger.debug("Found subpackage: name={}, resource={}", name, resource);
120                             }
121                         }
122                     }
123                 } else if ("jar".equals(resource.getProtocol())) {
124                     final JarURLConnection jarURLConnection = (JarURLConnection) resource.openConnection();
125                     try (JarFile jarFile = jarURLConnection.getJarFile()) {
126                         final Enumeration<JarEntry> entries = jarFile.entries();
127                         while (entries.hasMoreElements()) {
128                             final JarEntry entry = entries.nextElement();
129                             final String entryName = entry.getName();
130                             if (entryName.endsWith("/") && entryName.startsWith(path) && entryName.length() > path.length() + 1) {
131                                 final String name = entryName.substring(path.length() + 1, entryName.length() - 1);
132                                 if (name.indexOf('/') == -1) {
133                                     subPackages.add(name);
134                                     logger.debug("Found subpackage: name={}, resource={}", name, resource);
135                                 }
136                             }
137                         }
138                     }
139                 }
140             }
141         } catch (final IOException e) {
142             logger.warn("Cannot load subpackages: basePackage={}", basePackage, e);
143         }
144 
145         subPackages.stream().forEach(protocol -> {
146             try {
147                 final Class<Object> handlerClazz = ClassUtil.forName(basePackage + "." + protocol + ".Handler");
148                 final Field protocolTypeField = ClassUtil.getDeclaredField(handlerClazz, "PROTOCOL_TYPE");
149                 if (protocolTypeField.get(null) instanceof final String protocolType) {
150                     if ("web".equalsIgnoreCase(protocolType)) {
151                         addWebProtocol(protocol);
152                     } else if ("file".equalsIgnoreCase(protocolType)) {
153                         addFileProtocol(protocol);
154                     } else {
155                         logger.warn("Unknown protocol: protocol={}", protocol);
156                     }
157                 }
158             } catch (final ClassNotFoundRuntimeException e) {
159                 logger.debug("{}.{}.Handler does not exist.", basePackage, protocol, e);
160             } catch (final NoSuchFieldRuntimeException e) {
161                 logger.debug("{}.{}.Handler does not contain PROTOCOL_TYPE.", basePackage, protocol);
162             } catch (final Exception e) {
163                 logger.warn("Cannot load Handler from {}.{}", basePackage, protocol, e);
164             }
165         });
166     }
167 
168     /**
169      * Returns the array of supported web protocols.
170      *
171      * @return array of web protocol strings with colon suffix (e.g., "http:", "https:")
172      */
173     public String[] getWebProtocols() {
174         return webProtocols;
175     }
176 
177     /**
178      * Returns the array of supported file protocols.
179      *
180      * @return array of file protocol strings with colon suffix (e.g., "file:", "ftp:")
181      */
182     public String[] getFileProtocols() {
183         return fileProtocols;
184     }
185 
186     /**
187      * Checks if the given URL uses a valid web protocol.
188      *
189      * @param url the URL to validate
190      * @return true if the URL starts with a supported web protocol, false otherwise
191      */
192     public boolean isValidWebProtocol(final String url) {
193         return stream(webProtocols).get(stream -> stream.anyMatch(s -> url.startsWith(s)));
194     }
195 
196     /**
197      * Checks if the given URL uses a valid file protocol.
198      *
199      * @param url the URL to validate
200      * @return true if the URL starts with a supported file protocol, false otherwise
201      */
202     public boolean isValidFileProtocol(final String url) {
203         return stream(fileProtocols).get(stream -> stream.anyMatch(s -> url.startsWith(s)));
204     }
205 
206     /**
207      * Adds a new web protocol to the supported protocols list.
208      * If the protocol already exists, it will not be added again.
209      *
210      * @param protocol the protocol name to add (without colon suffix)
211      */
212     public void addWebProtocol(final String protocol) {
213         final String prefix = protocol + ":";
214         if (stream(webProtocols).get(stream -> stream.anyMatch(s -> s.equals(prefix)))) {
215             logger.debug("Web protocols already contains: protocol={}", protocol);
216             return;
217         }
218         webProtocols = Arrays.copyOf(webProtocols, webProtocols.length + 1);
219         webProtocols[webProtocols.length - 1] = prefix;
220     }
221 
222     /**
223      * Adds a new file protocol to the supported protocols list.
224      * If the protocol already exists, it will not be added again.
225      *
226      * @param protocol the protocol name to add (without colon suffix)
227      */
228     public void addFileProtocol(final String protocol) {
229         final String prefix = protocol + ":";
230         if (stream(fileProtocols).get(stream -> stream.anyMatch(s -> s.equals(prefix)))) {
231             logger.debug("File protocols already contains: protocol={}", protocol);
232             return;
233         }
234         fileProtocols = Arrays.copyOf(fileProtocols, fileProtocols.length + 1);
235         fileProtocols[fileProtocols.length - 1] = prefix;
236     }
237 
238     /**
239      * Checks if the given URL is a file path protocol that requires directory and permission handling.
240      * Used for incremental crawling directory detection and file permission processing.
241      *
242      * @param url the URL to check
243      * @return true if the URL uses a file path protocol (smb, smb1, file, ftp, s3, gcs)
244      */
245     public boolean isFilePathProtocol(final String url) {
246         return url.startsWith("smb:") || url.startsWith("smb1:") || url.startsWith("file:") || url.startsWith("ftp:")
247                 || url.startsWith("s3:") || url.startsWith("gcs:");
248     }
249 
250     /**
251      * Checks if the given URL represents a file system path for content serving.
252      * Used to determine if special handling is needed for file system URLs.
253      *
254      * @param url the URL to check
255      * @return true if the URL is a file system path (file, smb, smb1, ftp, storage, s3, gcs)
256      */
257     public boolean isFileSystemPath(final String url) {
258         return url.startsWith("file:") || url.startsWith("smb:") || url.startsWith("smb1:") || url.startsWith("ftp:")
259                 || url.startsWith("storage:") || url.startsWith("s3:") || url.startsWith("gcs:");
260     }
261 
262     /**
263      * Checks if the given URL should skip URL decoding when extracting file names.
264      * Some protocols (like SMB, FTP, S3, GCS) should preserve the original URL encoding.
265      *
266      * @param url the URL to check
267      * @return true if URL decoding should be skipped for this protocol
268      */
269     public boolean shouldSkipUrlDecode(final String url) {
270         return url.startsWith("smb:") || url.startsWith("smb1:") || url.startsWith("ftp:") || url.startsWith("s3:")
271                 || url.startsWith("gcs:");
272     }
273 
274     /**
275      * Checks if the given path has a known protocol prefix that should not be converted.
276      * Used to determine if path conversion is needed in the wizard.
277      *
278      * @param path the path to check
279      * @return true if the path has a known protocol prefix
280      */
281     public boolean hasKnownProtocol(final String path) {
282         return path.startsWith("http:") || path.startsWith("https:") || path.startsWith("smb:") || path.startsWith("smb1:")
283                 || path.startsWith("ftp:") || path.startsWith("storage:") || path.startsWith("s3:") || path.startsWith("gcs:");
284     }
285 }