/**
* Copyright 2011 meltmedia
*
* Licensed 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.
*/
package org.xchain.framework.filter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownServiceException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.Rule;
import org.apache.commons.digester.RuleSetBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xchain.framework.net.UrlFactory;
import org.xml.sax.Attributes;
/**
* <p>This filter specifies URL translation for the the webapp.</p>
*
* <p>The 'enabled' parameter defines whether the filter is enabled. This is assumed to be 'true'
* if it is not present.</p>
*
* <p>The 'config-resource-url' parameter defines the location of the filter config file relative
* to the context class loader.</p>
*
* <h4>Example filter element:</h4>
*
* <pre>
* <filter>
* <filter-name>UrlTranslationFilter</filter-name>
* <filter-class>org.xchain.framework.filter.UrlTranslationFilter</filter-class>
* <init-param>
* <param-name>config-resource-url</param-name>
* <param-value>META-INF/translation-config.xml</param-value>
* </init-param>
* <init-param>
* <param-name>enabled</param-name>
* <param-value>true</param-value>
* </init-param>
* </filter>
* </pre>
*
* <h4>Example filter-mapping element:</h4>
*
* <pre>
* <filter-mapping>
* <filter-name>UrlTranslationFilter</filter-name>
* <url-pattern>/redirect/*</url-pattern>
* </filter-mapping>
* </pre>
*
* Filter configuration file is in the namespace "http://xchain.org/container/url-translation-filter-config/1.0".
* The config can have any number of <code>entry</code> elements. Each <code>entry</code> element must have a <code>pattern</code>
* and a <code>location</code> attribute. The <code>pattern</code> attribute is a regular expression to match incoming requests. If
* the pattern matches then the request will be redirectd to the <code>location</code> attribute. The <code>location</code> attribute
* can contain markers in the form of ${<i>num</i>} where <i>num</i> is a group number from the matched pattern.
*
* <h4>Example filter config:</h4>
*
* <pre>
* <?xml version="1.0" encoding="UTF-8"?>
* <config:config xmlns:config="http://xchain.org/container/url-translation-filter-config/1.0">
* <config:entry pattern="\A/redirect/(.*)\Z" location="http://www.other.domain.com/${1}"/>
* </config:config>
* </pre>
*
* @author Devon Tackett
* @author Mike Moulton
* @author Christian Trimble
* @author Jason Rose
* @author Josh Kennedy
*/
public class UrlTranslationFilter
implements Filter
{
// Local logger
private static Logger log = LoggerFactory.getLogger(UrlTranslationFilter.class);
public static String CONFIG_RESOURCE_URL_PARAM_NAME = "config-resource-url";
public static String ENABLED_PARAM_NAME = "enabled";
private boolean enabled = true;
private Map<Pattern, String>translationMap = new LinkedHashMap<Pattern, String>();
private ServletContext servletContext;
public void init( FilterConfig filterConfig )
throws ServletException
{
servletContext = filterConfig.getServletContext();
if (filterConfig.getInitParameter(ENABLED_PARAM_NAME) != null) {
this.enabled = Boolean.valueOf(filterConfig.getInitParameter(ENABLED_PARAM_NAME)).booleanValue();
}
// Logger whether the filter is enabled.
if (log.isDebugEnabled()) {
if (this.enabled)
log.debug("UrlTranslationFilter is ENABLED");
else
log.debug("UrlTranslationFilter is DISABLED");
}
if (enabled) {
try {
URL configResourceUrl = getConfigurationURL(filterConfig);
if (configResourceUrl == null) {
throw new Exception("Configuration file could not be found.");
}
loadConfiguration(configResourceUrl);
} catch (Exception ex) {
if (log.isWarnEnabled()) {
log.warn("Failed to configure UrlTranslationFilter.", ex);
}
enabled = false;
}
}
}
/**
* Load the configuration from the given URL.
*
* @param configResourceUrl The URL to the configuration file.
*/
private void loadConfiguration(URL configResourceUrl)
throws Exception
{
Digester digester = new Digester();
digester.push(this);
new ConfigurationRuleSet().addRuleInstances(digester);
digester.parse(configResourceUrl);
}
/**
* Rule set for digesting the configuration file.
*/
private static class ConfigurationRuleSet
extends RuleSetBase
{
private static String CONFIG_ELEMENT = "config";
private static String ENTRY_ELEMENT = "entry";
private static String PATTERN_ATTRIBUTE_NAME = "pattern";
private static String LOCATION_ATTRIBUTE_NAME = "location";
private static String NAMESPACE_URI = "http://xchain.org/container/url-translation-filter-config/1.0";
public void addRuleInstances( Digester digester )
{
digester.setNamespaceAware(true);
digester.setRuleNamespaceURI(NAMESPACE_URI);
digester.addRule(CONFIG_ELEMENT, new ConfigRule());
digester.addRule(CONFIG_ELEMENT + "/" + ENTRY_ELEMENT, new EntryRule());
}
public static class ConfigRule
extends Rule
{
@Override
public void begin( String namespace, String name, Attributes attributes )
throws Exception
{
// Ensure that the UrlTranlationFilter is on the stack.
Object top = digester.peek();
if (!(top instanceof UrlTranslationFilter)) {
throw new Exception("UrlTranlationFilter not found on the digester stack.");
}
}
}
public static class EntryRule
extends Rule
{
@Override
public void begin(String namespace, String name, Attributes attributes)
throws Exception {
UrlTranslationFilter filter = (UrlTranslationFilter)digester.peek();
// Add the translation.
filter.addTranslation(Pattern.compile(attributes.getValue(PATTERN_ATTRIBUTE_NAME)), attributes.getValue(LOCATION_ATTRIBUTE_NAME));
}
}
}
/**
* Add a translation for the filter.
*
* @param regEx The pattern to match on.
* @param location The location to translate to.
*/
private void addTranslation(Pattern regEx, String location) {
translationMap.put(regEx, location);
}
/**
* Get the URL to the configuration file from the FilterConfig.
*
* @param filterConfig The FilterConfig to check.
*
* @return The URL to the configuration file.
*/
private URL getConfigurationURL(FilterConfig filterConfig)
throws Exception
{
URL configResourceUrl = null;
String configResourceUrlParameter = filterConfig.getInitParameter(CONFIG_RESOURCE_URL_PARAM_NAME);
if (configResourceUrlParameter != null && configResourceUrlParameter.trim().length() != 0) {
configResourceUrl = Thread.currentThread().getContextClassLoader().getResource( configResourceUrlParameter );
} else {
throw new Exception("Configuration is not specified.");
}
return configResourceUrl;
}
public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain )
throws IOException, ServletException
{
// Perform Url Translation if enabled.
if (this.enabled) {
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
URL url = null;
URLConnection connection = null;
InputStream in = null;
OutputStream out = null;
String path = httpRequest.getServletPath();
boolean matchFound = false;
try {
// Check the incoming path against the registered patterns.
for (Pattern pattern : translationMap.keySet()) {
Matcher match = pattern.matcher(path);
if (match.matches()) {
// Match found. Get the URI the pattern translates to.
String uri = translationMap.get(pattern);
// Perform group substitution.
for(int group = 1; group <= match.groupCount(); group++) {
uri = uri.replaceAll("\\$[{]" + group + "[}]", match.group(group));
}
// Create a new URL from the translated uri.
url = UrlFactory.getInstance().newUrl(uri);
matchFound = true;
break;
}
}
if (!matchFound) {
// No match found. Let the request go through.
chain.doFilter(request, response);
return;
}
if (log.isDebugEnabled()) {
log.debug("doFilter: redirecting " + path + " to " + url);
}
// get a connection to the url.
connection = url.openConnection();
// set the headers.
httpResponse.setContentLength(connection.getContentLength());
// try to set the content type header defined in the container.
String servletContentType = servletContext.getMimeType(path);
if( servletContentType != null ) {
httpResponse.setContentType(servletContentType);
}
// get the streams.
in = connection.getInputStream();
out = response.getOutputStream();
// create buffer and length for coping.
byte[] buffer = new byte[1024];
int length = 0;
// transfer the bytes.
while( (length = in.read(buffer)) > 0 ) {
out.write( buffer, 0, length );
}
}
catch( MalformedURLException mue ) {
throw new ServletException(mue);
}
catch( UnknownServiceException use ) {
throw new ServletException("The protocol '"+url.getProtocol()+"' does not support input.", use);
}
finally {
// close the streams.
if( in != null ) {
try { in.close(); }
catch( IOException ioe ) { }
}
if( out != null ) {
try { out.close(); }
catch( IOException ioe ) { }
}
}
}
else
chain.doFilter(request, response);
}
/**
* @return Whether the UrlTranslationFilter is enabled.
*/
public boolean isEnabled() {
return enabled;
}
public void destroy()
{
// There is no need to clean anything up.
}
}