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.filter;
17  
18  import java.io.IOException;
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.HashMap;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.concurrent.ConcurrentHashMap;
25  
26  import org.apache.commons.codec.DecoderException;
27  import org.apache.commons.codec.net.URLCodec;
28  import org.codelibs.core.lang.StringUtil;
29  import org.lastaflute.web.servlet.filter.LastaPrepareFilter;
30  
31  import jakarta.servlet.Filter;
32  import jakarta.servlet.FilterChain;
33  import jakarta.servlet.FilterConfig;
34  import jakarta.servlet.ServletContext;
35  import jakarta.servlet.ServletException;
36  import jakarta.servlet.ServletRequest;
37  import jakarta.servlet.ServletResponse;
38  import jakarta.servlet.http.HttpServletRequest;
39  import jakarta.servlet.http.HttpServletResponse;
40  
41  /**
42   * Servlet filter for handling character encoding conversion and URL redirection.
43   * This filter processes requests with specific encoding requirements and converts
44   * character encodings according to configured mapping rules.
45   *
46   * <p>The filter intercepts requests matching configured path patterns and
47   * redirects them with proper character encoding applied to parameters.</p>
48   */
49  public class EncodingFilter implements Filter {
50      /** Configuration key for encoding rules mapping */
51      public static final String ENCODING_MAP = "encodingRules";
52  
53      /** Map of path patterns to their corresponding character encodings */
54      protected Map<String, String> encodingMap = new ConcurrentHashMap<>();
55  
56      /** Default character encoding to use for requests */
57      protected String encoding;
58  
59      /** Servlet context for this filter */
60      protected ServletContext servletContext;
61  
62      /** URL codec for encoding and decoding URL parameters */
63      protected URLCodec urlCodec = new URLCodec();
64  
65      /**
66       * Default constructor for EncodingFilter.
67       */
68      public EncodingFilter() {
69          // Default constructor
70      }
71  
72      /**
73       * Initializes the filter with configuration parameters.
74       * Sets up encoding mappings and default encoding from filter configuration.
75       *
76       * @param config the filter configuration containing initialization parameters
77       * @throws ServletException if an error occurs during initialization
78       */
79      @Override
80      public void init(final FilterConfig config) throws ServletException {
81          servletContext = config.getServletContext();
82  
83          encoding = config.getInitParameter(LastaPrepareFilter.ENCODING_KEY);
84          if (encoding == null) {
85              encoding = LastaPrepareFilter.DEFAULT_ENCODING;
86          }
87  
88          // ex. sjis:Shift_JIS,eucjp:EUC-JP
89          final String value = config.getInitParameter(ENCODING_MAP);
90          if (StringUtil.isNotBlank(value)) {
91              final String[] encodingPairs = value.split(",");
92              for (final String pair : encodingPairs) {
93                  final String[] encInfos = pair.trim().split(":");
94                  if (encInfos.length == 2) {
95                      encodingMap.put("/" + encInfos[0] + "/", encInfos[1]);
96                  }
97              }
98          }
99      }
100 
101     /**
102      * Processes requests and applies character encoding conversion if needed.
103      * Checks if the request path matches any configured encoding rule and
104      * performs URL redirection with proper parameter encoding.
105      *
106      * @param request the servlet request to process
107      * @param response the servlet response to use for redirection
108      * @param chain the filter chain to continue processing
109      * @throws IOException if an I/O error occurs during processing
110      * @throws ServletException if a servlet error occurs
111      */
112     @Override
113     public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
114             throws IOException, ServletException {
115         final HttpServletRequest req = (HttpServletRequest) request;
116         final String servletPath = req.getServletPath();
117         for (final Map.Entry<String, String> entry : encodingMap.entrySet()) {
118             final String path = entry.getKey();
119             if (servletPath.startsWith(path)) {
120                 req.setCharacterEncoding(entry.getValue());
121                 final StringBuilder locationBuf = new StringBuilder(1000);
122                 final String contextPath = servletContext.getContextPath();
123                 if (StringUtil.isNotBlank(contextPath) && !"/".equals(contextPath)) {
124                     locationBuf.append(contextPath);
125                 }
126                 locationBuf.append('/');
127                 locationBuf.append(servletPath.substring(path.length()));
128                 boolean append = false;
129                 final Map<String, String[]> parameterMap = new HashMap<>(req.getParameterMap());
130                 parameterMap.putAll(getParameterMapFromQueryString(req, entry.getValue()));
131                 for (final Map.Entry<String, String[]> paramEntry : parameterMap.entrySet()) {
132                     final String[] values = paramEntry.getValue();
133                     if (values == null) {
134                         continue;
135                     }
136                     final String key = paramEntry.getKey();
137                     for (final String value : values) {
138                         if (append) {
139                             locationBuf.append('&');
140                         } else {
141                             locationBuf.append('?');
142                             append = true;
143                         }
144                         locationBuf.append(urlCodec.encode(key, encoding));
145                         locationBuf.append('=');
146                         locationBuf.append(urlCodec.encode(value, encoding));
147                     }
148 
149                 }
150                 final HttpServletResponse res = (HttpServletResponse) response;
151                 res.sendRedirect(locationBuf.toString());
152                 return;
153             }
154         }
155 
156         chain.doFilter(request, response);
157     }
158 
159     /**
160      * Extracts and parses parameters from the request query string.
161      * Applies the specified character encoding to decode parameter values.
162      *
163      * @param request the HTTP request containing the query string
164      * @param enc the character encoding to use for decoding
165      * @return a map of parameter names to their decoded values
166      * @throws IOException if an error occurs during parameter parsing
167      */
168     protected Map<String, String[]> getParameterMapFromQueryString(final HttpServletRequest request, final String enc) throws IOException {
169         final String queryString = request.getQueryString();
170         if (StringUtil.isNotBlank(queryString)) {
171             return parseQueryString(queryString, enc);
172         }
173         return Collections.emptyMap();
174     }
175 
176     /**
177      * Parses a query string and extracts parameter name-value pairs.
178      * Applies URL decoding with the specified character encoding.
179      *
180      * @param queryString the query string to parse
181      * @param enc the character encoding to use for URL decoding
182      * @return a map of parameter names to their decoded values
183      * @throws IOException if an error occurs during URL decoding
184      */
185     protected Map<String, String[]> parseQueryString(final String queryString, final String enc) throws IOException {
186         final Map<String, List<String>> paramListMap = new HashMap<>();
187         final String[] pairs = queryString.split("&");
188         try {
189             for (final String pair : pairs) {
190                 final int pos = pair.indexOf('=');
191                 if (pos >= 0) {
192                     final String key = urlCodec.decode(pair.substring(0, pos), enc);
193                     List<String> list = paramListMap.get(key);
194                     if (list == null) {
195                         list = new ArrayList<>();
196                         paramListMap.put(key, list);
197                     }
198                     if (pos + 1 < pair.length()) {
199                         list.add(urlCodec.decode(pair.substring(pos + 1), enc));
200                     } else {
201                         list.add(StringUtil.EMPTY);
202                     }
203                 } else {
204                     final String key = urlCodec.decode(pair, enc);
205                     List<String> list = paramListMap.get(key);
206                     if (list == null) {
207                         list = new ArrayList<>();
208                         paramListMap.put(key, list);
209                     }
210                     list.add(StringUtil.EMPTY);
211                 }
212             }
213         } catch (final DecoderException e) {
214             throw new IOException(e);
215         }
216 
217         final Map<String, String[]> paramMap = new HashMap<>(paramListMap.size());
218         for (final Map.Entry<String, List<String>> entry : paramListMap.entrySet()) {
219             final List<String> list = entry.getValue();
220             paramMap.put(entry.getKey(), list.toArray(new String[list.size()]));
221         }
222         return paramMap;
223     }
224 
225     /**
226      * Cleans up resources when the filter is destroyed.
227      * Currently performs no cleanup operations.
228      */
229     @Override
230     public void destroy() {
231         // nothing
232     }
233 }