View Javadoc
1   package org.apache.turbine.services.urlmapper;
2   
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  
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.util.Arrays;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.LinkedHashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Objects;
31  import java.util.Set;
32  import java.util.regex.Matcher;
33  import java.util.regex.Pattern;
34  import java.util.stream.Collectors;
35  import java.util.stream.Stream;
36  
37  import javax.xml.bind.JAXBContext;
38  import javax.xml.bind.JAXBException;
39  import javax.xml.bind.Unmarshaller;
40  
41  import org.apache.commons.configuration2.Configuration;
42  import org.apache.fulcrum.parser.ParameterParser;
43  import org.apache.logging.log4j.LogManager;
44  import org.apache.logging.log4j.Logger;
45  import org.apache.turbine.services.InitializationException;
46  import org.apache.turbine.services.TurbineBaseService;
47  import org.apache.turbine.services.TurbineServices;
48  import org.apache.turbine.services.servlet.ServletService;
49  import org.apache.turbine.services.urlmapper.model.URLMapEntry;
50  import org.apache.turbine.services.urlmapper.model.URLMappingContainer;
51  import org.apache.turbine.util.uri.TurbineURI;
52  import org.apache.turbine.util.uri.URIParam;
53  
54  import com.fasterxml.jackson.databind.ObjectMapper;
55  import com.fasterxml.jackson.databind.json.JsonMapper;
56  import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
57  
58  /**
59   * The URL mapper service provides methods to map a set of parameters to a
60   * simplified URL and vice-versa. This service was inspired by the
61   * Liferay Friendly URL Mapper.
62   * <p>
63   * A mapper valve and a link pull tool are provided for easy application.
64   *
65   * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
66   * @see URLMapperService
67   * @see MappedTemplateLink
68   * @see URLMapperValve
69   *
70   * @version $Id$
71   */
72  public class TurbineURLMapperService
73          extends TurbineBaseService
74          implements URLMapperService
75  {
76      /**
77       * Logging.
78       */
79      private static final Logger log = LogManager.getLogger(TurbineURLMapperService.class);
80  
81      /**
82       * The default configuration file.
83       */
84      private static final String DEFAULT_CONFIGURATION_FILE = "/WEB-INF/conf/turbine-url-mapping.xml";
85  
86      /**
87       * The configuration key for the configuration file.
88       */
89      private static final String CONFIGURATION_FILE_KEY = "configFile";
90  
91      /**
92       * The container with the URL mappings.
93       */
94      private URLMappingContainer container;
95  
96      /**
97       * Regex pattern for group names, equivalent to the characters defined in java {@link Pattern} (private) groupname method.
98       */
99      private static final Pattern NAMED_GROUPS_PATTERN = Pattern.compile("\\(\\?<([a-zA-Z][a-zA-Z0-9]*)>.+?\\)");
100 
101     /**
102      * Regex pattern for multiple slashes
103      */
104     private static final Pattern MULTI_SLASH_PATTERN = Pattern.compile("[/]+");
105 
106     /**
107      * Symbolic group name for context path
108      */
109     private static final String CONTEXT_PATH_PARAMETER = "contextPath";
110 
111     /**
112      * Symbolic group name for web application root
113      */
114     private static final String WEBAPP_ROOT_PARAMETER = "webAppRoot";
115 
116     /**
117      * Symbolic group names that will not be added to parameters
118      */
119     private static final Set<String> DEFAULT_PARAMETERS = new HashSet<>(Arrays.asList(
120             CONTEXT_PATH_PARAMETER,
121             WEBAPP_ROOT_PARAMETER
122     ));
123 
124     /**
125      * Map a set of parameters (contained in TurbineURI PathInfo and QueryData)
126      * to a TurbineURI
127      *
128      * @param uri the URI to be modified (with setScriptName())
129      */
130     @Override
131     public void mapToURL(TurbineURI uri)
132     {
133         if (!uri.hasPathInfo() && !uri.hasQueryData())
134         {
135             return; // no mapping or mapping already done
136         }
137 
138         List<URIParam> pathInfo = uri.getPathInfo();
139         List<URIParam> queryData = uri.getQueryData();
140 
141         // Create map from list, taking only the first appearance of a key
142         // PathInfo takes precedence
143         Map<String, Object> uriParameterMap =
144                 Stream.concat(pathInfo.stream(), queryData.stream())
145                     .collect(Collectors.toMap(
146                         URIParam::getKey,
147                         URIParam::getValue,
148                         (e1, e2) -> e1,
149                         LinkedHashMap::new));
150 
151         for (URLMapEntry urlMap : container.getMapEntries())
152         {
153             Set<String> keys = new HashSet<>(uriParameterMap.keySet());
154             keys.removeAll(urlMap.getIgnoreParameters().keySet());
155 
156             Set<String> entryKeys = new HashSet<>(urlMap.getGroupNamesMap().keySet());
157 
158             Set<String> implicitKeysFound = urlMap.getImplicitParameters().entrySet().stream()
159                     .filter(entry -> Objects.equals(uriParameterMap.get(entry.getKey()), entry.getValue()))
160                     .map(Map.Entry::getKey)
161                     .collect(Collectors.toSet());
162 
163             entryKeys.addAll(implicitKeysFound);
164 
165             if (entryKeys.containsAll(keys))
166             {
167                 Matcher matcher = NAMED_GROUPS_PATTERN.matcher(urlMap.getUrlPattern().pattern());
168                 StringBuffer sb = new StringBuffer();
169 
170                 while (matcher.find())
171                 {
172                     String key = matcher.group(1);
173 
174                     if (CONTEXT_PATH_PARAMETER.equals(key))
175                     {
176                         // remove
177                         matcher.appendReplacement(sb, "");
178                     } else if (WEBAPP_ROOT_PARAMETER.equals(key))
179                     {
180                         matcher.appendReplacement(sb, uri.getScriptName());
181                     } else
182                     {
183                         boolean ignore = urlMap.getIgnoreParameters().keySet().stream()
184                                 .anyMatch( x-> x.equals( key ) );
185                         matcher.appendReplacement(sb,
186                                  Matcher.quoteReplacement(
187                                         (!ignore)? Objects.toString(uriParameterMap.get(key)):""));
188                         // Remove handled parameters (all of them!)
189                         pathInfo.removeIf(uriParam -> key.equals(uriParam.getKey()));
190                         queryData.removeIf(uriParam -> key.equals(uriParam.getKey()));
191                     }
192                 }
193 
194                 matcher.appendTail(sb);
195                 
196                 implicitKeysFound.forEach(key -> {
197                     pathInfo.removeIf(uriParam -> key.equals(uriParam.getKey()));
198                     queryData.removeIf(uriParam -> key.equals(uriParam.getKey()));
199                 });
200 
201                 // Clean up
202                 uri.setScriptName(MULTI_SLASH_PATTERN.matcher(sb).replaceAll("/").replaceFirst( "/$", "" ));
203                 
204                 break;
205             }
206         }
207         
208         log.debug("mapped to uri: {} ", uri);
209     }
210 
211     /**
212      * Map a simplified URL to a set of parameters
213      *
214      * @param url the URL
215      * @param pp  a ParameterParser to use for parameter mangling
216      */
217     @Override
218     public void mapFromURL(String url, ParameterParser pp)
219     {
220         for (URLMapEntry urlMap : container.getMapEntries())
221         {
222             url = url.replaceFirst( "/$", "" );
223             Matcher matcher = urlMap.getUrlPattern().matcher(url);
224             if (matcher.matches())
225             {
226                 // extract parameters from URL
227                 urlMap.getGroupNamesMap().entrySet().stream()
228                         // ignore default parameters
229                         .filter(group -> !DEFAULT_PARAMETERS.contains(group.getKey()))
230                         .forEach(group ->
231                                 pp.setString(group.getKey(), matcher.group(group.getValue().intValue())));
232 
233                 // add implicit parameters
234                 urlMap.getImplicitParameters().entrySet().forEach(e ->
235                         pp.add(e.getKey(), e.getValue()));
236 
237                 // add override parameters
238                 urlMap.getOverrideParameters().entrySet().forEach(e ->
239                         pp.setString(e.getKey(), e.getValue()));
240 
241                 // remove ignore parameters
242                 urlMap.getIgnoreParameters().keySet().forEach(k ->
243                         pp.remove(k));
244                 
245                 log.debug("mapped {} params from url {} ", pp.getKeys().length, url);
246 
247                 break;
248             }
249         }
250     }
251 
252     // ---- Service initialization ------------------------------------------
253 
254     /**
255      * Initializes the service.
256      */
257     @Override
258     public void init() throws InitializationException
259     {
260         Configuration cfg = getConfiguration();
261 
262         String configFile = cfg.getString(CONFIGURATION_FILE_KEY, DEFAULT_CONFIGURATION_FILE);
263 
264         // context resource path has to begin with slash, cft.
265         // context.getResource
266         if (!configFile.startsWith("/"))
267         {
268             configFile = "/" + configFile;
269         }
270 
271         ServletServicerg/apache/turbine/services/servlet/ServletService.html#ServletService">ServletService servletService = (ServletService) TurbineServices.getInstance().getService(ServletService.SERVICE_NAME);
272 
273         try (InputStream reader = servletService.getResourceAsStream(configFile))
274         {
275             if (configFile.endsWith(".xml"))
276             {
277                 JAXBContext jaxb = JAXBContext.newInstance(URLMappingContainer.class);
278                 Unmarshaller unmarshaller = jaxb.createUnmarshaller();
279                 container = (URLMappingContainer) unmarshaller.unmarshal(reader);
280             } else if (configFile.endsWith(".yml"))
281             {
282                 // org.apache.commons.configuration2.YAMLConfiguration does only expose property like configuration values,
283                 // which is not what we need here -> java object deserialization.
284                 ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
285                 container = mapper.readValue(reader, URLMappingContainer.class);
286             } else if (configFile.endsWith(".json"))
287             {
288                 ObjectMapper mapper = JsonMapper.builder().build();
289                 container = mapper.readValue(reader, URLMappingContainer.class);
290             }
291         }
292         catch (IOException | JAXBException e)
293         {
294             throw new InitializationException("Could not load configuration file " + configFile, e);
295         }
296 
297         // Get groupNamesMap for every Pattern and store it in the entry
298         for (URLMapEntry urlMap : container.getMapEntries())
299         {
300             int position = 1;
301             Map<String, Integer> groupNamesMap = new HashMap<>();
302             Matcher matcher = NAMED_GROUPS_PATTERN.matcher(urlMap.getUrlPattern().pattern());
303 
304             while (matcher.find())
305             {
306                 groupNamesMap.put(matcher.group(1), Integer.valueOf(position++));
307             }
308             urlMap.setGroupNamesMap(groupNamesMap);
309         }
310 
311         log.info("Loaded {} url-mappings from {}", Integer.valueOf(container.getMapEntries().size()), configFile);
312 
313         setInit(true);
314     }
315 
316     /**
317      * Returns to uninitialized state.
318      */
319     @Override
320     public void shutdown()
321     {
322         container.getMapEntries().clear();
323         setInit(false);
324     }
325 }