package org.ocpsoft.rewrite.servlet.config.proxy;

/**
 * Copyright MITRE
 *
 * 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.
 */

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.BitSet;
import java.util.Enumeration;
import java.util.Formatter;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.AbortableHttpRequest;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.HeaderGroup;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.ocpsoft.logging.Logger;

/**
 * An HTTP reverse proxy/gateway servlet. It is designed to be extended for customization if desired. Most of the work
 * is handled by Apache HttpClient.
 *

 * There are alternatives to a servlet based proxy such as Apache mod_proxy if that is available to you. However this
 * servlet is easily customizable by Java, secure-able by your web application's security (e.g. spring-security),
 * portable across servlet engines, and is embeddable into another web application.
 *

 *

 * Inspiration: http://httpd.apache.org/docs/2.0/mod/mod_proxy.html
 *

 *
 * @author David Smiley dsmiley@mitre.org
 */
public class ProxyServlet
{
   /* INIT PARAMETER NAME CONSTANTS */

   private static final long serialVersionUID = -362164247914670579L;

   /**
    * A boolean parameter name to enable logging of input and target URLs to the servlet log.
    */
   public static final String P_LOG = "log";

   /**
    * The parameter name for the target (destination) URI to proxy to.
    */
   private static final String P_TARGET_URI = "targetUri";

   /* MISC */

   protected boolean doLog = false;
   protected URI targetUriObj;
   protected String targetUri;
   protected HttpClient proxyClient;

   private ServletConfig servletConfig;

   private static final Logger logger = Logger.getLogger(ProxyServlet.class);

   public ServletConfig getServletConfig()
   {
      return servletConfig;
   }

   public void init(ServletConfig servletConfig) throws ServletException
   {
      this.servletConfig = servletConfig;
      String doLogStr = servletConfig.getInitParameter(P_LOG);
      if (doLogStr != null)
      {
         this.doLog = Boolean.parseBoolean(doLogStr);
      }

      try
      {
         targetUriObj = new URI(servletConfig.getInitParameter(P_TARGET_URI));
      }
      catch (Exception e)
      {
         throw new RuntimeException("Trying to process targetUri init parameter: " + e, e);
      }
      targetUri = targetUriObj.toString();

      HttpParams hcParams = new BasicHttpParams();
      readConfigParam(hcParams, ClientPNames.HANDLE_REDIRECTS, Boolean.class);
      proxyClient = createHttpClient(hcParams);
   }

   /**
    * Called from {@link #init(javax.servlet.ServletConfig)}. HttpClient offers many opportunities for customization.
    */
   protected HttpClient createHttpClient(HttpParams hcParams)
   {
      return new DefaultHttpClient(new ThreadSafeClientConnManager(), hcParams);
   }

   protected void readConfigParam(HttpParams hcParams, String hcParamName, Class type)
   {
      String val_str = getServletConfig().getInitParameter(hcParamName);
      if (val_str == null)
         return;
      Object val_obj;
      if (type == String.class)
      {
         val_obj = val_str;
      }
      else
      {
         try
         {
            /*
             * noinspection unchecked
             */
            val_obj = type.getMethod("valueOf", String.class).invoke(type, val_str);
         }
         catch (Exception e)
         {
            throw new RuntimeException(e);
         }
      }
      hcParams.setParameter(hcParamName, val_obj);
   }

   public void destroy()
   {
      /*
       * shutdown() must be called according to documentation.
       */
      if (proxyClient != null)
         proxyClient.getConnectionManager().shutdown();
   }

   protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
            throws ServletException, IOException
   {
      /*
       * Note: we won't transfer the protocol version because I'm not sure it would truly be compatible
       */
      String method = servletRequest.getMethod();
      HttpRequest proxyRequest;
      /*
       * Spec: RFC 2616, sec 4.3: either of these two headers signal that there is a message body.
       */
      if (servletRequest.getHeader(HttpHeaders.CONTENT_LENGTH) != null ||
               servletRequest.getHeader(HttpHeaders.TRANSFER_ENCODING) != null)
      {
         HttpEntityEnclosingRequest eProxyRequest = new BasicHttpEntityEnclosingRequest(method, targetUri);
         /*
          * Add the input entity (streamed) note: we don't bother ensuring we close the servletInputStream since the
          * container handles it
          */
         eProxyRequest.setEntity(new InputStreamEntity(servletRequest.getInputStream(), servletRequest
                  .getContentLength()));
         proxyRequest = eProxyRequest;
      }
      else
         proxyRequest = new BasicHttpRequest(method, targetUri);

      copyRequestHeaders(servletRequest, proxyRequest);

      try
      {
         /*
          * Execute the request
          */
         if (doLog)
         {
            logger.debug("proxy " + method + " uri: " + servletRequest.getRequestURI() + " -- "
                     + proxyRequest.getRequestLine().getUri());
         }
         HttpResponse proxyResponse = proxyClient.execute(URIUtils.extractHost(targetUriObj), proxyRequest);

         /*
          * Process the response
          */
         int statusCode = proxyResponse.getStatusLine().getStatusCode();

         if (doResponseRedirectOrNotModifiedLogic(servletRequest, servletResponse, proxyResponse, statusCode))
         {
            /*
             * just to be sure, but is probably a no-op
             */
            EntityUtils.consume(proxyResponse.getEntity());
            return;
         }

         /*
          * Pass the response code. This method with the "reason phrase" is deprecated but it's the only way to pass the
          * reason along too. noinspection deprecation
          */
         servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase());

         copyResponseHeaders(proxyResponse, servletResponse);

         /*
          * Send the content to the client
          */
         copyResponseEntity(proxyResponse, servletResponse);

      }
      catch (Exception e)
      {
         /*
          * abort request, according to best practice with HttpClient
          */
         if (proxyRequest instanceof AbortableHttpRequest)
         {
            AbortableHttpRequest abortableHttpRequest = (AbortableHttpRequest) proxyRequest;
            abortableHttpRequest.abort();
         }
         if (e instanceof RuntimeException)
            throw (RuntimeException) e;
         if (e instanceof ServletException)
            throw (ServletException) e;
         // noinspection ConstantConditions
         if (e instanceof IOException)
            throw (IOException) e;
         throw new RuntimeException(e);
      }
   }

   protected boolean doResponseRedirectOrNotModifiedLogic(
            HttpServletRequest servletRequest, HttpServletResponse servletResponse,
            HttpResponse proxyResponse, int statusCode)
            throws ServletException, IOException
   {
      /*
       * Check if the proxy response is a redirect. The following code is adapted from
       * org.tigris.noodle.filters.CheckForRedirect
       */
      if (statusCode >= HttpServletResponse.SC_MULTIPLE_CHOICES /* 300 */
               && statusCode < HttpServletResponse.SC_NOT_MODIFIED /* 304 */)
      {
         Header locationHeader = proxyResponse.getLastHeader(HttpHeaders.LOCATION);
         if (locationHeader == null)
         {
            throw new ServletException("Received status code: " + statusCode
                     + " but no " + HttpHeaders.LOCATION + " header was found in the response");
         }
         /*
          * Modify the redirect to go to this proxy servlet rather that the proxied host
          */
         String locStr = rewriteUrlFromResponse(servletRequest, locationHeader.getValue());

         servletResponse.sendRedirect(locStr);
         return true;
      }
      /*
       * 304 needs special handling. See: http://www.ics.uci.edu/pub/ietf/http/rfc1945.html#Code304 . We get a 304
       * whenever passed an 'If-Modified-Since' header and the data on disk has not changed; server responds w/ a 304
       * saying I'm not going to send the body because the file has not changed.
       */
      if (statusCode == HttpServletResponse.SC_NOT_MODIFIED)
      {
         servletResponse.setIntHeader(HttpHeaders.CONTENT_LENGTH, 0);
         servletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
         return true;
      }
      return false;
   }

   protected void closeQuietly(Closeable closeable)
   {
      try
      {
         closeable.close();
      }
      catch (IOException e)
      {
         logger.warn(e.getMessage(), e);
      }
   }

   /**
    * These are the "hop-by-hop" headers that should not be copied.
    * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html I use an HttpClient HeaderGroup class instead of
    * Set<String> because this approach does case insensitive lookup faster.
    */
   protected static final HeaderGroup hopByHopHeaders;
   static
   {
      hopByHopHeaders = new HeaderGroup();
      String[] headers = new String[] {
               "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization",
               "TE", "Trailers", "Transfer-Encoding", "Upgrade" };
      for (String header : headers)
      {
         hopByHopHeaders.addHeader(new BasicHeader(header, null));
      }
   }

   /** Copy request headers from the servlet client to the proxy request. */
   protected void copyRequestHeaders(HttpServletRequest servletRequest, HttpRequest proxyRequest)
   {
      /*
       * Get an Enumeration of all of the header names sent by the client
       */
      Enumeration<String> enumerationOfHeaderNames = servletRequest.getHeaderNames();
      while (enumerationOfHeaderNames.hasMoreElements())
      {
         String headerName = enumerationOfHeaderNames.nextElement();
         // Instead the content-length is effectively set via InputStreamEntity
         if (headerName.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH))
            continue;
         if (hopByHopHeaders.containsHeader(headerName))
            continue;

         Enumeration<String> headers = servletRequest.getHeaders(headerName);
         while (headers.hasMoreElements())
         {
            /*
             * sometimes more than one value
             */
            String headerValue = headers.nextElement();
            /*
             * In case the proxy host is running multiple virtual servers, rewrite the Host header to ensure that we get
             * content from the correct virtual server
             */
            if (headerName.equalsIgnoreCase(HttpHeaders.HOST))
            {
               HttpHost host = URIUtils.extractHost(this.targetUriObj);
               headerValue = host.getHostName();
               if (host.getPort() != -1)
                  headerValue += ":" + host.getPort();
            }
            proxyRequest.addHeader(headerName, headerValue);
         }
      }
   }

   /**
    * Copy proxied response headers back to the servlet client.
    */
   protected void copyResponseHeaders(HttpResponse proxyResponse, HttpServletResponse servletResponse)
   {
      for (Header header : proxyResponse.getAllHeaders())
      {
         if (hopByHopHeaders.containsHeader(header.getName()))
            continue;
         servletResponse.addHeader(header.getName(), header.getValue());
      }
   }

   /**
    * Copy response body data (the entity) from the proxy to the servlet client.
    */
   protected void copyResponseEntity(HttpResponse proxyResponse, HttpServletResponse servletResponse)
            throws IOException
   {
      HttpEntity entity = proxyResponse.getEntity();
      if (entity != null)
      {
         OutputStream servletOutputStream = servletResponse.getOutputStream();
         try
         {
            entity.writeTo(servletOutputStream);
         }
         finally
         {
            closeQuietly(servletOutputStream);
         }
      }
   }

   /**
    * For a redirect response from the target server, this translates {@code theUrl} to redirect to and translates it to
    * one the original client can use.
    */
   protected String rewriteUrlFromResponse(HttpServletRequest servletRequest, String theUrl)
   {
      /*
       * TODO document example paths
       */
      if (theUrl.startsWith(targetUri))
      {
         String curUrl = servletRequest.getRequestURL().toString();// no query
         String pathInfo = servletRequest.getPathInfo();
         if (pathInfo != null)
         {
            assert curUrl.endsWith(pathInfo);
            curUrl = curUrl.substring(0, curUrl.length() - pathInfo.length());// take pathInfo off
         }
         theUrl = curUrl + theUrl.substring(targetUri.length());
      }
      return theUrl;
   }

   /**
    * Encodes characters in the query or fragment part of the URI.
    *
    *

    * Unfortunately, an incoming URI sometimes has characters disallowed by the spec. HttpClient insists that the
    * outgoing proxied request has a valid URI because it uses Java's {@link URI}. To be more forgiving, we must escape
    * the problematic characters. See the URI class for the spec.
    *
    * @param in example: name=value&foo=bar#fragment
    */
   protected static CharSequence encodeUriQuery(CharSequence in)
   {
      /*
       * Note that I can't simply use URI.java to encode because it will escape pre-existing escaped things. TODO:
       * replace/compare to with Rewrite Encoding
       */
      StringBuilder outBuf = null;
      Formatter formatter = null;
      for (int i = 0; i < in.length(); i++)
      {
         char c = in.charAt(i);
         boolean escape = true;
         if (c < 128)
         {
            if (asciiQueryChars.get(c))
            {
               escape = false;
            }
         }
         else if (!Character.isISOControl(c) && !Character.isSpaceChar(c))
         {
            /*
             * not-ascii
             */
            escape = false;
         }
         if (!escape)
         {
            if (outBuf != null)
               outBuf.append(c);
         }
         else
         {
            /*
             * escape
             */
            if (outBuf == null)
            {
               outBuf = new StringBuilder(in.length() + 5 * 3);
               outBuf.append(in, 0, i);
               formatter = new Formatter(outBuf);
            }
            /*
             * leading %, 0 padded, width 2, capital hex
             */
            formatter.format("%%%02X", (int) c);// TODO
         }
      }
      return outBuf != null ? outBuf : in;
   }

   protected static final BitSet asciiQueryChars;
   static
   {
      char[] c_unreserved = "_-!.~'()*".toCharArray();// plus alphanum
      char[] c_punct = ",;:$&+=".toCharArray();
      char[] c_reserved = "?/[]@".toCharArray();// plus punct

      asciiQueryChars = new BitSet(128);
      for (char c = 'a'; c <= 'z'; c++)
         asciiQueryChars.set(c);
      for (char c = 'A'; c <= 'Z'; c++)
         asciiQueryChars.set(c);
      for (char c = '0'; c <= '9'; c++)
         asciiQueryChars.set(c);
      for (char c : c_unreserved)
         asciiQueryChars.set(c);
      for (char c : c_punct)
         asciiQueryChars.set(c);
      for (char c : c_reserved)
         asciiQueryChars.set(c);

      asciiQueryChars.set('%');
      /*
       * leave existing percent escapes in place
       */
   }

}


/*
 * Copyright 2011 Lincoln Baxter, III
 *
 * 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.ocpsoft.rewrite.cdi;

import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;

import org.ocpsoft.rewrite.cdi.expressions.Expressions;
import org.ocpsoft.rewrite.el.spi.ExpressionLanguageProvider;

/**
 * @author Lincoln Baxter, III
 */
public class CdiExpressionLanguageProvider implements ExpressionLanguageProvider
{
   /**
    * We cannot simply inject an instance of Expressions here, because {@link CdiExpressionLanguageProvider} is created
    * only once for the entire application and {@link Expressions} is not thread-safe because it holds only a single
    * {@link ELContext} instance.
    */
   @Inject
   private Instance<Expressions> expressionsInstance;

   @Override
   public int priority()
   {
      return 10;
   }

   @Override
   public Object retrieveValue(final String expression)
   {
      Expressions expressions = expressionsInstance.get();
      return getValue(expressions.getELContext(), expressions.getExpressionFactory(),
               groomExpression(expression));
   }

   @Override
   public void submitValue(final String expression, final Object value)
   {
      Expressions expressions = expressionsInstance.get();
      String el = groomExpression(expression);
      if (getExpectedType(expressions.getELContext(), expressions.getExpressionFactory(), el).isArray())
      {
         Object[] toInject = null;
         if ((value != null) && !value.getClass().isArray())
         {
            toInject = new Object[] { value };
         }
         else
            toInject = (Object[]) value;

         setValue(expressions.getELContext(), expressions.getExpressionFactory(), el, toInject);
      }
      else
      {
         Object toInject = value;
         if ((value != null) && value.getClass().isArray())
         {
            Object[] array = (Object[]) value;
            if (array.length > 0)
            {
               toInject = array[0];
            }
            else
            {
               toInject = "";
            }
         }
         setValue(expressions.getELContext(), expressions.getExpressionFactory(), el, toInject);
      }
   }

   public String groomExpression(String expression)
   {
      String result = expression.trim();
      if (!result.startsWith("#{"))
         return expressionsInstance.get().toExpression(result);

      return result;
   }

   @Override
   public Object evaluateMethodExpression(final String expression)
   {
      String el = groomExpression(expression);
      return expressionsInstance.get().evaluateMethodExpression(el);
   }

   @Override
   public Object evaluateMethodExpression(final String expression, final Object... values)
   {
      String el = groomExpression(expression);
      return expressionsInstance.get().evaluateMethodExpression(el, values);
   }

   /**
    * Helpers
    */
   private Class<?> getExpectedType(final ELContext context, final ExpressionFactory factory, final String expression)
            throws ELException
   {
      ValueExpression ve = factory.createValueExpression(context, expression, Object.class);
      return ve.getType(context);
   }

   private Object getValue(final ELContext context, final ExpressionFactory factory, final String expression)
            throws ELException
   {
      ValueExpression ve = factory.createValueExpression(context, expression, Object.class);
      return ve.getValue(context);
   }

   private void setValue(final ELContext context, final ExpressionFactory factory, final String expression,
            final Object value) throws ELException
   {

      ValueExpression ve = factory.createValueExpression(context, expression, Object.class);
      ve.setValue(context, factory.coerceToType(value, ve.getType(context)));
   }

}


/*
 * Copyright 2010 Lincoln Baxter, III
 *
 * 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.ocpsoft.rewrite.servlet.util;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.ocpsoft.common.util.Strings;
import org.ocpsoft.urlbuilder.AddressBuilder;
import org.ocpsoft.urlbuilder.util.Decoder;
import org.ocpsoft.urlbuilder.util.Encoder;

/**
 * Utility for building URL strings. Also manages the URL query string with the help of {@link QueryStringBuilder}.
 *
 * @deprecated Use {@link AddressBuilder} instead. May be removed in subsequent releases.
 * @author Lincoln Baxter, III
 *
 */
@Deprecated
public class URLBuilder
{
   private Metadata metadata = new Metadata();

   private final List<String> segments = new ArrayList<String>();

   private QueryStringBuilder query = QueryStringBuilder.createNew();

   /**
    * Return a new instance of {@link URLBuilder}. Until modified. This URL will be blank.
    */
   public static URLBuilder createNew()
   {
      return new URLBuilder();
   }

   /**
    * Create a new instance of {@link URLBuilder} from the given URL segments, {@link Metadata}, and
    * {@link QueryStringBuilder}.
    */
   public static URLBuilder createFrom(final List<String> segments, final Metadata metadata,
            final QueryStringBuilder query)
   {
      return URLBuilder.createNew().appendPathSegments(segments).setMetadata(metadata).setQueryString(query);
   }

   /**
    * Create a new instance of {@link URLBuilder} from the given URL path segments and {@link Metadata}.
    */
   public static URLBuilder createFrom(final List<String> segments, final Metadata metadata)
   {
      return URLBuilder.createNew().appendPathSegments(segments).setMetadata(metadata);
   }

   /**
    * Create a new instance of {@link URLBuilder} from the given URL path segments.
    */
   public static URLBuilder createFrom(final String segments)
   {
      if (segments == null)
      {
         throw new IllegalArgumentException("URL cannot be null.");
      }
      if (segments.contains("?"))
      {
         String[] parts = segments.split("\\?", -1);
         String path = parts[0];
         String query = parts[1];
         if (parts.length > 2)
         {
            query = Strings.join(Arrays.asList(Arrays.copyOfRange(parts, 1, parts.length)), "?");
         }
         return new URLBuilder().appendPathSegments(path).addQueryParameters(query);
      }
      return new URLBuilder().appendPathSegments(segments);
   }

   /*
    * Constructors
    */
   private URLBuilder()
   {}

   private URLBuilder(final List<String> segments, final Metadata metadata, final QueryStringBuilder query)
   {
      this.segments.addAll(segments);
      this.metadata = metadata.copy();
      this.query = query;
   }

   /*
    * End Constructors
    */

   /**
    * Append additional path segments to the end of this URL. When called the first time, this method will also
    * initialize the {@link Metadata} for this {@link URLBuilder} based on the parsed segments given.
    */
   public URLBuilder appendPathSegments(final List<String> segments)
   {
      this.segments.addAll(segments);
      return this;
   }

   /**
    * Append additional path segments to the end of this URL. When called the first time, this method will also
    * initialize the {@link Metadata} for this {@link URLBuilder} based on the parsed segments given.
    */
   public URLBuilder appendPathSegments(final String segments)
   {
      if (segments != null)
      {
         String temp = segments.trim();

         // Only initialize the leading slash when adding the first path segments
         if (temp.startsWith("/") && this.segments.isEmpty())
         {
            metadata.setLeadingSlash(true);
         }
         if (temp.endsWith("/"))
         {
            metadata.setTrailingSlash(true);
         }

         String trimmedUrl = trimSurroundingSlashes(segments);

         // We reproduce this when building the URL by storing a single empty segment
         if (!trimmedUrl.isEmpty() || "//".equals(segments))
         {
            String[] newSegments = trimmedUrl.split("/", -1);
            this.segments.addAll(Arrays.asList(newSegments));
         }
      }
      else
      {
         throw new IllegalArgumentException("URL cannot be null.");
      }
      return this;
   }

   /**
    * Parse and add more query parameters to this {@link URLBuilder}
    */
   public URLBuilder addQueryParameters(final String parameters)
   {
      this.query.addParameters(parameters);
      return this;
   }

   /**
    * Return this {@link URLBuilder} after path segments and query parameters have been decoded.
    */
   public URLBuilder decode()
   {
      return new URLBuilder(getDecodedSegments(), metadata, query.decode());
   }

   /**
    * Return this {@link URLBuilder} after path segments and query parameters have been encoded.
    */
   public URLBuilder encode()
   {
      return new URLBuilder(getEncodedSegments(), metadata, query.encode());
   }

   private List<String> getDecodedSegments()
   {
      /*
       * Normal loop for performance reasons
       */
      List<String> result = new ArrayList<String>();
      for (int i = 0; i < segments.size(); i++) {
         result.add(Decoder.path(segments.get(i)));
      }
      return result;
   }

   private List<String> getEncodedSegments()
   {
      /*
       * Normal loop for performance reasons
       */
      List<String> result = new ArrayList<String>();
      for (int i = 0; i < segments.size(); i++) {
         result.add(Encoder.path(segments.get(i)));
      }
      return result;
   }

   /**
    * Get the character encoding of this URL (default UTF-8)
    */
   public String getEncoding()
   {
      return metadata.getEncoding();
   }

   /*
    * Getters & Setters
    */

   /**
    * Get the {@link Metadata} object for this URL
    */
   public Metadata getMetadata()
   {
      return metadata;
   }

   /**
    * Get the {@link QueryStringBuilder} object for this URL
    */
   public QueryStringBuilder getQueryStringBuilder()
   {
      return query;
   }

   /**
    * Set the {@link QueryStringBuilder} object for this URL
    */
   public URLBuilder setQueryString(final QueryStringBuilder query)
   {
      this.query = query;
      return this;
   }

   /**
    * Return all segments (separated by '/') in this URL
    */
   public List<String> getSegments()
   {
      return Collections.unmodifiableList(segments);
   }

   /**
    * Return true if this URL begins with '/'
    */
   public boolean hasLeadingSlash()
   {
      return metadata.hasLeadingSlash();
   }

   /**
    * Return true if this URL ends with '/'
    */
   public boolean hasTrailingSlash()
   {
      return metadata.hasTrailingSlash();
   }

   /**
    * Get the number of segments (separated by '/') in this URL
    */
   public int numSegments()
   {
      return segments.size();
   }

   /**
    * Set the character encoding of this URL (default UTF-8)
    */
   public void setEncoding(final String encoding)
   {
      metadata.setEncoding(encoding);
   }

   /**
    * Set the {@link Metadata} object for this URL
    */
   public URLBuilder setMetadata(final Metadata metadata)
   {
      this.metadata = metadata;
      return this;
   }

   /**
    * Return the portion of this URL representing the path.
    */
   public String toPath()
   {
      return metadata.buildURLFromSegments(segments);
   }

   /**
    * Return a String representation of this URL
    */
   @Override
   public String toString()
   {
      return toURL();
   }

   /**
    * Return a URI representation of this URL including path and query string
    */
   public URI toURI()
   {
      try {
         URI uri = new URI(toURL());
         return uri;
      }
      catch (URISyntaxException e) {
         throw new IllegalStateException("URL cannot be parsed.", e);
      }
   }

   /**
    * Return a String representation of this URL including path and query string
    */
   public String toURL()
   {
      return toPath() + query.toQueryString();
   }

   /*
    * Helpers
    */
   private String trimSurroundingSlashes(final String url)
   {
      String result = null;
      if (url != null)
      {
         result = url.trim();
         if (result.startsWith("/"))
         {
            result = result.substring(1);
         }
         if (result.endsWith("/"))
         {
            result = result.substring(0, result.length() - 1);
         }
      }
      return result;
   }
}


/*
 * Copyright 2011 Lincoln Baxter, III
 *
 * 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.ocpsoft.rewrite.faces.config;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.faces.event.PhaseId;
import javax.servlet.http.HttpServletRequest;

import org.ocpsoft.common.pattern.Weighted;
import org.ocpsoft.common.pattern.WeightedComparator;
import org.ocpsoft.rewrite.config.Operation;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.servlet.config.HttpOperation;
import org.ocpsoft.rewrite.servlet.http.event.HttpServletRewrite;

/**
 * An {@link Operation} that wraps another operation to be performed during the JavaServer Faces lifecycle.
 *
 * @author Fabien Marsaud
 * @author Lincoln Baxter, III
 */
public abstract class PhaseOperation<T extends PhaseOperation<T>> extends HttpOperation implements Weighted
{
   private static final String REQUEST_KEY = PhaseOperation.class.getName() + "_DEFERRED";

   private final Set<PhaseId> beforePhases = new HashSet<PhaseId>();
   private final Set<PhaseId> afterPhases = new HashSet<PhaseId>();

   /**
    * Perform the wrapped operation before or after the specified phases.
    */
   public abstract void performOperation(final HttpServletRewrite event, final EvaluationContext context);

   /**
    * Create an {@link Operation} that will defer a given {@link Operation} until one or more specified JavaServer Faces
    * {@link PhaseId} instances specified via {@link #before(PhaseId...)} or {@link #after(PhaseId...)}.
    */
   public static PhaseOperation<?> enqueue(final Operation operation)
   {
      return enqueue(operation, 0);
   }

   /**
    * Create an {@link Operation} that will enqueue a given {@link Operation} to be performed before or after one or
    * more JavaServer Faces {@link PhaseId} instances specified via {@link #before(PhaseId...)} or
    * {@link #after(PhaseId...)}.
    */
   @SuppressWarnings("rawtypes")
   public static PhaseOperation<?> enqueue(final Operation operation, final int priority)
   {
      return new PhaseOperation() {

         @Override
         public int priority()
         {
            return priority;
         }

         @Override
         public void performOperation(HttpServletRewrite event, EvaluationContext context)
         {
            operation.perform(event, context);
         }

         @Override
         public String toString()
         {
            return "PhaseOperation.enqueue(" + operation + ", " + priority + ")";
         }
      };
   }

   /**
    * Get the phases before which this {@link PhaseOperation} will be performed.
    */
   public Set<PhaseId> getBeforePhases()
   {
      return beforePhases;
   }

   /**
    * Get the phases after which this {@link PhaseOperation} will be performed.
    */
   public Set<PhaseId> getAfterPhases()
   {
      return afterPhases;
   }

   /**
    * Perform this {@link PhaseOperation} before the given phases (Except {@link PhaseId#RESTORE_VIEW}). The deferred
    * {@link Operation} will be performed once for each {@link PhaseId} provided.
    */
   @SuppressWarnings("unchecked")
   public T before(final PhaseId... phases)
   {
      if (phases != null)
         this.beforePhases.addAll(Arrays.asList(phases));
      return (T) this;
   }

   /**
    * Perform this {@link PhaseOperation} after the given phases (Except {@link PhaseId#RENDER_RESPONSE}). The deferred
    * {@link Operation} will be performed once for each {@link PhaseId} provided.
    */
   @SuppressWarnings("unchecked")
   public T after(final PhaseId... phases)
   {
      if (phases != null)
         this.afterPhases.addAll(Arrays.asList(phases));
      return (T) this;
   }

   @Override
   public final void performHttp(HttpServletRewrite event, EvaluationContext context)
   {
      getSortedPhaseOperations(event.getRequest()).add(new DeferredOperation(event, context, this));
   }

   @SuppressWarnings("unchecked")
   public static ArrayList<DeferredOperation> getSortedPhaseOperations(HttpServletRequest request)
   {
      ArrayList<DeferredOperation> operations = (ArrayList<DeferredOperation>) request.getAttribute(REQUEST_KEY);
      if (operations == null)
      {
         operations = new ArrayList<DeferredOperation>();
         request.setAttribute(REQUEST_KEY, operations);
      }

      Collections.sort(operations, new WeightedComparator());

      return operations;
   }

   public static class DeferredOperation implements Weighted
   {

      private final HttpServletRewrite event;
      private final EvaluationContext context;
      private final PhaseOperation<?> operation;

      public DeferredOperation(HttpServletRewrite event, EvaluationContext context, PhaseOperation<?> operation)
      {
         this.event = event;
         this.context = context;
         this.operation = operation;
      }

      @Override
      public int priority()
      {
         return operation.priority();
      }

      public HttpServletRewrite getEvent()
      {
         return event;
      }

      public EvaluationContext getContext()
      {
         return context;
      }

      public PhaseOperation<?> getOperation()
      {
         return operation;
      }

   }

}


/*
 * JBoss, Home of Professional Open Source
 * Copyright 2011, Red Hat, Inc. and/or its affiliates, and individual
 * contributors by the @authors tag. See the copyright.txt in the
 * distribution for a full listing of individual contributors.
 *
 * 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.ocpsoft.rewrite.cdi.expressions;

import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ExpressionFactory;
import javax.el.MethodExpression;
import javax.el.MethodNotFoundException;
import javax.el.PropertyNotFoundException;
import javax.el.ValueExpression;
import javax.inject.Inject;

import org.ocpsoft.rewrite.cdi.util.Reflections;

/**
 *

 * Provides various utility methods for working with EL expressions.
 *

 *

 *

 * This utility can be used through injection:
 *

 *

 *
 *

 * @Inject
 * Expressions expressions;
 *

 *

 *

 * Alternatively, if you aren't working in a CDI environment, it can be instantiated using the new keyword:
 *

 *

 *
 *

 * Expressions expressions = new Expressions(context, expressionFactory);
 *

 *
 * @author Pete Muir
 * @author Dan Allen
 * @author Stuart Douglas
 */
public class Expressions
{

   private final ELContext context;
   private final ExpressionFactory expressionFactory;

   /**
    * Create a new instance of the {@link Expressions} class, providing the {@link ELContext} and
    * {@link ExpressionFactory} to be used.
    *
    * @param context the {@link ELContext} against which to operate
    * @param expressionFactory the {@link ExpressionFactory} to use
    * @throws IllegalArgumentException if context is null or expressionFactory is null
    */
   @Inject
   public Expressions(
            @Composite ELContext context, @Composite ExpressionFactory expressionFactory)
   {
      if (context == null) {
         throw new IllegalArgumentException("context must not be null");
      }
      if (expressionFactory == null) {
         throw new IllegalArgumentException("expressionFactory must not be null");
      }
      this.context = context;
      this.expressionFactory = expressionFactory;
   }

   /**
    * Obtain the {@link ELContext} that this instance of {@link Expressions} is using.
    *
    * @return the {@link ELContext} in use
    */
   public ELContext getELContext()
   {
      return context;
   }

   /**
    * Obtain the {@link ExpressionFactory} that this instance of {@link Expressions} is using.
    *
    * @return the {@link ExpressionFactory} in use
    */
   public ExpressionFactory getExpressionFactory()
   {
      return expressionFactory;
   }

   /**
    *

    * Evaluate a {@link ValueExpression}.
    *

    *

    *

    * A {@link ValueExpression} is created by calling
    * {@link ExpressionFactory#createValueExpression(ELContext, String, Class)} and then
    * {@link ValueExpression#getValue(ELContext)} is called to obtain the value. For more details on the semantics of
    * EL, refer to the javadoc for these classes and methods.
    *

    *
    * @param <T> the type of the evaluated expression
    * @param expression the expression to evaluate
    * @param expectedType the expected type of the evaluated expression
    * @return the result of evaluating the expression
    * @throws NullPointerException if expectedType is null
    * @throws ELException if there are syntactical errors in the provided expression or if an exception was thrown while
    *            performing property or variable resolution. The thrown exception will be included as the cause property
    *            of this exception, if available.
    * @throws PropertyNotFoundException if one of the property resolutions failed because a specified variable or
    *            property does not exist or is not readable.
    * @throws ClassCastException if the result cannot be cast to the expected type
    * @see ExpressionFactory#createValueExpression(ELContext, String, Class)
    * @see ValueExpression#getValue(ELContext)
    */
   public <T> T evaluateValueExpression(String expression, Class<T> expectedType)
   {
      Object result = expressionFactory.createValueExpression(context, expression, expectedType).getValue(context);
      if (result != null) {
         return expectedType.cast(result);
      }
      else {
         return null;
      }
   }

   /**
    *

    * Evaluate a {@link ValueExpression} inferring the return type.
    *

    *

    *

    * A {@link ValueExpression} is created by calling
    * {@link ExpressionFactory#createValueExpression(ELContext, String, Class)} and then
    * {@link ValueExpression#getValue(ELContext)} is called to obtain the value. For more details on the semantics of
    * EL, refer to the javadoc for these methods.
    *

    *
    * @param <T> the type of the evaluated expression
    * @param expression expression the expression to evaluate
    * @return the result of evaluating the expression
    * @throws ELException if there are syntactical errors in the provided expression
    * @throws ELException if an exception was thrown while performing property or variable resolution. The thrown
    *            exception will be included as the cause property of this exception, if available.
    * @throws PropertyNotFoundException if one of the property resolutions failed because a specified variable or
    *            property does not exist or is not readable
    * @throws ClassCastException if the result cannot be cast to T
    * @see ExpressionFactory#createValueExpression(ELContext, String, Class)
    * @see ValueExpression#getValue(ELContext)
    */
   public <T> T evaluateValueExpression(String expression)
   {
      Object result = evaluateValueExpression(expression, Object.class);
      if (result != null) {
         return Reflections.<T> cast(result);
      }
      else {
         return null;
      }
   }

   /**
    *

    * Evaluate a {@link MethodExpression}, passing arguments and argument types to the method.
    *

    *

    *

    * A {@link MethodExpression} is created by calling
    * {@link ExpressionFactory#createMethodExpression(ELContext, String, Class, Class[])} and then
    * {@link MethodExpression#invoke(ELContext, Object[])} is called to obtain the value. For more details on the
    * semantics of EL, refer to the javadoc for these methods.
    *

    *
    * @param <T> the type of the evaluated expression
    * @param expression expression the expression to evaluate
    * @param expectedReturnType the expected return type of the evaluated expression
    * @param params arguments to the method
    * @param expectedParamTypes of the arguments to the method
    * @return the result of evaluating the expression
    * @throws ClassCastException if the result cannot be cast to expectedReturnType
    * @throws ELException if there are syntactical errors in the provided expression.
    * @throws NullPointerException if expectedParamTypes is null.
    * @throws PropertyNotFoundException if one of the property resolutions failed because a specified variable or
    *            property does not exist or is not readable.
    * @throws MethodNotFoundException if no suitable method can be found.
    * @throws ELException if a String literal is specified and expectedReturnType of the MethodExpression is void or if
    *            the coercion of the String literal to the expectedReturnType yields an error (see Section
    *            "1.18 Type Conversion").
    * @throws ELException if an exception was thrown while performing property or variable resolution. The thrown
    *            exception must be included as the cause property of this exception, if available. If the exception
    *            thrown is an InvocationTargetException, extract its cause and pass it to the
    *            ELException constructor.
    * @see MethodExpression#invoke(ELContext, Object[])
    * @see ExpressionFactory#createMethodExpression(ELContext, String, Class, Class[])
    */
   public <T> T evaluateMethodExpression(String expression, Class<T> expectedReturnType, Object[] params,
            Class<?>[] expectedParamTypes)
   {
      Object result = expressionFactory.createMethodExpression(context, expression, expectedReturnType,
               expectedParamTypes).invoke(context, params);
      if (result != null) {
         return expectedReturnType.cast(result);
      }
      else {
         return null;
      }
   }

   /**
    *

    * Evaluate a {@link MethodExpression} with no parameters.
    *

    *

    *

    * A {@link MethodExpression} is created by calling
    * {@link ExpressionFactory#createMethodExpression(ELContext, String, Class, Class[])} and then
    * {@link MethodExpression#invoke(ELContext, Object[])} is called to obtain the value. For more details on the
    * semantics of EL, refer to the javadoc for these methods.
    *

    *
    * @param <T> the type of the evaluated expression
    * @param expression expression the expression to evaluate
    * @param expectedReturnType the expected return type of the evaluated expression
    * @return the result of evaluating the expression
    * @throws ClassCastException if the result cannot be cast to expectedReturnType
    * @throws ELException if there are syntactical errors in the provided expression.
    * @throws NullPointerException if expectedParamTypes is null.
    * @throws PropertyNotFoundException if one of the property resolutions failed because a specified variable or
    *            property does not exist or is not readable.
    * @throws MethodNotFoundException if no suitable method can be found.
    * @throws ELException if a String literal is specified and expectedReturnType of the MethodExpression is void or if
    *            the coercion of the String literal to the expectedReturnType yields an error (see Section
    *            "1.18 Type Conversion").
    * @throws ELException if an exception was thrown while performing property or variable resolution. The thrown
    *            exception must be included as the cause property of this exception, if available. If the exception
    *            thrown is an InvocationTargetException, extract its cause and pass it to the
    *            ELException constructor.
    * @see MethodExpression#invoke(ELContext, Object[])
    * @see ExpressionFactory#createMethodExpression(ELContext, String, Class, Class[])
    */
   public <T> T evaluateMethodExpression(String expression, Class<T> expectedReturnType)
   {
      return evaluateMethodExpression(expression, expectedReturnType, new Object[0], new Class[0]);
   }

   /**
    *

    * Evaluate a {@link MethodExpression} with no parameters, inferring the return type.
    *

    *

    *

    * A {@link MethodExpression} is created by calling
    * {@link ExpressionFactory#createMethodExpression(ELContext, String, Class, Class[])} and then
    * {@link MethodExpression#invoke(ELContext, Object[])} is called to obtain the value. For more details on the
    * semantics of EL, refer to the javadoc for these methods.
    *

    *
    * @param <T> the type of the evaluated expression
    * @param expression expression the expression to evaluate
    * @return the result of evaluating the expression
    * @throws ClassCastException if the result cannot be cast to T
    * @throws ELException if there are syntactical errors in the provided expression.
    * @throws NullPointerException if expectedParamTypes is null.
    * @throws PropertyNotFoundException if one of the property resolutions failed because a specified variable or
    *            property does not exist or is not readable.
    * @throws MethodNotFoundException if no suitable method can be found.
    * @throws ELException if a String literal is specified and expectedReturnType of the MethodExpression is void or if
    *            the coercion of the String literal to the expectedReturnType yields an error (see Section
    *            "1.18 Type Conversion").
    * @throws ELException if an exception was thrown while performing property or variable resolution. The thrown
    *            exception must be included as the cause property of this exception, if available. If the exception
    *            thrown is an InvocationTargetException, extract its cause and pass it to the
    *            ELException constructor.
    * @see MethodExpression#invoke(ELContext, Object[])
    * @see ExpressionFactory#createMethodExpression(ELContext, String, Class, Class[])
    */
   public <T> T evaluateMethodExpression(String expression)
   {
      Object result = evaluateMethodExpression(expression, Object.class);
      if (result != null) {
         return Reflections.<T> cast(result);
      }
      else {
         return null;
      }
   }

   /**
    *

    * Evaluate a {@link MethodExpression}, passing arguments to the method. The types of the arguments are discoverted
    * from the arguments, and the return type is inferred.
    *

    *

    *

    * A {@link MethodExpression} is created by calling
    * {@link ExpressionFactory#createMethodExpression(ELContext, String, Class, Class[])} and then
    * {@link MethodExpression#invoke(ELContext, Object[])} is called to obtain the value. For more details on the
    * semantics of EL, refer to the javadoc for these methods.
    *

    *
    * @param <T> the type of the evaluated expression
    * @param expression expression the expression to evaluate
    * @param params arguments to the method
    * @return the result of evaluating the expression
    * @throws ClassCastException if the result cannot be cast to T
    * @throws ELException if there are syntactical errors in the provided expression.
    * @throws NullPointerException if expectedParamTypes is null.
    * @throws PropertyNotFoundException if one of the property resolutions failed because a specified variable or
    *            property does not exist or is not readable.
    * @throws MethodNotFoundException if no suitable method can be found.
    * @throws ELException if a String literal is specified and expectedReturnType of the MethodExpression is void or if
    *            the coercion of the String literal to the expectedReturnType yields an error (see Section
    *            "1.18 Type Conversion").
    * @throws ELException if an exception was thrown while performing property or variable resolution. The thrown
    *            exception must be included as the cause property of this exception, if available. If the exception
    *            thrown is an InvocationTargetException, extract its cause and pass it to the
    *            ELException constructor.
    * @see MethodExpression#invoke(ELContext, Object[])
    * @see ExpressionFactory#createMethodExpression(ELContext, String, Class, Class[])
    */
   public <T> T evaluateMethodExpression(String expression, Object... params)
   {
      Object result = evaluateMethodExpression(expression, Object.class, params, new Class[params.length]);
      if (result != null) {
         return Reflections.<T> cast(result);
      }
      else {
         return null;
      }
   }

   /**
    * Convert's a bean name to an EL expression string.
    *
    * @param name the name of the bean to convert
    * @return the expression string
    */
   public String toExpression(String name)
   {
      return "#{" + name + "}";
   }
}


/*
 * Copyright 2010 Lincoln Baxter, III
 *
 * 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 com.ocpsoft.pretty.faces.url;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.ocpsoft.pretty.PrettyException;
import com.ocpsoft.pretty.faces.config.mapping.RequestParameter;

/**
 * @author Lincoln Baxter, III <lincoln@ocpsoft.com>
 */
public class QueryString
{

   private final static Log log = LogFactory.getLog(QueryString.class);

   private final Map<String, List<String>> parameters = new LinkedHashMap<String, List<String>>();

   /**
    * Build a query string from the given map of name=value pairs. For parameters with more than one value, each value
    * will be appended using the same name.
    */
   public static QueryString build(final Map<String, String[]> params)
   {
      QueryString queryString = new QueryString();
      queryString.addParameters(params);
      return queryString;
   }

   /**
    * Build a query string from the given list of {@link RequestParameter} objects.
    */
   public static <T extends RequestParameter> QueryString build(final List<T> params)
   {
      QueryString queryString = new QueryString();
      queryString.addParameters(params);
      return queryString;
   }

   /**
    * Build a query string from the given URL. If a '?' character is encountered in the URL, the any characters up to
    * and including the first '?' will be ignored.
    */
   public static QueryString build(final String url)
   {
      QueryString queryString = new QueryString();
      queryString.addParameters(url);
      return queryString;
   }

   /*
    * Utility Methods
    */
   /**
    * Return true if this query string currently contains no parameters.
    */
   public boolean isEmpty()
   {
      return parameters.isEmpty();
   }

   /*
    * Work methods
    */
   /**
    * Get the first value of given parameter name.
    *
    * @return The value of the parameter, null if the parameter does not exist, or "" if the parameter exists but has no
    *         values.
    */
   public String getParameter(final String name)
   {
      List<String> values = parameters.get(name);
      if (values == null)
      {
         return null;
      }

      if (values.size() == 0)
      {
         return "";
      }

      return values.get(0);
   }

   /**
    * Get the array of values for a given parameter name.
    *
    * @return The values of the parameter, null if the parameter does not exist.
    */
   public String[] getParameterValues(final String name)
   {
      List<String> values = parameters.get(name);
      if (values == null)
      {
         return null;
      }

      return values.toArray(new String[values.size()]);
   }

   /**
    * Get the list of parameter names currently stored in this query string..
    */
   public Enumeration<String> getParameterNames()
   {
      return Collections.enumeration(parameters.keySet());
   }

   /**
    * Get the name, values[] map representing this query string.
    */
   public Map<String, String[]> getParameterMap()
   {
      Map<String, String[]> map = new TreeMap<String, String[]>();
      for (Map.Entry<String, List<String>> entry : parameters.entrySet())
      {
         List<String> list = entry.getValue();
         String[] values;
         if (list == null)
         {
            values = null;
         }
         else
         {
            values = list.toArray(new String[list.size()]);
         }
         map.put(entry.getKey(), values);
      }
      return map;
   }

   /*
    * Add Parameters
    */

   /**
    * Add query parameters from the given list of {@link RequestParameter} objects. If a parameter already exists,
    * append new values to the existing list of values for that parameter.
    */
   public <T extends RequestParameter> void addParameters(final List<T> params)
   {
      for (RequestParameter rp : params)
      {
         String value = rp.getValue();
         String name = rp.getName();

         if (!parameters.containsKey(name))
         {
            ArrayList<String> values = new ArrayList<String>();
            if (value != null)
            {
               values.add(value);
            }
            parameters.put(name, values);
         }
         else
         {
            parameters.get(name).add(value);
         }
      }
   }

   /**
    * Add parameters from the given map of name=value pairs. For parameters with more than one value, each value will be
    * appended using the same name. If a parameter already exists, append new values to the existing list of values for
    * that parameter.
    */
   public void addParameters(final Map<String, String[]> params)
   {
      if (params != null)
      {
         for (Entry<String, String[]> entry : params.entrySet())
         {
            List<String> values = null;
            if (entry.getValue() != null)
            {
               values = Arrays.asList(entry.getValue());
            }
            parameters.put(entry.getKey(), values);
         }
      }
   }

   /**
    * Add parameters from the given URL. If a '?' character is encountered in the URL, the any characters up to and
    * including the first '?' will be ignored. If a parameter already exists, append new values to the existing list of
    * values for that parameter.
    */
   public void addParameters(String url)
   {
      if ((url != null) && !"".equals(url))
      {
         url = url.trim();
         if (url.length() > 1)
         {
            if (url.contains("?"))
            {
               url = url.substring(url.indexOf('?') + 1);
            }

            String pairs[] = url.split("&(amp;)?");
            for (String pair : pairs)
            {
               String name;
               String value;
               int pos = pair.indexOf('=');
               // for "n=", the value is "", for "n", the value is null
               if (pos == -1)
               {
                  name = pair;
                  value = null;
               }
               else
               {
                  try
                  {
                     // FIXME This probably shouldn't be happening here.
                     name = URLDecoder.decode(pair.substring(0, pos), "UTF-8");
                     value = URLDecoder.decode(pair.substring(pos + 1, pair.length()), "UTF-8");
                  }
                  catch (IllegalArgumentException e)
                  {
                     // thrown by URLDecoder if character decoding fails
                     log.warn("Ignoring invalid query parameter: " + pair);
                     continue;
                  }
                  catch (UnsupportedEncodingException e)
                  {
                     throw new PrettyException(
                              "UTF-8 encoding not supported. Something is seriously wrong with your environment.");
                  }
               }
               List<String> list = parameters.get(name);
               if (list == null)
               {
                  list = new ArrayList<String>();
                  parameters.put(name, list);
               }
               list.add(value);
            }
         }
      }
      return;
   }

   /**
    * Convert the current parameters to a valid query string, including the leading '?' character. This method
    * automatically URLEncodes query-parameter values
    *

    *
    * For example, a {@link QueryString} with the values [key=>value, name=>value1,value2,value3] will become:
    *
    *

    *
    * ?key=value&name=value1&name=value2&name=value3
    *

    *
    * @return If parameters exist, return a valid query string with leading '?' character. If no parameters exist,
    *         return an empty string.
    */
   public String toQueryString()
   {
      try
      {
         StringBuffer result = new StringBuffer();

         if ((null != parameters) && !parameters.isEmpty())
         {
            result.append("?");
            Iterator<Entry<String, List<String>>> iterator = parameters.entrySet().iterator();
            while (iterator.hasNext())
            {
               Entry<String, List<String>> entry = iterator.next();
               String key = entry.getKey();
               List<String> values = entry.getValue();

               if ((key != null) && !"".equals(key))
               {
                  key = URLEncoder.encode(key, "UTF-8");
                  result.append(key);

                  if ((values != null) && !values.isEmpty())
                  {
                     for (int i = 0; i < values.size(); i++)
                     {
                        String value = values.get(i);
                        if ((value != null) && !"".equals(value))
                        {
                           value = URLEncoder.encode(value, "UTF-8");
                           result.append("=" + value);
                        }
                        else if ((value != null) && "".equals(value))
                        {
                           result.append("=");
                        }

                        if (i < values.size() - 1)
                        {
                           result.append("&" + key);
                        }
                     }
                  }
               }

               if (iterator.hasNext())
               {
                  result.append("&");
               }
            }
         }

         return result.toString();
      }
      catch (UnsupportedEncodingException e)
      {
         throw new PrettyException("Error building query string.", e);
      }
   }

   @Override
   public String toString()
   {
      return toQueryString();
   }

   public List<String> removeParameter(final String string)
   {
      return parameters.remove(string);
   }
}


/*
 * Copyright 2011 Lincoln Baxter, III
 *
 * 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.ocpsoft.rewrite.servlet.config;

import javax.servlet.http.HttpServletResponse;

import org.ocpsoft.rewrite.config.Operation;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.servlet.http.event.HttpInboundServletRewrite;
import org.ocpsoft.rewrite.servlet.http.event.HttpServletRewrite;

/**
 * An {@link Operation} responsible for sending status codes via {@link HttpServletResponse#setStatus(int)} and
 * {@link HttpServletResponse#flushBuffer()}
 *
 * @author Lincoln Baxter, III
 */
public abstract class SendStatus extends HttpOperation
{
   private final int code;
   private String message;

   private SendStatus(final int code)
   {
      this.code = code;
   }

   private SendStatus(final int code, String message)
   {
      this.code = code;
      this.message = message;
   }

   @Override
   public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
   {
      if (event instanceof HttpInboundServletRewrite) {
         if (getMessage() != null)
            ((HttpInboundServletRewrite) event).sendStatusCode(this.getCode(), this.getMessage());
         else
            ((HttpInboundServletRewrite) event).sendStatusCode(this.getCode());
      }
   }

   protected int getCode()
   {
      return code;
   }

   protected String getMessage()
   {
      return message;
   }

   /**
    * Create an {@link Operation} that will send an HTTP status code to the browser, then call
    * {@link HttpInboundServletRewrite#abort()}
    */
   public static SendStatus code(final int code)
   {
      return new SendStatus(code) {
         @Override
         public String toString()
         {
            return "SendStatus.code(" + code + ")";
         }
      };
   }

   /**
    * Create an {@link Operation} that will send an HTTP error code to the browser, then call
    * {@link HttpInboundServletRewrite#abort()}
    */
   public static SendStatus error(final int code)
   {
      return new SendError(code, null) {
         @Override
         public String toString()
         {
            return "SendStatus.error(" + code + ")";
         }
      };
   }

   /**
    * Create an {@link Operation} that will send an HTTP error code and message to the browser, then call
    * {@link HttpInboundServletRewrite#abort()}
    */
   public static SendStatus error(final int code, final String message)
   {
      return new SendError(code, message) {
         @Override
         public String toString()
         {
            return "SendStatus.error(" + code + ", \"" + message + "\")";
         }
      };
   }

   private abstract static class SendError extends SendStatus
   {
      public SendError(final int code, String message)
      {
         super(code, message);
      }

      @Override
      public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
      {
         if (event instanceof HttpInboundServletRewrite)
         {
            if (getMessage() != null)
               ((HttpInboundServletRewrite) event).sendErrorCode(this.getCode(), this.getMessage());
            else
               ((HttpInboundServletRewrite) event).sendErrorCode(this.getCode());

         }
      }
   }

}


/*
 * Copyright 2010 Lincoln Baxter, III
 *
 * 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.ocpsoft.rewrite.faces;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.faces.FacesException;
import javax.faces.application.ViewHandler;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.view.ViewDeclarationLanguage;
import javax.servlet.http.HttpServletRequest;

import org.ocpsoft.common.pattern.WeightedComparator;
import org.ocpsoft.common.services.ServiceLoader;
import org.ocpsoft.common.util.Iterators;
import org.ocpsoft.rewrite.faces.spi.FacesActionUrlProvider;
import org.ocpsoft.rewrite.servlet.util.URLBuilder;

/**
 * @author Lincoln Baxter, III <lincoln@ocpsoft.com>
 */
public class RewriteViewHandler extends ViewHandler
{
   protected ViewHandler parent;
   private final ThreadLocal<Boolean> bookmarkable = new ThreadLocal<Boolean>();
   private volatile List<FacesActionUrlProvider> providers;

   /**
    * <b>NOTE:</b> This method should only be used by the getBookmarkableURL and getActionURL methods, for the purposes
    * of rewriting form URLs (which do not include viewParameters.)
    *
    * @return Bookmarkable state - defaults to false if not previously set;
    */
   private boolean isBookmarkable()
   {
      Boolean result = bookmarkable.get();
      if (result == null)
      {
         result = false;
         bookmarkable.set(result);
      }
      return result;
   }

   @Override
   public String deriveLogicalViewId(final FacesContext context, final String rawViewId)
   {
      return parent.deriveLogicalViewId(context, rawViewId);
   }

   private void setBookmarkable(final boolean value)
   {
      bookmarkable.set(value);
   }

   public RewriteViewHandler(final ViewHandler viewHandler)
   {
      super();
      parent = viewHandler;
   }

   @Override
   public Locale calculateLocale(final FacesContext facesContext)
   {
      return parent.calculateLocale(facesContext);
   }

   @Override
   public String calculateRenderKitId(final FacesContext facesContext)
   {
      return parent.calculateRenderKitId(facesContext);
   }

   @Override
   public UIViewRoot createView(final FacesContext context, final String viewId)
   {
      UIViewRoot view = parent.createView(context, viewId);
      return view;
   }

   @Override
   public UIViewRoot restoreView(final FacesContext context, final String viewId)
   {
      UIViewRoot view = parent.restoreView(context, viewId);
      return view;
   }

   @Override
   public String getActionURL(final FacesContext context, final String viewId)
   {
      /*
       * When this method is called for forms, getBookmarkableURL is NOT called; therefore, we have a way to distinguish
       * the two.
       */
      HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
      String result = null;
      if (!isBookmarkable() && !RewriteNavigationHandler.isInNavigation(request)
               && (viewId != null)
               && viewId.equals(context.getViewRoot().getViewId()))
      {
         /*
          * When rendering a form URL, Faces only provides the bare view-id, sans any bookmarkable parameters from the request.
          * We need to restore those ourselves.
          */
         for (FacesActionUrlProvider provider : getProviders()) {
            result = provider.getActionURL(context, viewId);
            if (result != null)
               break;
         }

         if (result != null)
         {
            String parentActionURL = parent.getActionURL(context, viewId);
            if (parentActionURL.contains("?"))
            {
               URLBuilder builder = URLBuilder.createFrom(result);
               builder.getQueryStringBuilder().addParameters(parentActionURL);
               result = builder.toURL();
            }
         }
      }
      if (result == null)
         result = parent.getActionURL(context, viewId);
      return result;
   }

   @SuppressWarnings("unchecked")
   public List<FacesActionUrlProvider> getProviders()
   {
      List<FacesActionUrlProvider> result = providers;

      if (result == null)
      {
        synchronized (this)
        {
          result = providers;
          if (result == null)
          {
            result = Iterators.asList(ServiceLoader.load(FacesActionUrlProvider.class));
            Collections.sort(result, new WeightedComparator());
            providers = result;
          }
        }
      }
      return result;
   }

   @Override
   public String getBookmarkableURL(final FacesContext context, final String viewId,
            final Map<String, List<String>> parameters, final boolean includeViewParams)
   {
      /*
       * When this method is called for <h:link> tags, getActionURL is called as part of the parent call
       */
      setBookmarkable(true);
      String result = parent.getBookmarkableURL(context, viewId, parameters, includeViewParams);
      setBookmarkable(false);
      return result;
   }

   @Override
   public String getRedirectURL(final FacesContext context, final String viewId,
            final Map<String, List<String>> parameters, final boolean includeViewParams)
   {
      return parent.getRedirectURL(context, viewId, parameters, includeViewParams);
   }

   @Override
   public String getResourceURL(final FacesContext facesContext, final String path)
   {
      return parent.getResourceURL(facesContext, path);
   }

   @Override
   public void renderView(final FacesContext facesContext, final UIViewRoot viewRoot) throws IOException,
            FacesException
   {
      parent.renderView(facesContext, viewRoot);
   }

   @Override
   public void writeState(final FacesContext facesContext) throws IOException
   {
      parent.writeState(facesContext);
   }

   /**
    * Canonicalize the given viewId, then pass that viewId to the next ViewHandler in the chain.
    */
   @Override
   public String deriveViewId(final FacesContext context, final String rawViewId)
   {
      String canonicalViewId = new URLDuplicatePathCanonicalizer().canonicalize(rawViewId);
      return parent.deriveViewId(context, canonicalViewId);
   }

   @Override
   public String calculateCharacterEncoding(final FacesContext context)
   {
      return parent.calculateCharacterEncoding(context);
   }

   @Override
   public ViewDeclarationLanguage getViewDeclarationLanguage(final FacesContext context, final String viewId)
   {
      return parent.getViewDeclarationLanguage(context, viewId);
   }

   @Override
   public void initView(final FacesContext context) throws FacesException
   {
      parent.initView(context);
   }
}


/*
 * Copyright 2010 Lincoln Baxter, III
 *
 * 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 com.ocpsoft.pretty.faces.config.mapping;

import java.util.ArrayList;
import java.util.List;

import com.ocpsoft.pretty.faces.el.Expressions;
import com.ocpsoft.pretty.faces.url.URL;
import com.ocpsoft.pretty.faces.url.URLPatternParser;

/**
 * @author Lincoln Baxter, III <lincoln@ocpsoft.com>
 */
public class UrlMapping
{
   private String id = "";
   private String parentId = "";
   private boolean outbound = true;
   private String viewId = "";
   private List<UrlAction> actions = new ArrayList<UrlAction>();
   private String pattern = "";
   private List<QueryParameter> queryParams = new ArrayList<QueryParameter>();
   private List<PathValidator> pathValidators = new ArrayList<PathValidator>();
   private boolean onPostback = true;
   private URLPatternParser parser = null;

   /**
    * Return whether or not this Mapping requires DynaView capabilities
    *
    * @return True if using DynaView, false if not
    */
   public boolean isDynaView()
   {
      return Expressions.isEL(viewId);
   }

   /**
    * Get this patterns pattern parser instance.
    */
   public URLPatternParser getPatternParser()
   {
      if ((parser == null) && (pattern != null))
      {
         this.parser = new URLPatternParser(pattern);
      }
      return parser;
   }

   /**
    * Return a list of validators that belong to the given parameter.
    */
   public List<PathValidator> getValidatorsForPathParam(final PathParameter param)
   {
      List<PathValidator> result = new ArrayList<PathValidator>();
      for (PathValidator pv : pathValidators)
      {
         if (pv.getIndex() == param.getPosition())
         {
            result.add(pv);
         }
      }
      return result;
   }

   /**
    * Test this mapping against a given URL.
    *
    * @return True if this mapping matches the URL, false if not.
    */
   public boolean matches(final URL url)
   {
      return getPatternParser().matches(url);
   }

   /**
    * Get this mapping's ID (not prefixed with "pretty:")
    */
   public String getId()
   {
      return id;
   }

   /**
    * Set this mapping's ID (not prefixed with "pretty:")
    */
   public void setId(final String id)
   {
      this.id = id;
   }

   public String getViewId()
   {
      return viewId;
   }

   public void setViewId(final String viewId)
   {
      this.viewId = viewId;
   }

   public List<UrlAction> getActions()
   {
      return actions;
   }

   public void setActions(final List<UrlAction> actions)
   {
      this.actions = actions;
   }

   public void addAction(final UrlAction action)
   {
      actions.add(action);
   }

   public void addPathValidator(final PathValidator v)
   {
      pathValidators.add(v);
   }

   public List<PathValidator> getPathValidators()
   {
      return pathValidators;
   }

   public void setPathValidators(final List<PathValidator> pathValidators)
   {
      this.pathValidators = pathValidators;
   }

   public String getPattern()
   {
      return pattern;
   }

   public void setPattern(final String pattern)
   {
      this.pattern = pattern;
      // reset the parser so it gets recreated
      this.parser = null;
   }

   public boolean addQueryParam(final QueryParameter param)
   {
      return queryParams.add(param);
   }

   public List<QueryParameter> getQueryParams()
   {
      return queryParams;
   }

   public void setQueryParams(final List<QueryParameter> queryParams)
   {
      this.queryParams = queryParams;
   }

   @Override
   public int hashCode()
   {
      final int prime = 31;
      int result = 1;
      result = prime * result + (id == null ? 0 : id.hashCode());
      return result;
   }

   @Override
   public boolean equals(final Object obj)
   {
      if (this == obj)
      {
         return true;
      }
      if (obj == null)
      {
         return false;
      }
      if (!(obj instanceof UrlMapping))
      {
         return false;
      }
      UrlMapping other = (UrlMapping) obj;
      if (id == null)
      {
         if (other.id != null)
         {
            return false;
         }
      }
      else if (!id.equals(other.id))
      {
         return false;
      }
      return true;
   }

   @Override
   public String toString()
   {
      return "UrlMapping [ " + "id=" + id + ", pattern=" + pattern + ", parentId=" + parentId + ", viewId=" + viewId
               + ", actions=" + actions + ", outbound=" + outbound + ", parser=" + parser + ", pathValidators="
               + pathValidators + ", queryParams=" + queryParams + "]";
   }

   public boolean isOutbound()
   {
      return outbound;
   }

   public void setOutbound(final boolean outbound)
   {
      this.outbound = outbound;
   }

   public boolean isOnPostback()
   {
      return onPostback;
   }

   public void setOnPostback(final boolean onPostback)
   {
      this.onPostback = onPostback;
   }

   public String getParentId()
   {
      return parentId;
   }

   public void setParentId(final String parentId)
   {
      this.parentId = parentId;
   }

   public boolean hasParent()
   {
      return (parentId != null) && !"".equals(parentId.trim());
   }
}


/*
 * Copyright 2011 Lincoln Baxter, III
 *
 * 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.ocpsoft.rewrite.faces.config;

import javax.faces.application.Application;
import javax.faces.context.FacesContext;
import javax.faces.convert.ConverterException;

import org.ocpsoft.common.util.Assert;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.event.Rewrite;
import org.ocpsoft.rewrite.faces.util.NullComponent;
import org.ocpsoft.rewrite.param.Converter;

/**
 * Adapter class that allows to use JSF converters with {@link Rewrite} framework {@link Converter} instances.
 *
 * @author Christian Kaltepoth
 */
public class FacesConverter<T> implements Converter<T>
{

   private final Class<?> targetType;
   private final String converterId;

   private FacesConverter(String converterId, Class<?> targetType)
   {
      this.converterId = converterId;
      this.targetType = targetType;
   }

   /**
    * Create a {@link Converter} adapter for the given target type. The converter will use
    * {@link Application#createConverter(Class)} to obtain the underlying JSF converter.
    */
   public static <T> FacesConverter<T> forType(Class<?> targetType)
   {
      return new FacesConverter<T>(null, targetType);
   }

   /**
    * Create a {@link Converter} with the given ID. The converter will use {@link Application#createConverter(String))}
    * to obtain the underlying JSF converter.
    */
   public static <T> FacesConverter<T> forId(String converterId)
   {
      return new FacesConverter<T>(converterId, null);
   }

   @Override
   @SuppressWarnings("unchecked")
   public T convert(Rewrite event, EvaluationContext context, Object value)
   {
      FacesContext facesContext = FacesContext.getCurrentInstance();
      Assert.notNull(facesContext, "FacesContext.getCurrentInstance() returned null. " +
               "You should use @Deferred so the binding gets executed within the JSF lifecycle.");

      javax.faces.convert.Converter converter = null;
      if (converterId != null) {
         converter = facesContext.getApplication().createConverter(converterId);
         Assert.notNull(converter, "Could not create JSF converter for ID: " + converterId);
      }
      else {
         converter = facesContext.getApplication().createConverter(targetType);
         Assert.notNull(converter, "Could not create JSF converter for type: " + targetType.getName());
      }

      String valueAsString = value != null ? value.toString() : null;
      try {
         return (T) converter.getAsObject(facesContext, new NullComponent(), valueAsString);
      }
      catch (ConverterException e) {
         return null; // TODO: is this correct
      }
   }

   @Override
   public String toString()
   {
      StringBuilder builder = new StringBuilder();
      builder.append("FacesConverterAdapter[");
      if (converterId != null) {
         builder.append("converterId=");
         builder.append(converterId);
      }
      else {
         builder.append("targetType=");
         builder.append(targetType.getName().toString());
      }
      builder.append("]");
      return builder.toString();
   }

}


/*
 * Copyright 2010 Lincoln Baxter, III
 *
 * 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 com.ocpsoft.pretty.faces.util;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Lincoln Baxter, III <lincoln@ocpsoft.com>
 */
public class URLDuplicatePathCanonicalizer
{
    private final Pattern pattern = Pattern.compile("(/[^/]+/)\\.\\.(/[^/]+/)");

    /**
     * Canonicalize a given URL, replacing relative path structures with the
     * condensed path. (Eg. /xxxx/../xxxx/ => /xxxx/)
     *
     * @return Return the canonicalized URL. If the URL requires no
     *         canonicalization, return the original unmodified URL.
     */
    public String canonicalize(final String url)
    {
        if (url != null && url.contains("/../"))
        {
            StringBuffer result = new StringBuffer();
            Matcher m = pattern.matcher(url);
            while (m.find())
            {
                if (m.group(1).equals(m.group(2)))
                {
                    m.appendReplacement(result, m.group(1));
                }
                m.appendTail(result);
            }
            return result.toString();
        }
        return url;
    }

}


/*
 * Copyright 2011 Lincoln Baxter, III
 *
 * 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.ocpsoft.rewrite.servlet.impl;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.ocpsoft.common.pattern.WeightedComparator;
import org.ocpsoft.common.services.NonEnriching;
import org.ocpsoft.common.services.ServiceLoader;
import org.ocpsoft.common.util.Iterators;
import org.ocpsoft.rewrite.servlet.DispatcherType;
import org.ocpsoft.rewrite.servlet.http.HttpRequestCycleWrapper;
import org.ocpsoft.rewrite.servlet.spi.DispatcherTypeProvider;
import org.ocpsoft.rewrite.servlet.spi.RequestParameterProvider;

/**
 * @author Lincoln Baxter, III
 *
 */
public class HttpRewriteRequestCycleWrapper extends HttpRequestCycleWrapper implements NonEnriching
{
   private volatile List<RequestParameterProvider> providers;
   private final List<DispatcherTypeProvider> dispatcherProviders;

   @SuppressWarnings("unchecked")
   public HttpRewriteRequestCycleWrapper()
   {
      if (providers == null)
         synchronized (this) {
            if (providers == null)
               providers = Iterators.asList(ServiceLoader.load(RequestParameterProvider.class));
         }

      dispatcherProviders = Iterators.asList(
               ServiceLoader.loadTypesafe(DispatcherTypeProvider.class).iterator());
      Collections.sort(dispatcherProviders, new WeightedComparator());

   }

   @Override
   public HttpServletRequest wrapRequest(final HttpServletRequest request, final HttpServletResponse response,
            final ServletContext servletContext)
   {
      HttpServletRequest result = request;
      if (HttpRewriteWrappedRequest.getCurrentInstance(request) == null) {
         Map<String, String[]> additionalParams = new LinkedHashMap<String, String[]>();

         for (RequestParameterProvider provider : providers) {
            Map<String, String[]> m = provider.getAdditionalParameters(request, response);
            if (m != null)
            {
               additionalParams.putAll(m);
            }
         }

         result = new HttpRewriteWrappedRequest(request, additionalParams);
      }
      return result;
   }

   @Override
   public HttpServletResponse wrapResponse(final HttpServletRequest request, final HttpServletResponse response,
            final ServletContext servletContext)
   {
      HttpServletResponse result = response;
      /*
       * The Servlet spec requires that the unwrapped response is used in case of dispatch type ERROR.
       * Therefore we must ALWAYS wrap the response for the ERROR dispatch type.
       */
      if (HttpRewriteWrappedResponse.getCurrentInstance(request) == null ||
               getDispatcherType(request, servletContext) == DispatcherType.ERROR)
         result = new HttpRewriteWrappedResponse(request, response, servletContext);
      return result;
   }

   /**
    * Determines the {@link DispatcherType} of the current request using the {@link DispatcherTypeProvider} SPI.
    */
   private DispatcherType getDispatcherType(HttpServletRequest request, ServletContext context)
   {
      for (DispatcherTypeProvider provider : dispatcherProviders) {
         DispatcherType dispatcherType = provider.getDispatcherType(request, context);
         if (dispatcherType != null) {
            return dispatcherType;
         }
      }
      throw new IllegalStateException("Unable to determine dispatcher type of current request");
   }

   @Override
   public int priority()
   {
      return 0;
   }
}