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 java.util.ArrayList;
19  import java.util.HashMap;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.function.BiFunction;
23  import java.util.regex.Matcher;
24  
25  import org.apache.logging.log4j.LogManager;
26  import org.apache.logging.log4j.Logger;
27  import org.codelibs.core.lang.StringUtil;
28  import org.codelibs.fess.Constants;
29  import org.codelibs.fess.opensearch.config.exbhv.PathMappingBhv;
30  import org.codelibs.fess.opensearch.config.exentity.PathMapping;
31  import org.codelibs.fess.util.ComponentUtil;
32  import org.codelibs.fess.util.DocumentUtil;
33  import org.lastaflute.di.core.exception.ComponentNotFoundException;
34  import org.lastaflute.di.core.factory.SingletonLaContainerFactory;
35  import org.lastaflute.web.util.LaRequestUtil;
36  
37  import jakarta.annotation.PostConstruct;
38  
39  /**
40   * Helper class for path mapping configuration.
41   */
42  public class PathMappingHelper extends AbstractConfigHelper {
43  
44      /**
45       * Default constructor.
46       */
47      public PathMappingHelper() {
48          super();
49      }
50  
51      private static final Logger logger = LogManager.getLogger(PathMappingHelper.class);
52  
53      /** Function matcher for encode URL. */
54      protected static final String FUNCTION_ENCODEURL_MATCHER = "function:encodeUrl";
55  
56      /** Groovy matcher prefix. */
57      protected static final String GROOVY_MATCHER = "groovy:";
58  
59      /** Map of path mappings by process type. */
60      protected final Map<String, List<PathMapping>> pathMappingMap = new HashMap<>();
61  
62      /** Cached list of path mappings. */
63      protected volatile List<PathMapping> cachedPathMappingList = null;
64  
65      /**
66       * Initializes the path mapping helper.
67       */
68      @PostConstruct
69      public void init() {
70          if (logger.isDebugEnabled()) {
71              logger.debug("Initializing {}", this.getClass().getSimpleName());
72          }
73          load();
74      }
75  
76      @Override
77      public int load() {
78          final List<String> ptList = getProcessTypeList();
79  
80          try {
81              final PathMappingBhv pathMappingBhv = ComponentUtil.getComponent(PathMappingBhv.class);
82              cachedPathMappingList = pathMappingBhv.selectList(cb -> {
83                  cb.query().addOrderBy_SortOrder_Asc();
84                  cb.query().setProcessType_InScope(ptList);
85                  cb.fetchFirst(ComponentUtil.getFessConfig().getPagePathMappingMaxFetchSizeAsInteger());
86              });
87              if (logger.isDebugEnabled()) {
88                  cachedPathMappingList.forEach(e -> {
89                      logger.debug("path mapping: {}: {} -> {}", e.getId(), e.getRegex(), e.getReplacement());
90                  });
91              }
92              return cachedPathMappingList.size();
93          } catch (final ComponentNotFoundException e) {
94              if (logger.isDebugEnabled()) {
95                  logger.debug("Failed to load path mappings.", e);
96              }
97              cachedPathMappingList = new ArrayList<>();
98          } catch (final Exception e) {
99              logger.warn("Failed to load path mappings.", e);
100         }
101         return 0;
102     }
103 
104     /**
105      * Gets the list of process types.
106      *
107      * @return the list of process types
108      */
109     protected List<String> getProcessTypeList() {
110         final List<String> ptList = new ArrayList<>();
111         final String executeType = System.getProperty("lasta.env");
112         if (Constants.EXECUTE_TYPE_CRAWLER.equalsIgnoreCase(executeType)) {
113             ptList.add(Constants.PROCESS_TYPE_REPLACE);
114         } else {
115             ptList.add(Constants.PROCESS_TYPE_DISPLAYING);
116             ptList.add(Constants.PROCESS_TYPE_BOTH);
117         }
118         return ptList;
119     }
120 
121     /**
122      * Sets the path mapping list for a session.
123      *
124      * @param sessionId the session ID
125      * @param pathMappingList the path mapping list
126      */
127     public void setPathMappingList(final String sessionId, final List<PathMapping> pathMappingList) {
128         if (sessionId != null) {
129             if (pathMappingList != null) {
130                 pathMappingMap.put(sessionId, pathMappingList);
131             } else {
132                 removePathMappingList(sessionId);
133             }
134         }
135     }
136 
137     /**
138      * Removes the path mapping list for a session.
139      *
140      * @param sessionId the session ID
141      */
142     public void removePathMappingList(final String sessionId) {
143         pathMappingMap.remove(sessionId);
144     }
145 
146     /**
147      * Gets the path mapping list for a session.
148      *
149      * @param sessionId the session ID
150      * @return the path mapping list
151      */
152     public List<PathMapping> getPathMappingList(final String sessionId) {
153         if (sessionId == null) {
154             return null;
155         }
156         return pathMappingMap.get(sessionId);
157     }
158 
159     /**
160      * Replaces URL for crawling.
161      *
162      * @param sessionId the session ID
163      * @param url the URL to replace
164      * @return the replaced URL
165      */
166     public String replaceUrl(final String sessionId, final String url) { // for crawling
167         final List<PathMapping> pathMappingList = getPathMappingList(sessionId);
168         if (pathMappingList == null) {
169             return url;
170         }
171         return replaceUrl(pathMappingList, url);
172     }
173 
174     /**
175      * Replaces URLs in text.
176      *
177      * @param text the text containing URLs
178      * @return the text with replaced URLs
179      */
180     public String replaceUrls(final String text) {
181         if (cachedPathMappingList == null) {
182             synchronized (this) {
183                 if (cachedPathMappingList == null) {
184                     init();
185                 }
186             }
187         }
188         String result = text;
189         for (final PathMapping pathMapping : cachedPathMappingList) {
190             if (matchUserAgent(pathMapping)) {
191                 String replacement = pathMapping.getReplacement();
192                 if (replacement == null) {
193                     replacement = StringUtil.EMPTY;
194                 }
195                 result = result.replaceAll("(\"[^\"]*)" + pathMapping.getRegex() + "([^\"]*\")", "$1" + replacement + "$2");
196             }
197         }
198         return result;
199     }
200 
201     /**
202      * Replaces URL for display or URL converter.
203      *
204      * @param url the URL to replace
205      * @return the replaced URL
206      */
207     public String replaceUrl(final String url) { // for display or url converer
208         if (cachedPathMappingList == null) {
209             synchronized (this) {
210                 if (cachedPathMappingList == null) {
211                     init();
212                 }
213             }
214         }
215         return replaceUrl(cachedPathMappingList, url);
216     }
217 
218     /**
219      * Creates a path matcher function for path mapping.
220      *
221      * @param matcher the regex matcher
222      * @param replacement the replacement string
223      * @return the path matcher function
224      */
225     public BiFunction<String, Matcher, String> createPathMatcher(final Matcher matcher, final String replacement) { // for PathMapping
226         if (FUNCTION_ENCODEURL_MATCHER.equals(replacement)) {
227             return (u, m) -> DocumentUtil.encodeUrl(u);
228         }
229         if (!replacement.startsWith(GROOVY_MATCHER)) {
230             return (u, m) -> m.replaceAll(replacement);
231         }
232         final String template = replacement.substring(GROOVY_MATCHER.length());
233         return (u, m) -> {
234             final Map<String, Object> paramMap = new HashMap<>();
235             paramMap.put("url", u);
236             paramMap.put("matcher", m);
237             final Object value =
238                     ComponentUtil.getScriptEngineFactory().getScriptEngine(Constants.DEFAULT_SCRIPT).evaluate(template, paramMap);
239             if (value == null) {
240                 return u;
241             }
242             return value.toString();
243         };
244     }
245 
246     /**
247      * Replaces URL using the given path mapping list.
248      *
249      * @param pathMappingList the path mapping list
250      * @param url the URL to replace
251      * @return the replaced URL
252      */
253     protected String replaceUrl(final List<PathMapping> pathMappingList, final String url) {
254         String newUrl = url;
255         for (final PathMapping pathMapping : pathMappingList) {
256             if (matchUserAgent(pathMapping)) {
257                 newUrl = pathMapping.process(this, newUrl);
258             }
259         }
260         if (logger.isDebugEnabled() && !StringUtil.equals(url, newUrl)) {
261             logger.debug("replace: {} -> {}", url, newUrl);
262         }
263         return newUrl;
264     }
265 
266     /**
267      * Checks if the user agent matches the path mapping.
268      *
269      * @param pathMapping the path mapping
270      * @return true if the user agent matches
271      */
272     protected boolean matchUserAgent(final PathMapping pathMapping) {
273         if (!pathMapping.hasUAMathcer()) {
274             return true;
275         }
276 
277         if (SingletonLaContainerFactory.getExternalContext().getRequest() != null) {
278             return LaRequestUtil.getOptionalRequest().map(request -> {
279                 final String userAgent = request.getHeader("user-agent");
280                 if (StringUtil.isBlank(userAgent)) {
281                     return false;
282                 }
283 
284                 return pathMapping.getUAMatcher(userAgent).find();
285             }).orElse(false);
286         }
287         return false;
288     }
289 }