001package org.apache.turbine.services.urlmapper; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.IOException; 023import java.io.InputStream; 024import java.util.Arrays; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.Objects; 031import java.util.Set; 032import java.util.regex.Matcher; 033import java.util.regex.Pattern; 034import java.util.stream.Collectors; 035import java.util.stream.Stream; 036 037import javax.xml.bind.JAXBContext; 038import javax.xml.bind.JAXBException; 039import javax.xml.bind.Unmarshaller; 040 041import org.apache.commons.configuration2.Configuration; 042import org.apache.fulcrum.parser.ParameterParser; 043import org.apache.logging.log4j.LogManager; 044import org.apache.logging.log4j.Logger; 045import org.apache.turbine.services.InitializationException; 046import org.apache.turbine.services.TurbineBaseService; 047import org.apache.turbine.services.TurbineServices; 048import org.apache.turbine.services.servlet.ServletService; 049import org.apache.turbine.services.urlmapper.model.URLMapEntry; 050import org.apache.turbine.services.urlmapper.model.URLMappingContainer; 051import org.apache.turbine.util.uri.TurbineURI; 052import org.apache.turbine.util.uri.URIParam; 053 054import com.fasterxml.jackson.databind.ObjectMapper; 055import com.fasterxml.jackson.databind.json.JsonMapper; 056import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 057 058/** 059 * The URL mapper service provides methods to map a set of parameters to a 060 * simplified URL and vice-versa. This service was inspired by the 061 * Liferay Friendly URL Mapper. 062 * <p> 063 * A mapper valve and a link pull tool are provided for easy application. 064 * 065 * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a> 066 * @see URLMapperService 067 * @see MappedTemplateLink 068 * @see URLMapperValve 069 * 070 * @version $Id$ 071 */ 072public class TurbineURLMapperService 073 extends TurbineBaseService 074 implements URLMapperService 075{ 076 /** 077 * Logging. 078 */ 079 private static final Logger log = LogManager.getLogger(TurbineURLMapperService.class); 080 081 /** 082 * The default configuration file. 083 */ 084 private static final String DEFAULT_CONFIGURATION_FILE = "/WEB-INF/conf/turbine-url-mapping.xml"; 085 086 /** 087 * The configuration key for the configuration file. 088 */ 089 private static final String CONFIGURATION_FILE_KEY = "configFile"; 090 091 /** 092 * The container with the URL mappings. 093 */ 094 private URLMappingContainer container; 095 096 /** 097 * Regex pattern for group names, equivalent to the characters defined in java {@link Pattern} (private) groupname method. 098 */ 099 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 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}