View Javadoc
1   /*
2    * $Id$
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  package org.apache.struts.config;
22  
23  import java.io.Serializable;
24  import java.util.ArrayList;
25  import java.util.HashMap;
26  import java.util.Map;
27  import java.util.Properties;
28  
29  import org.apache.commons.beanutils.BeanUtils;
30  import org.apache.struts.action.ActionForward;
31  import org.apache.struts.util.WildcardHelper;
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  
35  /**
36   * <p> Matches paths against pre-compiled wildcard expressions pulled from
37   * action configs. It uses the wildcard matcher from the Apache Cocoon
38   * project. Patterns will be matched in the order they exist in the Struts
39   * config file. The last match wins, so more specific patterns should be
40   * defined after less specific patterns.
41   *
42   * @since Struts 1.2
43   */
44  public class ActionConfigMatcher implements Serializable {
45      private static final long serialVersionUID = -7803926870173575845L;
46  
47      /**
48       * The {@code Log} instance for this class.
49       */
50      private transient final Logger log =
51          LoggerFactory.getLogger(ActionConfigMatcher.class);
52  
53      /**
54       * <p> Handles all wildcard pattern matching. </p>
55       */
56      private static final WildcardHelper wildcard = new WildcardHelper();
57  
58      /**
59       * <p> The compiled paths and their associated ActionConfig's </p>
60       */
61      private ArrayList<Mapping> compiledPaths;
62  
63      /**
64       * <p> Finds and precompiles the wildcard patterns from the ActionConfig
65       * "path" attributes. ActionConfig's will be evaluated in the order they
66       * exist in the Struts config file. Only paths that actually contain a
67       * wildcard will be compiled. </p>
68       *
69       * @param configs An array of ActionConfig's to process
70       */
71      public ActionConfigMatcher(ActionConfig[] configs) {
72          compiledPaths = new ArrayList<>();
73  
74          int[] pattern;
75          String path;
76  
77          for (int x = 0; x < configs.length; x++) {
78              path = configs[x].getPath();
79  
80              if ((path != null) && (path.indexOf('*') > -1)) {
81                  if ((path.length() > 0) && (path.charAt(0) == '/')) {
82                      path = path.substring(1);
83                  }
84  
85                  log.debug("Compiling action config path '{}'", path);
86  
87                  pattern = wildcard.compilePattern(path);
88                  compiledPaths.add(new Mapping(pattern, configs[x]));
89              }
90          }
91      }
92  
93      /**
94       * <p> Matches the path against the compiled wildcard patterns. </p>
95       *
96       * @param path The portion of the request URI for selecting a config.
97       * @return The action config if matched, else null
98       */
99      public ActionConfig match(String path) {
100         ActionConfig config = null;
101 
102         if (compiledPaths.size() > 0) {
103             log.debug("Attempting to match '{}' to a wildcard pattern",
104                 path);
105 
106             if ((path.length() > 0) && (path.charAt(0) == '/')) {
107                 path = path.substring(1);
108             }
109 
110             HashMap<String, String> vars = new HashMap<>();
111 
112             for (Mapping m : compiledPaths) {
113                 if (wildcard.match(vars, path, m.getPattern())) {
114                     log.debug("Path matches pattern '{}'",
115                         m.getActionConfig().getPath());
116 
117                     try {
118                         config =
119                             convertActionConfig(path, m.getActionConfig(), vars);
120                     } catch (IllegalStateException e) {
121                         log.warn("Path matches pattern '{}' but is "
122                             + "incompatible with the matching config due "
123                             + "to recursive substitution: {}",
124                             m.getActionConfig().getPath(), path);
125                         config = null;
126                     }
127                 }
128             }
129         }
130 
131         return config;
132     }
133 
134     /**
135      * <p> Clones the ActionConfig and its children, replacing various
136      * properties with the values of the wildcard-matched strings. </p>
137      *
138      * @param path The requested path
139      * @param orig The original ActionConfig
140      * @param vars A Map of wildcard-matched strings
141      * @return A cloned ActionConfig with appropriate properties replaced with
142      *         wildcard-matched values
143      * @throws IllegalStateException if a placeholder substitution is
144      * impossible due to recursion
145      */
146     protected ActionConfig convertActionConfig(String path, ActionConfig orig,
147         Map<String, String> vars) {
148         ActionConfig config = null;
149 
150         try {
151             config = (ActionConfig) BeanUtils.cloneBean(orig);
152         } catch (Exception ex) {
153             log.warn("Unable to clone action config, recommend not using "
154                 + "wildcards", ex);
155 
156             return null;
157         }
158 
159         config.setName(convertParam(orig.getName(), vars));
160 
161         if ((path.length() == 0) || (path.charAt(0) != '/')) {
162             path = "/" + path;
163         }
164 
165         config.setPath(path);
166         config.setType(convertParam(orig.getType(), vars));
167         config.setRoles(convertParam(orig.getRoles(), vars));
168         config.setParameter(convertParam(orig.getParameter(), vars));
169         config.setAttribute(convertParam(orig.getAttribute(), vars));
170         config.setForward(convertParam(orig.getForward(), vars));
171         config.setInclude(convertParam(orig.getInclude(), vars));
172         config.setInput(convertParam(orig.getInput(), vars));
173         config.setCatalog(convertParam(orig.getCatalog(), vars));
174         config.setCommand(convertParam(orig.getCommand(), vars));
175         config.setMultipartClass(convertParam(orig.getMultipartClass(), vars));
176         config.setPrefix(convertParam(orig.getPrefix(), vars));
177         config.setSuffix(convertParam(orig.getSuffix(), vars));
178 
179         ForwardConfig[] fConfigs = orig.findForwardConfigs();
180         ForwardConfig cfg;
181 
182         for (int x = 0; x < fConfigs.length; x++) {
183             try {
184                 cfg = (ActionForward) BeanUtils.cloneBean(fConfigs[x]);
185             } catch (Exception ex) {
186                 log.warn("Unable to clone action config, recommend not using "
187                         + "wildcards", ex);
188                 return null;
189             }
190             cfg.setName(fConfigs[x].getName());
191             cfg.setPath(convertParam(fConfigs[x].getPath(), vars));
192             cfg.setRedirect(fConfigs[x].getRedirect());
193             cfg.setCommand(convertParam(fConfigs[x].getCommand(), vars));
194             cfg.setCatalog(convertParam(fConfigs[x].getCatalog(), vars));
195             cfg.setModule(convertParam(fConfigs[x].getModule(), vars));
196 
197             replaceProperties(fConfigs[x].getProperties(), cfg.getProperties(),
198                 vars);
199 
200             config.removeForwardConfig(fConfigs[x]);
201             config.addForwardConfig(cfg);
202         }
203 
204         replaceProperties(orig.getProperties(), config.getProperties(), vars);
205 
206         ExceptionConfig[] exConfigs = orig.findExceptionConfigs();
207 
208         for (int x = 0; x < exConfigs.length; x++) {
209             config.addExceptionConfig(exConfigs[x]);
210         }
211 
212         config.freeze();
213 
214         return config;
215     }
216 
217     /**
218      * <p> Replaces placeholders from one Properties values set to another.
219      * </p>
220      *
221      * @param orig  The original properties set with placehold values
222      * @param props The target properties to store the processed values
223      * @param vars  A Map of wildcard-matched strings
224      * @throws IllegalStateException if a placeholder substitution is
225      * impossible due to recursion
226      */
227     protected void replaceProperties(Properties orig, Properties props, Map<String, String> vars) {
228         for (Map.Entry<Object, Object> entry : orig.entrySet()) {
229             props.setProperty((String) entry.getKey(),
230                 convertParam((String) entry.getValue(), vars));
231         }
232     }
233 
234     /**
235      * <p> Inserts into a value wildcard-matched strings where specified.
236      * </p>
237      *
238      * @param val  The value to convert
239      * @param vars A Map of wildcard-matched strings
240      * @return The new value
241      * @throws IllegalStateException if a placeholder substitution is
242      * impossible due to recursion
243      */
244     protected String convertParam(String val, Map<String, String> vars) {
245         if (val == null) {
246             return null;
247         } else if (val.indexOf("{") == -1) {
248             return val;
249         }
250 
251         StringBuilder key = new StringBuilder("{0}");
252         StringBuilder ret = new StringBuilder(val);
253         String keyStr;
254         int x;
255 
256         for (Map.Entry<String, String> entry : vars.entrySet()) {
257             key.setCharAt(1, entry.getKey().charAt(0));
258             keyStr = key.toString();
259 
260             // STR-3169
261             // Prevent an infinite loop by retaining the placeholders
262             // that contain itself in the substitution value
263             if (entry.getValue().contains(keyStr)) {
264                 throw new IllegalStateException();
265             }
266 
267             // Replace all instances of the placeholder
268             while ((x = ret.toString().indexOf(keyStr)) > -1) {
269                 ret.replace(x, x + 3, entry.getValue());
270             }
271         }
272 
273         return ret.toString();
274     }
275 
276     /**
277      * <p> Stores a compiled wildcard pattern and the ActionConfig it came
278      * from. </p>
279      */
280     private class Mapping implements Serializable {
281         private static final long serialVersionUID = -4524356639556048603L;
282 
283         /**
284          * <p> The compiled pattern. </p>
285          */
286         private int[] pattern;
287 
288         /**
289          * <p> The original ActionConfig. </p>
290          */
291         private ActionConfig config;
292 
293         /**
294          * <p> Contructs a read-only Mapping instance. </p>
295          *
296          * @param pattern The compiled pattern
297          * @param config  The original ActionConfig
298          */
299         public Mapping(int[] pattern, ActionConfig config) {
300             this.pattern = pattern;
301             this.config = config;
302         }
303 
304         /**
305          * <p> Gets the compiled wildcard pattern. </p>
306          *
307          * @return The compiled pattern
308          */
309         public int[] getPattern() {
310             return this.pattern;
311         }
312 
313         /**
314          * <p> Gets the ActionConfig that contains the pattern. </p>
315          *
316          * @return The associated ActionConfig
317          */
318         public ActionConfig getActionConfig() {
319             return this.config;
320         }
321     }
322 }