blob: dc79c2f9032f3f40f64ad77edb5a97b6fe05614e [file] [log] [blame]
package org.apache.turbine.services.urlmapper;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.apache.commons.configuration2.Configuration;
import org.apache.fulcrum.parser.ParameterParser;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.services.servlet.ServletService;
import org.apache.turbine.services.urlmapper.model.URLMapEntry;
import org.apache.turbine.services.urlmapper.model.URLMappingContainer;
import org.apache.turbine.util.uri.TurbineURI;
import org.apache.turbine.util.uri.URIParam;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
/**
* The URL mapper service provides methods to map a set of parameters to a
* simplified URL and vice-versa. This service was inspired by the
* Liferay Friendly URL Mapper.
* <p>
* A mapper valve and a link pull tool are provided for easy application.
*
* @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
* @see URLMapperService
* @see MappedTemplateLink
* @see URLMapperValve
*
* @version $Id$
*/
public class TurbineURLMapperService
extends TurbineBaseService
implements URLMapperService
{
/**
* Logging.
*/
private static final Logger log = LogManager.getLogger(TurbineURLMapperService.class);
/**
* The default configuration file.
*/
private static final String DEFAULT_CONFIGURATION_FILE = "/WEB-INF/conf/turbine-url-mapping.xml";
/**
* The configuration key for the configuration file.
*/
private static final String CONFIGURATION_FILE_KEY = "configFile";
/**
* The container with the URL mappings.
*/
private URLMappingContainer container;
/**
* Regex pattern for group names
*/
private static final Pattern namedGroupsPattern = Pattern.compile("\\(\\?<([a-zA-Z][a-zA-Z0-9]*)>.+?\\)");
/**
* Symbolic group name for context path
*/
private static final String CONTEXT_PATH_PARAMETER = "contextPath";
/**
* Symbolic group name for web application root
*/
private static final String WEBAPP_ROOT_PARAMETER = "webAppRoot";
/**
* Symbolic group names that will not be added to parameters
*/
private static final Set<String> DEFAULT_PARAMETERS = Stream.of(
CONTEXT_PATH_PARAMETER,
WEBAPP_ROOT_PARAMETER
).collect(Collectors.toSet());
/**
* Map a set of parameters (contained in TurbineURI PathInfo and QueryData)
* to a TurbineURI
*
* @param uri the URI to be modified (with setScriptName())
*/
@Override
public void mapToURL(TurbineURI uri)
{
// Create map from list, taking only the first appearance of a key
// PathInfo takes precedence
Map<String, Object> uriParameterMap = Stream.concat(
uri.getPathInfo().stream(),
uri.getQueryData().stream())
.collect(Collectors.toMap(
URIParam::getKey,
URIParam::getValue,
(e1, e2) -> e1,
LinkedHashMap::new));
Set<String> keys = new HashSet<>(uriParameterMap.keySet());
if (keys.isEmpty() && uri.getQueryData().isEmpty() || uri.getPathInfo().isEmpty())
{
return; // no mapping or mapping already done
}
for (URLMapEntry urlMap : container.getMapEntries())
{
Set<String> entryKeys = new HashSet<>();
Map<String, Integer> groupNamesMap = urlMap.getGroupNamesMap();
if (groupNamesMap != null)
{
entryKeys.addAll(groupNamesMap.keySet());
}
Set<String> implicitKeysFound = urlMap.getImplicitParameters().entrySet().stream()
.filter(entry -> Objects.equals(uriParameterMap.get(entry.getKey()), entry.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
entryKeys.addAll(implicitKeysFound);
implicitKeysFound.forEach(key -> {
uri.removePathInfo(key);
uri.removeQueryData(key);
});
keys.removeAll(urlMap.getIgnoreParameters().keySet());
if (entryKeys.containsAll(keys))
{
Matcher matcher = namedGroupsPattern.matcher(urlMap.getUrlPattern().pattern());
StringBuffer sb = new StringBuffer();
while (matcher.find())
{
String key = matcher.group(1);
if (CONTEXT_PATH_PARAMETER.equals(key))
{
// remove
matcher.appendReplacement(sb, "");
} else if (WEBAPP_ROOT_PARAMETER.equals(key))
{
matcher.appendReplacement(sb, uri.getScriptName());
} else
{
matcher.appendReplacement(sb,
Matcher.quoteReplacement(
Objects.toString(uriParameterMap.get(key))));
// Remove handled parameters (all of them!)
uri.removePathInfo(key);
uri.removeQueryData(key);
}
}
matcher.appendTail(sb);
// Clean up
uri.setScriptName(sb.toString().replace("//", "/"));
break;
}
}
}
/**
* Map a simplified URL to a set of parameters
*
* @param url the URL
* @param pp a ParameterParser to use for parameter mangling
*/
@Override
public void mapFromURL(String url, ParameterParser pp)
{
for (URLMapEntry urlMap : container.getMapEntries())
{
Matcher matcher = urlMap.getUrlPattern().matcher(url);
if (matcher.matches())
{
// extract parameters from URL
Map<String, Integer> groupNameMap = urlMap.getGroupNamesMap();
if (groupNameMap != null)
{
groupNameMap.entrySet().stream()
// ignore default parameters
.filter(group -> !DEFAULT_PARAMETERS.contains(group.getKey()))
.forEach(group ->
pp.setString(group.getKey(), matcher.group(group.getValue().intValue())));
}
// add implicit parameters
urlMap.getImplicitParameters().entrySet().forEach(e ->
pp.add(e.getKey(), e.getValue()));
// add override parameters
urlMap.getOverrideParameters().entrySet().forEach(e ->
pp.setString(e.getKey(), e.getValue()));
// remove ignore parameters
urlMap.getIgnoreParameters().keySet().forEach(k ->
pp.remove(k));
break;
}
}
}
// ---- Service initialization ------------------------------------------
/**
* Initializes the service.
*/
@Override
public void init() throws InitializationException
{
Configuration cfg = getConfiguration();
String configFile = cfg.getString(CONFIGURATION_FILE_KEY, DEFAULT_CONFIGURATION_FILE);
// context resource path has to begin with slash, cft.
// context.getResource
if (!configFile.startsWith("/"))
{
configFile = "/" + configFile;
}
ServletService servletService = (ServletService) TurbineServices.getInstance().getService(ServletService.SERVICE_NAME);
try (InputStream reader = servletService.getResourceAsStream(configFile))
{
if (configFile.endsWith(".xml"))
{
JAXBContext jaxb = JAXBContext.newInstance(URLMappingContainer.class);
Unmarshaller unmarshaller = jaxb.createUnmarshaller();
container = (URLMappingContainer) unmarshaller.unmarshal(reader);
} else if (configFile.endsWith(".yml"))
{
// org.apache.commons.configuration2.YAMLConfiguration does only expose property like configuration values,
// which is not what we need here -> java object deserialization.
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
container = mapper.readValue(reader, URLMappingContainer.class);
}
}
catch (IOException | JAXBException e)
{
throw new InitializationException("Could not load configuration file " + configFile, e);
}
// Get groupNamesMap for every Pattern and store it in the entry
try
{
Method namedGroupsMethod = Pattern.class.getDeclaredMethod("namedGroups");
namedGroupsMethod.setAccessible(true);
for (URLMapEntry urlMap : container.getMapEntries())
{
@SuppressWarnings("unchecked")
Map<String, Integer> groupNamesMap = (Map<String, Integer>) namedGroupsMethod.invoke(urlMap.getUrlPattern());
urlMap.setGroupNamesMap(groupNamesMap);
}
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e)
{
throw new InitializationException("Could not invoke method Pattern.getNamedGroups", e);
}
log.info("Loaded {} url-mappings from {}", Integer.valueOf(container.getMapEntries().size()), configFile);
setInit(true);
}
/**
* Returns to uninitialized state.
*/
@Override
public void shutdown()
{
container = null;
setInit(false);
}
}