/*
 * 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.annotation.scan;

import java.util.LinkedHashSet;
import java.util.Set;

import org.ocpsoft.logging.Logger;

/**
 *

 * This class represents a package filter specified by the com.ocpsoft.pretty.SCAN_PACKAGES initialization
 * parameter.
 *

 *
 * @author Christian Kaltepoth
 */
public class PackageFilter
{

   private final static Logger log = Logger.getLogger(PackageFilter.class);

   private final Set<String> packageSet = new LinkedHashSet<String>();

   /**
    * Creates a new {@link PackageFilter}
    *
    * @param config Comma-separated list of packages (null safe)
    */
   public PackageFilter(String config)
   {
      // keep empty package set for empty config value
      if (config == null || config.trim().length() == 0)
      {
         if (log.isDebugEnabled())
         {
            log.debug("No package filter specified!");
         }
         return;
      }

      // split string and add all package names
      String[] names = config.split(",");
      for (String name : names)
      {
         if (name.trim().length() > 0)
         {
            packageSet.add(name.trim());
         }
      }

      if (log.isDebugEnabled())
      {
         log.debug("Number of packages in filter: " + packageSet.size());
      }

   }

   /**
    *

    * Checks whether the supplied packages matches the filter.
    *

    *

    * The method returns true if one of the following checks succeed:
    *

    *

    *
The supplied package is one of the packages specified in the filter condition.

    *
The supplied package is a sub-package of one of the packages specified in the filter condition.

    *

    *
    * @param packageName A package name
    * @return true if the filter matches
    */
   public boolean isAllowedPackage(String packageName)
   {

      // No packages in set? Accept all packages.
      if (packageSet.isEmpty())
      {
         return true;
      }

      // check all valid packages
      for (String validPackage : packageSet)
      {
         // accept if package is a sub-package of a valid package
         if (packageName.startsWith(validPackage))
         {
            return true;
         }
      }

      // package noch accepted
      return false;

   }

   /**
    * Returns the total number of base packages that this instance is holding.
    *
    * @return Number of base packages
    */
   public int getNumberOfBasePackages()
   {
      return packageSet.size();
   }

   @Override
   public String toString()
   {
      return "PackageFilter [packageSet=" + packageSet + "]";
   }

}


/*
 * 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.navigate;

import java.util.List;
import java.util.Map.Entry;

import org.ocpsoft.common.services.ServiceLoader;
import org.ocpsoft.common.util.Assert;
import org.ocpsoft.rewrite.faces.RewriteNavigationHandler;
import org.ocpsoft.rewrite.servlet.spi.ResourcePathResolver;
import org.ocpsoft.urlbuilder.Address;
import org.ocpsoft.urlbuilder.AddressBuilder;
import org.ocpsoft.urlbuilder.AddressBuilderPath;

/**
 * Helper class to build JSF action outcomes.
 *
 * @author Christian Kaltepoth
 */
public class Navigate
{
   private final String viewId;
   private final ParameterMap parameters = new ParameterMap();
   private boolean redirect = true;

   private Navigate(String viewId)
   {
      this.viewId = viewId;
   }

   /**
    * Create a new {@link Navigate} instance that will invoke navigation to the specified View ID.
    *

    * Query parameters to be provided to the destination View ID may be supplied via {@link #with(CharSequence, Object)}.
    *
    * @param viewId the destination View ID
    */
   public static Navigate to(String viewId)
   {
      Assert.notNull(viewId, "viewId must not be null");
      return new Navigate(viewId);
   }

   /**
    * Create a new {@link Navigate} instance that will invoke navigation to the specified {@link Class}. For instance,
    * if the class specifies a {@literal @}Join rule, this would generate a navigation case targeting the
    * {@link Address} for the defined resource.
    *

    * Query parameters to be provided to the destination View ID may be supplied via {@link #with(CharSequence, Object)}.
    *
    * @see ResourcePathResolver
    *
    * @param clazz the target {@link Class}
    */
   public static Navigate to(Class<?> clazz)
   {

      Assert.notNull(clazz, "clazz must not be null");

      @SuppressWarnings("unchecked")
      Iterable<ResourcePathResolver> resolvers = ServiceLoader.load(ResourcePathResolver.class);

      for (ResourcePathResolver resolver : resolvers) {
         String viewId = resolver.resolveFrom(clazz);
         if (viewId != null) {
            return new Navigate(viewId);
         }
      }

      throw new IllegalArgumentException("Unable to find the resource path for: " + clazz.getName());
   }

   /**
    * Set a query parameter to be passed to the specified View Id.
    *
    * @param The query parameter name.
    * @param The query parameter value.
    */
   public Navigate with(CharSequence name, Object value)
   {
      Assert.notNull(name, "name must not be null");
      if (value != null) {
         parameters.put(name.toString(), value.toString());
      }
      return this;
   }

   /**
    * Specify that navigation should be performed using JavaServer Faces non-redirecting navigation.
    */
   public Navigate withoutRedirect()
   {
      redirect = false;
      return this;
   }

   /**
    * Build and return the fully constructed navigation {@link String}.
    */
   public String build()
   {
      if (redirect) {
         return buildRedirectOutcome();
      }
      else {
         return buildStandardOutcome();
      }
   }

   /**
    * Builds a special outcome processed by {@link RewriteNavigationHandler}
    */
   private String buildRedirectOutcome()
   {
      AddressBuilderPath builderPath = AddressBuilder.begin().pathEncoded(viewId);
      for (Entry<String, List<String>> param : parameters.entrySet()) {
         String[] values = param.getValue().toArray(new String[param.getValue().size()]);
         builderPath.queryEncoded(param.getKey(), (Object[]) values);
      }
      String url = builderPath.toString();
      return RewriteNavigationHandler.REDIRECT_PREFIX + url;
   }

   /**
    * Builds a standard JSF 2.0 implicit navigation outcome
    */
   private String buildStandardOutcome()
   {
      StringBuilder outcome = new StringBuilder();
      outcome.append(viewId);

      boolean first = true;
      for (Entry<String, List<String>> param : parameters.entrySet()) {

         for (String value : param.getValue()) {

            outcome.append(first ? '?' : '&');

            outcome.append(param.getKey());
            outcome.append('=');
            outcome.append(value);

            first = false;

         }

      }

      return outcome.toString();
   }

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

}


/*
 * 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.annotation.scan;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.servlet.ServletContext;

import org.ocpsoft.rewrite.annotation.api.ClassVisitor;
import org.ocpsoft.rewrite.annotation.spi.ClassFinder;

/**
 * Implementation of {@link ClassFinder} that searches for classes in the /WEB-INF/classes directory of a
 * web application. Please note that this class is stateful. It should be used only for one call to
 * {@link #findClasses(ClassVisitor)}.
 *
 * @author Christian Kaltepoth
 */
public class WebClassesFinder extends AbstractClassFinder
{

   /**
    * The name of the classes directory
    */
   private final static String CLASSES_FOLDER = "/WEB-INF/classes/";

   /**
    * Manage a set of classes already processed
    */
   private final Set<String> processedClasses = new LinkedHashSet<String>();

   /**
    * Initialization
    */
   public WebClassesFinder(ServletContext servletContext, ClassLoader classLoader, PackageFilter packageFilter,
            ByteCodeFilter byteCodeFilter)
   {
      super(servletContext, classLoader, packageFilter, byteCodeFilter);
   }

   @Override
   public void findClasses(ClassVisitor visitor)
   {
      try
      {
         // get the absolute URL of the classes folder
         URL classesFolderUrl = servletContext.getResource(CLASSES_FOLDER);

         // abort if classes folder is missing
         if (classesFolderUrl == null)
         {
            log.warn("Cannot find classes folder: " + CLASSES_FOLDER);
            return;
         }

         // call recursive directory processing method
         processDirectory(classesFolderUrl, CLASSES_FOLDER, visitor);

      }
      catch (MalformedURLException e)
      {
         throw new IllegalStateException("Invalid URL: " + e.getMessage(), e);
      }
   }

   /**
    * Scan for classes in a single directory. This method will call itself recursively if it finds other directories and
    * call {@link #processClass(String, InputStream, ClassVisitor)} when it finds a file ending with ".class" and
    * that is accepted by the {@link PackageFilter}
    *
    * @param absoluteUrl The absolute URL of the WEB-INF node to scan
    * @param relativePath The path of the node inside the WEB-INF
    * @param visitor The visitor class to call for classes found
    * @throws MalformedURLException for invalid URLs
    */
   protected void processDirectory(URL absoluteUrl, String relativePath, ClassVisitor visitor)
            throws MalformedURLException
   {

      // log directory name on trace level
      if (log.isTraceEnabled())
      {
         log.trace("Processing directory: " + relativePath);
      }

      // Convert url to string, this will result in a full path of a node in an exploded war archive
      // let it be "file://opt/server/application/abc/expoded/ear/war/WEB-INF/classes/com/"
      String urlAsString = absoluteUrl.toString();

      Set<?> paths = servletContext.getResourcePaths(relativePath);
      if (paths == null || paths.isEmpty())
      {
         return;
      }

      // Process child nodes
      for (Object obj : paths)
      {
         // produces "/WEB-INF/classes/com/mycompany/"
         String childNodeName = obj.toString();

         // get last part of the node path (folder or class entry)
         // for example for childnode "/WEB-INF/classes/com/mycompany/" returns "mycompany/"
         String childNodeRelative = getChildNodeName(childNodeName);

         // get the folder of the node inside WEB-INF
         // for example for childnode "/WEB-INF/classes/com/mycompany/" returns "/WEB-INF/classes/com/"
         String webInfFolder = childNodeName.substring(0, childNodeName.length() - childNodeRelative.length());

         // Find relative base folder
         // produces "file://opt/server/application/abc/expoded/ear/war/"
         String urlBase = urlAsString.substring(0, urlAsString.length() - webInfFolder.length());

         // Create child node URL
         // produces "file://opt/server/application/abc/expoded/ear/war/WEB-INF/classes/com/mycompany/"
         URL childNodeUrl = new URL(urlBase + childNodeName);

         if (childNodeRelative.endsWith("/"))
         {
            // Recursive cal
            processDirectory(childNodeUrl, childNodeName, visitor);
         }
         if (childNodeRelative.endsWith(".class"))
         {
            handleClassEntry(childNodeName, visitor);
         }
      }
   }

   /**
    * Handles class entry in a WEB-INF.
    */
   private void handleClassEntry(String entryName, ClassVisitor visitor)
   {

      // build class name from relative name
      String className = getClassName(entryName.substring(CLASSES_FOLDER.length()));

      // check filter
      if (mustProcessClass(className) && !processedClasses.contains(className))
      {

         // mark this class as processed
         processedClasses.add(className);

         // the class file stream
         InputStream classFileStream = null;

         // close the stream in finally block
         try
         {

            /*
             * Try to open the .class file. if this isn't possible, we will scan it anyway.
             */
            classFileStream = servletContext.getResourceAsStream(entryName);

            if (classFileStream == null)
            {
               if (log.isDebugEnabled())
               {
                  log.debug("Could not obtain InputStream for class file: " + entryName);
               }
            }

            // analyze the class (with or without classFileStream)
            processClass(className, classFileStream, visitor);

         }
         finally
         {
            try
            {
               if (classFileStream != null)
               {
                  classFileStream.close();
               }
            }
            catch (IOException e)
            {
               if (log.isDebugEnabled())
               {
                  log.debug("Failed to close input stream: " + e.getMessage());
               }
            }
         }
      }

   }

   /**
    * @param path The path
    * @return last node in a a string representation of URL path. For example for "/a/b/c/d/" returns "d/", for
    *         "/a/b/c/d.class" returns "d.class"
    */
   private String getChildNodeName(String path)
   {
      String[] elements = path.split("/");
      int size = elements.length;
      String nodeName = elements[size - 1];
      return path.endsWith("/") ? nodeName + "/" : nodeName;
   }

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

}


/*
 * 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 java.net.MalformedURLException;
import java.util.Set;

import javax.servlet.ServletContext;

import org.ocpsoft.logging.Logger;
import org.ocpsoft.rewrite.config.Condition;
import org.ocpsoft.rewrite.config.ConfigurationRuleParameterBuilder;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.param.ParameterStore;
import org.ocpsoft.rewrite.param.Parameterized;
import org.ocpsoft.rewrite.param.ParameterizedPattern;
import org.ocpsoft.rewrite.param.ParameterizedPatternBuilder;
import org.ocpsoft.rewrite.param.ParameterizedPatternParser;
import org.ocpsoft.rewrite.param.RegexParameterizedPatternParser;
import org.ocpsoft.rewrite.servlet.http.event.HttpServletRewrite;
import org.ocpsoft.rewrite.util.Transpositions;
import org.ocpsoft.urlbuilder.Address;

/**
 * A {@link Condition} responsible for determining existence of resources within the
 * {@link ServletContext#getResourcePaths(String)} of the servlet container.
 *
 * @author Lincoln Baxter, III
 */
public abstract class Resource extends HttpCondition implements Parameterized
{
   private static final Logger log = Logger.getLogger(Resource.class);

   private final ParameterizedPatternParser resource;

   private Resource(final String resource)
   {
      this.resource = new RegexParameterizedPatternParser(resource);
   }

   @Override
   public boolean evaluateHttp(final HttpServletRewrite event, final EvaluationContext context)
   {
      if (resource != null)
      {
         ParameterizedPatternBuilder builder = resource.getBuilder();
         if (builder.isParameterComplete(event, context))
         {
            String file = builder.build(event, context, Transpositions.encodePath());
            try
            {
               return (event.getServletContext().getResource(file) != null);
            }
            catch (MalformedURLException e)
            {
               log.debug("Invalid file format [{}]", file);
            }
         }
         else
         {
            /*
             * Parameter didn't exist, that's OK, switch to parsing mode.
             */
            @SuppressWarnings("unchecked")
            Set<String> paths = event.getServletContext().getResourcePaths("/");
            for (String path : paths)
            {
               if (resource.parse(path).matches())
               {
                  return true;
               }
            }
         }
      }
      return false;
   }

   /**
    * Create a {@link Condition} that returns true if the given resource exists in the
    * {@link ServletContext#getResourcePaths(String)} of the current application.
    *
    *

    * The given resource path may be parameterized:
    *

    *
    *    /example/{param}.html

    *    /css/{value}.css

    *    ...
    *
    *

    *
    * @param location {@link ParameterizedPattern} specifying the {@link Address} of the internal resource.
    *
    * @see {@link ConfigurationRuleParameterBuilder#where(String)}
    */
   public static Resource exists(final String resource)
   {
      return new Resource(resource) {
         @Override
         public String toString()
         {
            return "Resource.exists(\"" + resource + "\")";
         }
      };
   }

   /**
    * Get the {@link ParameterizedPattern} of this {@link Resource}.
    */
   public ParameterizedPatternParser getExpression()
   {
      return resource;
   }

   @Override
   public Set<String> getRequiredParameterNames()
   {
      return resource.getRequiredParameterNames();
   }

   @Override
   public void setParameterStore(ParameterStore store)
   {
      resource.setParameterStore(store);
   }

}


/*
 * 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.config;

import org.ocpsoft.rewrite.util.Visitor;

/**
 * This class allows {@link Visitor} instances to walk through a {@link Rule} tree which may contain
 * {@link CompositeCondition} or {@link CompositeOperation} elements containing other rules.
 *
 * @author Lincoln Baxter, III
 */
public class RuleVisit
{

   private final Rule root;

   /**
    * Initialize class with the supplied root {@link Rule}
    */
   public RuleVisit(Rule root)
   {
      this.root = root;
   }

   /**
    * Submit the supplied {@link Visitor} and apply it to all {@link Rule} instances in the tree
    */
   public void accept(Visitor<Rule> visitor)
   {
      visit(root, visitor);
   }

   /**
    * Method to call the {@link Visitor} for the supplied {@link Rule} and recursively calls itself, if the {@link Rule}
    * is a {@link CompositeRule}.
    */
   private void visit(Rule rule, Visitor<Rule> visitor)
   {
      visitor.visit(rule);

      if (rule instanceof CompositeRule) {
         if (((CompositeRule) rule).getRules() != null)
         {
            for (Rule child : ((CompositeRule) rule).getRules()) {
               visit(child, visitor);
            }
         }
      }
   }

}


/*
 * Copyright 2013 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.urlbuilder;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.ocpsoft.urlbuilder.util.Decoder;
import org.ocpsoft.urlbuilder.util.Encoder;

/**
 * Representation of a uniform resource locator, or web address. Internal state is stored as it is originally provided,
 * and must be encoded or decoded as necessary.
 *
 * @author Lincoln Baxter, III
 */
public class AddressBuilder
{
   private volatile Address address;
   protected volatile CharSequence scheme;
   protected volatile CharSequence schemeSpecificPart;
   protected volatile CharSequence domain;
   protected volatile Integer port;
   protected volatile CharSequence path;
   protected Map<CharSequence, Parameter> parameters = new LinkedHashMap<CharSequence, Parameter>();
   protected Map<CharSequence, Parameter> queries = new LinkedHashMap<CharSequence, Parameter>();
   protected CharSequence anchor;

   protected AddressBuilder()
   {}

   /**
    * Create a new {@link AddressBuilder} instance.
    */
   public static AddressBuilderBase begin()
   {
      return new AddressBuilderBase(new AddressBuilder());
   }

   /**
    * Generate an {@link Address} representing the current state of this {@link AddressBuilder}.
    */
   protected Address build()
   {
      if (address == null)
      {
         address = new ParameterizedAddressResult(this);
      }
      return address;
   }

   /**
    * Generate an {@link Address} representing the current literal state of this {@link AddressBuilder}.
    *

    * (Does not apply parameterization. E.g. The URL `/{foo}` will be treated as literal text, as opposed to calling
    * {@link #build()}, which would result in `foo` being treated as a parameterized expression)
    */
   protected Address buildLiteral()
   {
      if (address == null)
      {
         address = new AddressResult(this);
      }
      return address;
   }

   /**
    * Create a new {@link Address} from the given URL. Improperly formatted or encoded URLs are not parse-able and will
    * result in an exception. No builder parameterization is possible using this method.
    *
    * @see http://en.wikipedia.org/wiki/URI_scheme
    * @throws IllegalArgumentException when the input URL or URL fragment is not valid.
    */
   public static Address create(String url) throws IllegalArgumentException
   {
      try {
         URI u = new URI(url);
         String scheme = u.getScheme();
         String host = u.getHost();
         if (scheme != null && host == null)
            return AddressBuilder.begin().scheme(u.getScheme()).schemeSpecificPart(u.getRawSchemeSpecificPart())
                     .buildLiteral();
         else
            return AddressBuilder.begin().scheme(scheme).domain(host).port(u.getPort())
                     .path(u.getRawPath()).queryLiteral(u.getRawQuery()).anchor(u.getRawFragment()).buildLiteral();
      }
      catch (URISyntaxException e) {
         throw new IllegalArgumentException(
                  "[" + url + "] is not a valid URL fragment. Consider encoding relevant portions of the URL with ["
                           + Encoder.class
                           + "], or use the provided builder pattern via this class to specify part encoding.", e);
      }
   }

   /**
    * Set the scheme section of this {@link Address}.
    */
   AddressBuilderScheme scheme(CharSequence scheme)
   {
      this.scheme = scheme;
      return new AddressBuilderScheme(this);
   }

   /**
    * Set the scheme section of this {@link Address}.
    */
   AddressBuilderSchemeSpecificPart schemeSpecificPart(CharSequence schemeSpecificPart)
   {
      this.schemeSpecificPart = schemeSpecificPart;
      return new AddressBuilderSchemeSpecificPart(this);
   }

   /**
    * Set the domain section of this {@link Address}.
    */
   AddressBuilderDomain domain(CharSequence domain)
   {
      this.domain = domain;
      return new AddressBuilderDomain(this);
   }

   /**
    * Set the port section of this {@link Address}.
    */
   AddressBuilderPort port(int port)
   {
      if (port >= 0)
         this.port = port;
      return new AddressBuilderPort(this);
   }

   /**
    * Set the path section of this {@link Address}. The given value will be stored without additional encoding or
    * decoding.
    */
   AddressBuilderPath path(CharSequence path)
   {
      this.path = path;
      return new AddressBuilderPath(this);
   }

   /**
    * Set the path section of this {@link Address}. The given value will be decoded before it is stored.
    */
   AddressBuilderPath pathDecoded(CharSequence path)
   {
      this.path = Decoder.path(path);
      return new AddressBuilderPath(this);
   }

   /**
    * Set the path section of this {@link Address}. The given value will be encoded before it is stored.
    */
   AddressBuilderPath pathEncoded(CharSequence path)
   {
      this.path = Encoder.path(path);
      return new AddressBuilderPath(this);
   }

   /**
    * Set a query-parameter to a value or multiple values. The given name and values will be stored without additional
    * encoding or decoding.
    */
   AddressBuilderQuery query(CharSequence name, Object... values)
   {
      if (name != null && values != null)
      {
         this.queries.put(name.toString(), Parameter.create(name.toString(), values));
      }
      return new AddressBuilderQuery(this);
   }

   /**
    * Set a query-parameter value or multiple values. The given name and values be decoded before they are stored.
    */
   AddressBuilderQuery queryDecoded(CharSequence name, Object... values)
   {
      if (name != null && values != null)
      {
         List<Object> encodedValues = new ArrayList<Object>(values.length);
         for (Object value : values)
         {
            if (value == null)
               encodedValues.add(value);
            else
               encodedValues.add(Decoder.query(value.toString()));
         }
         this.queries.put(Decoder.query(name.toString()), Parameter.create(name.toString(), encodedValues));
      }
      return new AddressBuilderQuery(this);
   }

   /**
    * Set a query-parameter to a value or multiple values. The given name and values be encoded before they are stored.
    */
   AddressBuilderQuery queryEncoded(CharSequence name, Object... values)
   {
      if (name != null && values != null)
      {
         List<Object> encodedValues = new ArrayList<Object>(values.length);
         for (Object value : values)
         {
            if (value == null)
               encodedValues.add(value);
            else
               encodedValues.add(Encoder.query(value.toString()));
         }
         this.queries.put(Encoder.query(name.toString()), Parameter.create(name.toString(), encodedValues));
      }
      return new AddressBuilderQuery(this);
   }

   /**
    * Set a literal query string without additional encoding or decoding. A leading '?' character is optional; the
    * builder will add one if necessary.
    */
   AddressBuilderQuery queryLiteral(String query)
   {
      if (query != null)
      {
         if (query.startsWith("?"))
            query = query.substring(1);

         /*
          * Linked hash map is important here in order to retain the order of query parameters.
          */
         Map<CharSequence, List<CharSequence>> params = new LinkedHashMap<CharSequence, List<CharSequence>>();
         query = decodeHTMLAmpersands(query);
         int index = 0;

         while ((index = query.indexOf('&')) >= 0 || !query.isEmpty())
         {
            String pair = query;
            if (index >= 0)
            {
               pair = query.substring(0, index);
               query = query.substring(index);
               if (!query.isEmpty())
                  query = query.substring(1);
            }
            else
               query = "";

            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
            {
               name = pair.substring(0, pos);
               value = pair.substring(pos + 1, pair.length());
            }

            List<CharSequence> list = params.get(name);
            if (list == null)
            {
               list = new ArrayList<CharSequence>();
               params.put(name, list);
            }
            list.add(value);
         }

         for (Entry<CharSequence, List<CharSequence>> entry : params.entrySet()) {
            query(entry.getKey(), entry.getValue().toArray());
         }
      }
      return new AddressBuilderQuery(this);
   }

   private String decodeHTMLAmpersands(String url)
   {
      if (url != null)
      {
         int index = 0;
         while ((index = url.indexOf("&")) >= 0)
         {
            url = url.substring(0, index + 1) + url.substring(index + 5);
         }
      }
      return url;
   }

   /**
    * Set the anchor section of this {@link Address}.
    */
   AddressBuilderAnchor anchor(CharSequence anchor)
   {
      this.anchor = anchor;
      return new AddressBuilderAnchor(this);
   }

   /**
    * Set a parameter name and value or values. The supplied values will be stored without additional encoding.
    */
   void set(CharSequence name, Object... values)
   {
      this.parameters.put(name.toString(), Parameter.create(name.toString(), values));
   }

   /**
    * Set a parameter name and value or values. The values will be decoded before they are stored.
    */
   void setDecoded(CharSequence name, Object... values)
   {
      if (name != null && values != null)
      {
         List<Object> encodedValues = new ArrayList<Object>(values.length);
         for (Object value : values)
         {
            if (value == null)
               encodedValues.add(value);
            else
               encodedValues.add(Decoder.path(value.toString()));
         }
         this.parameters.put(name.toString(), Parameter.create(name.toString(), encodedValues));
      }
   }

   /**
    * Set a parameter name and value or values. The values will be encoded before they are stored.
    */
   void setEncoded(CharSequence name, Object... values)
   {
      if (name != null && values != null)
      {
         List<Object> encodedValues = new ArrayList<Object>(values.length);
         for (Object value : values)
         {
            if (value == null)
               encodedValues.add(value);
            else
               encodedValues.add(Encoder.path(value.toString()));
         }
         this.parameters.put(name.toString(), Parameter.create(name.toString(), encodedValues));
      }
   }

   @Override
   public String toString()
   {
      return buildLiteral().toString();
   }

   /**
    * Package private method for {@link Address} implementations to use for rendering.
    */
   static StringBuilder toString(Address address)
   {
      StringBuilder result = new StringBuilder();

      if (address.isSchemeSet())
         result.append(address.getScheme()).append(":");

      if (address.isSchemeSpecificPartSet())
      {
         result.append(address.getSchemeSpecificPart());
      }
      else
      {
         if (address.isDomainSet())
            result.append("//").append(address.getDomain());

         if (address.isPortSet())
            result.append(":").append(address.getPort());

         if (address.isPathSet())
            result.append(address.getPath());

         if (address.isQuerySet())
         {
            if (address.isDomainSet() && !address.isPathSet())
               result.append("/");
            result.append('?').append(address.getQuery());
         }

         if (address.isAnchorSet())
            result.append('#').append(address.getAnchor());
      }
      return result;
   }

   Map<String, List<Object>> getQueries()
   {
      Map<String, List<Object>> result = new LinkedHashMap<String, List<Object>>();
      for (Entry<CharSequence, Parameter> entry : this.queries.entrySet()) {
         CharSequence key = entry.getKey();
         result.put(key == null ? null : key.toString(), entry.getValue().getValues());
      }
      return Collections.unmodifiableMap(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.servlet.config;

import java.net.URL;
import java.util.Set;

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

import org.ocpsoft.rewrite.config.ConfigurationRuleParameterBuilder;
import org.ocpsoft.rewrite.config.Operation;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.param.ParameterStore;
import org.ocpsoft.rewrite.param.Parameterized;
import org.ocpsoft.rewrite.param.ParameterizedPattern;
import org.ocpsoft.rewrite.param.RegexParameterizedPatternBuilder;
import org.ocpsoft.rewrite.servlet.http.event.HttpInboundServletRewrite;
import org.ocpsoft.rewrite.servlet.http.event.HttpServletRewrite;
import org.ocpsoft.rewrite.util.Transpositions;

/**
 * An {@link Operation} that performs redirects via {@link HttpInboundServletRewrite#redirectPermanent(String)} and
 * {@link HttpInboundServletRewrite#redirectTemporary(String)}
 *
 * @author Lincoln Baxter, III
 */
public abstract class Redirect extends HttpOperation implements Parameterized
{
   private final RedirectType type;

   private final RegexParameterizedPatternBuilder location;

   private Redirect(final String location, final RedirectType type)
   {
      this.location = new RegexParameterizedPatternBuilder("[^/]+", location);
      this.type = type;
   }

   @Override
   public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
   {
      if (event instanceof HttpInboundServletRewrite)
      {
         String target = location.build(event, context, Transpositions.encodePath());
         switch (type)
         {
         case PERMANENT:
            ((HttpInboundServletRewrite) event).redirectPermanent(target);
            break;
         case TEMPORARY:
            ((HttpInboundServletRewrite) event).redirectTemporary(target);
            break;
         default:
            break;
         }
      }
   }

   /**
    * Create an {@link Operation} that issues a permanent {@link Redirect} ( 301
    * {@link HttpServletResponse#SC_MOVED_PERMANENTLY} ) to the given location. If the given location is not the same as
    * {@link HttpServletRewrite#getAddress()}, this will change the browser {@link URL} and result in a new request.
    *
    *

    * Note that in order to redirect to a resource within the {@link ServletContext}, you must prepend the
    * {@link ServletContext#getContextPath()}.
    *
    *

    * For example:

    *
    *    Redirect.permanent(contextPath + "/example/location.html")

    *
    *
    *

    * The given location may be parameterized:
    *

    *
    *    /store/global
    *    /store/{category}

    *    /store/{category}/item/{itemId}

    *    ...
    *
    *

    *
    * @param location {@link ParameterizedPattern} specifying the target location.
    *
    * @see ConfigurationRuleParameterBuilder#where(String)
    */
   public static Redirect permanent(final String location)
   {
      return new Redirect(location, RedirectType.PERMANENT) {
         @Override
         public String toString()
         {
            return "Redirect.permanent(\"" + location + "\")";
         }
      };
   }

   /**
    * Create an {@link Operation} that issues a temporary {@link Redirect} ( 302
    * {@link HttpServletResponse#SC_MOVED_TEMPORARILY} ) to the given location. If the given location is not the same as
    * {@link HttpServletRewrite#getAddress()}, this will change the browser {@link URL} and result in a new request.
    *

    * Note that in order to redirect within the {@link ServletContext}, you must prepend the
    * {@link ServletContext#getContextPath()}.
    *

    * For example:

    *
    *    Redirect.temporary(contextPath + "/example/location.html")

    *
    *
    *

    * The given location may be parameterized:
    *

    *
    *    /store/global
    *    /store/{category}

    *    /store/{category}/item/{itemId}

    *    ...
    *
    *

    *
    * @param location {@link ParameterizedPattern} specifying the target location.
    *
    * @see ConfigurationRuleParameterBuilder#where(String)
    */
   public static Redirect temporary(final String location)
   {
      return new Redirect(location, RedirectType.TEMPORARY) {
         @Override
         public String toString()
         {
            return "Redirect.temporary(\"" + location + "\")";
         }
      };
   }

   private enum RedirectType
   {
      PERMANENT(301),
      TEMPORARY(302);

      private int code;

      private RedirectType(int code)
      {
         this.code = code;
      }

      @SuppressWarnings("unused")
      public int getCode()
      {
         return code;
      }
   }

   public RegexParameterizedPatternBuilder getTargetExpression()
   {
      return location;
   }

   @Override
   public Set<String> getRequiredParameterNames()
   {
      return location.getRequiredParameterNames();
   }

   @Override
   public void setParameterStore(ParameterStore store)
   {
      location.setParameterStore(store);
   }
}


/*
 * 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 java.util.Arrays;

import javax.servlet.http.HttpServletRequest;

import org.ocpsoft.common.util.Assert;
import org.ocpsoft.common.util.Strings;
import org.ocpsoft.rewrite.config.Condition;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.servlet.http.event.HttpServletRewrite;
import org.ocpsoft.urlbuilder.Address;

/**
 * A {@link Condition} responsible for asserting on the {@link HttpServletRequest#getServerPort()}
 *
 * @author Lincoln Baxter, III
 */
public abstract class ServerPort extends HttpCondition
{
   private final int[] ports;

   private ServerPort(final int... ports)
   {
      Assert.notNull(ports, "Must specify at least one valid port.");
      for (int port : ports) {

         if ((port < 1) || (port > 65535))
         {
            throw new IllegalArgumentException("Invalid port number: " + ports
                     + " - must be between 1 and 65535, inclusive.");
         }
      }
      this.ports = ports;
   }

   /**
    * Create a {@link Condition} to assert that the current {@link Address#getPort()} matches any of the given ports.
    */
   public static ServerPort is(final int... ports)
   {
      return new ServerPort(ports) {
         @Override
         public String toString()
         {
            return "ServerPort.is(" + Strings.join(Arrays.asList(ports), ", ") + ")";
         }
      };
   }

   @Override
   public boolean evaluateHttp(final HttpServletRewrite event, final EvaluationContext context)
   {
      int serverPort = event.getAddress().getPort();
      for (int port : ports) {
         if (serverPort == port)
            return true;
      }
      return false;
   }

}


/*
 * Copyright 2013 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.urlbuilder;

/**
 * An {@link Address} with a scheme section.
 *
 * @author Lincoln Baxter, III
 */
public class AddressBuilderScheme implements BuildableAddress
{
   private AddressBuilder parent;

   AddressBuilderScheme(AddressBuilder parent)
   {
      this.parent = parent;
   }

   @Override
   public Address build()
   {
      return parent.build();
   }

   @Override
   public Address buildLiteral()
   {
      return parent.buildLiteral();
   }

   /**
    * Set the domain section of this {@link Address}.
    */
   public AddressBuilderDomain domain(CharSequence host)
   {
      return parent.domain(host);
   }

   /**
    * Set the scheme specific part section of this {@link Address}.
    */
   public AddressBuilderSchemeSpecificPart schemeSpecificPart(CharSequence schemeSpecificPart)
   {
      return parent.schemeSpecificPart(schemeSpecificPart);
   }

   /**
    * Set a parameter name and value or values. The supplied values will be stored without additional encoding.
    */
   public AddressBuilderScheme set(CharSequence name, Object... values)
   {
      parent.set(name, values);
      return this;
   }

   /**
    * Set a parameter name and value or values. The values will be decoded before they are stored.
    */
   public AddressBuilderScheme setDecoded(CharSequence name, Object... values)
   {
      parent.setDecoded(name, values);
      return this;
   }

   /**
    * Set a parameter name and value or values. The values will be encoded before they are stored.
    */
   public AddressBuilderScheme setEncoded(CharSequence name, Object... values)
   {
      parent.setEncoded(name, values);
      return this;
   }

   /**
    * Set the port section of this {@link Address}.
    */
   public AddressBuilderPort port(int port)
   {
      return parent.port(port);
   }

   /**
    * Set the non-encoded path section of this {@link Address}. The given value will be stored without additional
    * encoding or decoding.
    */
   public AddressBuilderPath path(CharSequence path)
   {
      return parent.path(path);
   }

   /**
    * Set the path section of this {@link Address}. The given value will be decoded before it is stored.
    */
   public AddressBuilderPath pathDecoded(CharSequence path)
   {
      return parent.pathDecoded(path);
   }

   /**
    * Set the path section of this {@link Address}. The given value will be encoded before it is stored.
    */
   public AddressBuilderPath pathEncoded(CharSequence path)
   {
      return parent.pathDecoded(path);
   }

   /**
    * Set a query-parameter to a value or multiple values. The given name and values will be stored without additional
    * encoding or decoding.
    */
   public AddressBuilderQuery query(CharSequence name, Object... values)
   {
      return parent.query(name, values);
   }

   /**
    * Set a query-parameter value or multiple values. The given name and values be decoded before they are stored.
    */
   public AddressBuilderQuery queryDecoded(CharSequence name, Object... values)
   {
      return parent.queryDecoded(name, values);
   }

   /**
    * Set a query-parameter to a value or multiple values. The given name and values be encoded before they are stored.
    */
   public AddressBuilderQuery queryEncoded(CharSequence name, Object... values)
   {
      return parent.queryEncoded(name, values);
   }

   /**
    * Set a literal query string without additional encoding or decoding. A leading '?' character is optional; the
    * builder will add one if necessary.
    */
   public AddressBuilderQuery queryLiteral(String query)
   {
      return parent.queryLiteral(query);
   }

   /**
    * Set the anchor section of this {@link Address}.
    */
   public AddressBuilderAnchor anchor(String anchor)
   {
      return parent.anchor(anchor);
   }

   @Override
   public String toString()
   {
      return parent.toString();
   }

}


/*
 * 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 org.ocpsoft.rewrite.config.Operation;
import org.ocpsoft.rewrite.config.OperationBuilder;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.event.Rewrite;
import org.ocpsoft.rewrite.servlet.event.ServletRewrite;
import org.ocpsoft.rewrite.servlet.http.event.HttpServletRewrite;

/**
 * An {@link Operation} responsible for controlling the {@link Rewrite} life-cycle itself.
 *
 * @author Lincoln Baxter, III
 */
public abstract class Lifecycle extends HttpOperation
{
   /**
    * Create an {@link Operation} that calls {@link ServletRewrite#abort()}
    */
   public static OperationBuilder abort()
   {
      return new Lifecycle() {
         @Override
         public void performHttp(HttpServletRewrite event, EvaluationContext context)
         {
            event.abort();
         }

         @Override
         public String toString()
         {
            return "Lifecycle.abort()";
         }
      };
   }

   /**
    * Create an {@link Operation} that calls {@link ServletRewrite#handled()}
    */
   public static OperationBuilder handled()
   {
      return new Lifecycle() {
         @Override
         public void performHttp(HttpServletRewrite event, EvaluationContext context)
         {
            event.handled();
         }

         @Override
         public String toString()
         {
            return "Lifecycle.handled()";
         }
      };
   }

   /**
    * Create an {@link Operation} that calls {@link ServletRewrite#proceed()}
    */
   public static OperationBuilder proceed()
   {
      return new Lifecycle() {
         @Override
         public void performHttp(HttpServletRewrite event, EvaluationContext context)
         {
            event.proceed();
         }

         @Override
         public String toString()
         {
            return "Lifecycle.proceed()";
         }
      };
   }
}


/*
 * 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.rewrite;

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

/**
 * @author Lincoln Baxter, III <lincoln@ocpsoft.com>
 */
public class RewriteRule
{
    private String match = "";
    private String substitute = "";
    private String processor = "";
    private String url = "";
    private Redirect redirect = Redirect.PERMANENT;
    private boolean inbound = true;
    private boolean outbound = true;
    private Case toCase = Case.IGNORE;
    private TrailingSlash trailingSlash = TrailingSlash.IGNORE;

    private Pattern pattern = Pattern.compile("");

    /**
     * Return true if the <b>match</b> field contains a regex that matches some
     * or all of the given URL, false if no match is found.
     */
    public boolean matches(final String url)
    {
        if (url != null)
        {
            if (getMatch().length() == 0)
            {
                return true;
            }
            else
            {
                Matcher m = pattern.matcher(url).useAnchoringBounds(outbound);
                while (m.find())
                {
                    return true;
                }
            }
        }
        return false;
    }

    public String getMatch()
    {
        return match == null ? "" : match;
    }

    public void setMatch(final String match)
    {
        pattern = Pattern.compile(match);
        this.match = match;
    }

    public String getSubstitute()
    {
        return substitute == null ? "" : substitute;
    }

    public void setSubstitute(final String substitute)
    {
        this.substitute = substitute;
    }

    public boolean isInbound()
    {
        return inbound;
    }

    public void setInbound(final boolean inbound)
    {
        this.inbound = inbound;
    }

    public boolean isOutbound()
    {
        return outbound;
    }

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

    public Case getToCase()
    {
        return toCase == null ? Case.IGNORE : toCase;
    }

    public void setToCase(final Case toCase)
    {
        this.toCase = toCase;
    }

    public TrailingSlash getTrailingSlash()
    {
        return trailingSlash == null ? TrailingSlash.IGNORE : trailingSlash;
    }

    public void setTrailingSlash(final TrailingSlash trailingSlash)
    {
        this.trailingSlash = trailingSlash;
    }

    public Pattern getPattern()
    {
        return pattern;
    }

    public String getProcessor()
    {
        return processor == null ? "" : processor;
    }

    public void setProcessor(final String processor)
    {
        this.processor = processor;
    }

    public Redirect getRedirect()
    {
        return redirect == null ? Redirect.PERMANENT : redirect;
    }

    public void setRedirect(final Redirect redirect)
    {
        this.redirect = redirect;
    }

    public String getUrl()
    {
        return url == null ? "" : url;
    }

    public void setUrl(final String url)
    {
        this.url = url;
    }

    @Override
    public String toString()
    {
        return "RewriteRule [inbound=" + inbound + ",  match=" + match + ", outbound=" + outbound + ", pattern="
                + pattern + ", processor=" + processor + ", redirect=" + redirect + ", substitute=" + substitute
                + ", toCase=" + toCase + ", trailingSlash=" + trailingSlash + ", url=" + url + "]";
    }
}


/*
 * 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.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.ocpsoft.pretty.PrettyException;
import com.ocpsoft.pretty.faces.config.mapping.PathParameter;
import com.ocpsoft.pretty.faces.el.ExpressionProcessorRunner;
import com.ocpsoft.pretty.faces.el.Expressions;

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

   private static final Pattern EL_REGEX_PATTERN = Pattern.compile(Expressions.EL_REGEX);

   private final String originalPattern;
   private boolean elPattern;
   private URL urlPattern = null;
   private Pattern urlElPattern;
   private List<Segment> pathSegments = new ArrayList<Segment>();
   private List<PathParameter> pathParameters = new ArrayList<PathParameter>();

   /**
    * Set the pattern for which this parser will match. Find and replace all el expressions with regular expressions to
    * extract values from parsed URLs. Also extract all parameter names from expressions, and replace with valid EL
    *
    * @param pattern Pattern to use as a parse template
    */
   public URLPatternParser(final String pattern)
   {
      originalPattern = pattern;
      Matcher expressionMatcher = EL_REGEX_PATTERN.matcher(pattern);
      StringBuffer segmentableExpressions = new StringBuffer();

      elPattern = false;
      if (pattern.indexOf('|') != -1) {
         elPattern = true;
      }

      /*
       * Extract path parameters
       */
      int paramIndex = 0;
      while (expressionMatcher.find())
      {
         elPattern = true;
         String expression = expressionMatcher.group(1);
         PathParameter param = ExpressionProcessorRunner.process(expression);
         param.setPosition(paramIndex);
         pathParameters.add(param);

         expressionMatcher.appendReplacement(segmentableExpressions, Segment.parameterize(paramIndex));
         paramIndex++;
      }
      expressionMatcher.appendTail(segmentableExpressions);
      this.pathParameters = Collections.unmodifiableList(pathParameters);

      URL segmentedPattern = new URL(segmentableExpressions.toString());

      /*
       * Extract path segments, overlaying regexes found during parameter
       * discovery.
       */
      for (String segmentPattern : segmentedPattern.getSegments())
      {
         Segment segment = new Segment();
         segment.setTemplate(segmentPattern);
         StringBuffer regex = new StringBuffer();

         Matcher parameterMatcher = Segment.getTemplateMatcher(segmentPattern);
         while (parameterMatcher.find())
         {
            String group = parameterMatcher.group(1);
            PathParameter parameter = pathParameters.get(Integer.valueOf(group));
            segment.addParameter(parameter);
            parameterMatcher.appendReplacement(regex, "(" + parameter.getRegex() + ")");
         }
         parameterMatcher.appendTail(regex);

         segment.setRegex(regex.toString());
         pathSegments.add(segment);
      }

      List<String> regexSegments = new ArrayList<String>();
      for (Segment s : pathSegments)
      {
         regexSegments.add(s.getRegex());
      }

      urlPattern = new URL(regexSegments, segmentedPattern.getMetadata().copy());
      if (elPattern) {
         urlElPattern = Pattern.compile(urlPattern.toURL());
      }
      this.pathSegments = Collections.unmodifiableList(pathSegments);
   }

   /**
    * Return true if this parser matches the given URL, otherwise, return false.
    */
   public boolean matches(final URL target)
   {
      if (elPattern) {
         return urlElPattern.matcher(target.toURL()).matches();
      }
      else {
         return urlPattern.toURL().equals(target.toURL());
      }
   }

   /**
    * Return true if this parser matches the given URL, otherwise, return false.
    */
   @Deprecated
   public boolean matches(final String target)
   {
      URL url = new URL(target);
      return matches(url);
   }

   /**
    * Return the list of parameters specified by this pattern.
    */
   public List<PathParameter> getPathParameters()
   {
      return pathParameters;
   }

   /**
    * Builds a list of PathParameters for this UrlPattern, extracted from the provided URL (assuming a match is found).
    * This list excludes duplicate named parameters.
    */
   public List<PathParameter> parse(final URL url)
   {
      List<PathParameter> result = new ArrayList<PathParameter>();

      String inboundUrl = url.toURL();
      if (this.matches(url))
      {
         Iterator<Segment> iter = pathSegments.iterator();
         while (iter.hasNext())
         {
            Segment segment = iter.next();

            String regex = "";
            if (url.hasLeadingSlash() && !"/".equals(url.toURL()))
            {
               regex += "/";
            }

            regex += segment.getRegex();
            if (iter.hasNext() || url.hasTrailingSlash())
            {
               regex += "/";
            }

            Matcher segmentMatcher = Pattern.compile(regex).matcher(inboundUrl);
            if (segmentMatcher.find())
            {
               for (int j = 0; j < segment.numParameters(); j++)
               {
                  String value = segmentMatcher.group(j + 1);
                  PathParameter param = segment.getParameter(j).copy();
                  param.setValue(value);
                  result.add(param);
               }

               int regionEnd = segmentMatcher.end();

               inboundUrl = inboundUrl.substring(regionEnd - 1);
            }
            else
            {
               throw new PrettyException("Error parsing url: <" + url
                        + ">, a parameter did not match compiled segment in pattern: " + originalPattern);
            }
         }
      }
      else
      {
         throw new IllegalArgumentException("The given URL: " + url + ", cannot be parsed by the pattern: "
                  + originalPattern);
      }
      return result;
   }

   /**
    * URL encoding/decoding is not a concern of this method.
    *
    * @param params Array of Object parameters, in order, to be substituted for mapping pattern values or el
    *           expressions. This method will call the toString() method on each object provided.
    *           <p>
    *           If only one param is specified and it is an instance of List, the list items will be used as parameters
    *           instead. An empty list or a single null parameter are both treated as if no parameters were specified.
    *           </p>
    *           E.g: getMappedUrl(12,55,"foo","bar") for a pattern of /#{el.one}/#{el.two}/#{el.three}/#{el.four}/ will
    *           return the String: "/12/55/foo/bar/"
    * @return A URL based on this object's urlPatten, with values substituted for el expressions in the order provided
    */
   public URL getMappedURL(final Object... params)
   {
      URL result = null;
      if (params != null)
      {
         Object[] parameters = params;

         /*
          * Check to see if our parameters were provided as a single List
          */
         if ((params.length == 1) && (params[0] != null) && (params[0] instanceof List<?>))
         {
            List<?> list = (List<?>) params[0];
            if (list.size() == 0)
            {
               parameters = new Object[0];
            }
            else
            {
               parameters = list.toArray(params);
            }
         }
         else if ((params.length == 1) && (params[0] == null))
         {
            parameters = new Object[0];
         }

         /*
          * Assert that we have the proper number of parameters.
          */
         if (getParameterCount() != parameters.length)
         {
            throw new PrettyException("Invalid number of path parameters supplied for pattern: " + originalPattern
                     + ", expected <" + getParameterCount() + ">, but got <" + parameters.length + ">");
         }

         /*
          * Build the result URL
          */
         int paramIndex = 0;
         List<String> resultSegments = new ArrayList<String>();
         for (Segment segment : pathSegments)
         {
            String template = segment.getTemplate();
            Matcher parameterMatcher = Segment.getTemplateMatcher(template);

            StringBuffer sb = new StringBuffer();
            while (parameterMatcher.find())
            {
               /*
                * We need to escape $ and \ because they have a special meaning when
                * used in Matcher#appendReplacement(). From the docs:
                *
                * Note that backslashes (\) and dollar signs ($) in the replacement string
                * may cause the results to be different than if it were being treated as a
                * literal replacement string. Dollar signs may be treated as references to
                * captured subsequences as described above, and backslashes are used to
                * escape literal characters in the replacement string.
                */
               String replacement = parameters[paramIndex].toString()
                        .replace("$", "\\$")
                        .replace("\\", "\\\\");

               parameterMatcher.appendReplacement(sb, replacement);
               paramIndex++;
            }
            parameterMatcher.appendTail(sb);
            resultSegments.add(sb.toString());
         }
         result = new URL(resultSegments, urlPattern.getMetadata());
      }
      else if (getParameterCount() > 0)
      {
         throw new PrettyException("Invalid number of parameters supplied: " + originalPattern + ", expected <"
                  + getParameterCount() + ">, got <0>");
      }

      return result;
   }

   /**
    * Get the number of URL parameters that this parser expects to find in any given input string
    *
    * @return Number of parameters
    */
   public int getParameterCount()
   {
      return pathParameters.size();
   }

   public Object getPattern()
   {
      return originalPattern;
   }

   /**
    * @return whether the URL pattern is an expression language
    */
   public boolean isElPattern()
   {
      return elPattern;
   }

}


/*
 * 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.bind;

import java.util.Map;

import javax.servlet.ServletRequest;

import org.ocpsoft.rewrite.bind.Binding;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.event.Rewrite;
import org.ocpsoft.rewrite.param.Parameter;
import org.ocpsoft.rewrite.servlet.RewriteWrappedRequest;
import org.ocpsoft.rewrite.servlet.event.ServletRewrite;
import org.ocpsoft.rewrite.servlet.http.event.HttpServletRewrite;
import org.ocpsoft.rewrite.util.Maps;

/**
 * A {@link Binding} responsible for storing and retrieving {@link Parameter} values in the
 * {@link ServletRequest#setAttribute(String, Object)} and {@link ServletRequest#getParameterMap()} contexts.
 *
 * @author Lincoln Baxter, III
 */
public abstract class RequestBinding implements Binding
{
   /**
    * Create a {@link Binding} to the {@link ServletRequest#setAttribute(String, Object)} map.
    */
   public static RequestBinding attribute(final String property)
   {
      return new RequestAttributeBinding(property) {
         @Override
         public String toString()
         {
            return "RequestBinding.attribute(\"" + property + "\")";
         }
      };
   }

   /**
    * Create a {@link Binding} to the {@link ServletRequest#getParameterMap()}.
    */
   public static RequestBinding parameter(final String property)
   {
      return new RequestParameterBinding(property) {
         @Override
         public String toString()
         {
            return "RequestBinding.parameter(\"" + property + "\")";
         }
      };
   }

   @Override
   public boolean supportsRetrieval()
   {
      return true;
   }

   @Override
   public boolean supportsSubmission()
   {
      return true;
   }

   private static class RequestParameterBinding extends RequestBinding
   {
      private final String name;

      public RequestParameterBinding(final String parameter)
      {
         this.name = parameter;
      }

      @Override
      public Object submit(final Rewrite event, final EvaluationContext context, final Object value)
      {
         ServletRequest request = ((ServletRewrite<?, ?>) event).getRequest();
         RewriteWrappedRequest wrapper = RewriteWrappedRequest.getCurrentInstance(request);

         Map<String, String[]> modifiableParameters = wrapper.getModifiableParameters();
         if (value != null)
         {
            if (value.getClass().isArray())
            {
               Object[] values = (Object[]) value;
               for (Object object : values) {
                  Maps.addArrayValue(modifiableParameters, name, object.toString());
               }
            }
            else
            {
               Maps.addArrayValue(modifiableParameters, name, value.toString());
            }
         }
         else
         {
            Maps.addArrayValue(modifiableParameters, name, null);
         }

         return null;
      }

      @Override
      public Object retrieve(final Rewrite event, final EvaluationContext context)
      {
         return ((HttpServletRewrite) event).getRequest().getParameter(name);
      }
   }

   private static class RequestAttributeBinding extends RequestBinding
   {
      private final String name;

      public RequestAttributeBinding(final String attribute)
      {
         this.name = attribute;
      }

      @Override
      public Object submit(final Rewrite event, final EvaluationContext context, final Object value)
      {
         ((HttpServletRewrite) event).getRequest().setAttribute(name, value);
         return null;
      }

      @Override
      public Object retrieve(final Rewrite event, final EvaluationContext context)
      {
         if (event instanceof HttpServletRewrite)
            return ((HttpServletRewrite) event).getRequest().getParameter(name);

         else
            return null;
      }

   }
}


/*
 * 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;

import org.ocpsoft.common.util.Strings;
import org.ocpsoft.rewrite.config.Operation;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.event.Rewrite;
import org.ocpsoft.rewrite.servlet.RewriteWrappedRequest;
import org.ocpsoft.rewrite.servlet.config.encodequery.Base64EncodingStrategy;
import org.ocpsoft.rewrite.servlet.config.encodequery.ChecksumStrategy;
import org.ocpsoft.rewrite.servlet.config.encodequery.EncodingStrategy;
import org.ocpsoft.rewrite.servlet.config.encodequery.HashCodeChecksumStrategy;
import org.ocpsoft.rewrite.servlet.http.event.HttpInboundServletRewrite;
import org.ocpsoft.rewrite.servlet.http.event.HttpOutboundServletRewrite;
import org.ocpsoft.rewrite.servlet.util.QueryStringBuilder;
import org.ocpsoft.rewrite.servlet.util.URLBuilder;
import org.ocpsoft.rewrite.util.Maps;
import org.ocpsoft.urlbuilder.Address;
import org.ocpsoft.urlbuilder.AddressBuilder;

/**
 * An {@link Operation} that encodes any or many {@link Address} query-parameters into a single parameter using the
 * given {@link ChecksumStrategy} and {@link EncodingStrategy}. This can be used to effectively encrypt or obfuscate
 * inbound and outbound query-parameters. Additionally, encoded parameters contain a checksum which can be used to
 * reveal tampering, allowing for appropriate action to be taken on checksum verification failure.
 *
 *

 * For example:

 * ?c=LTg1NDM0OTA1OSM&lang=en_US
 *

 * The above query string contains multiple parameters. The value of parameter 'c' has been encoded using
 * {@link EncodeQuery#to(String)} and has specified that lang be excluded via {@link #excluding(String...)}.
 *
 * @author Lincoln Baxter, III
 *
 */
@SuppressWarnings("deprecation")
public class EncodeQuery implements Operation
{
   private String tokenName;
   private ChecksumStrategy checksumStrategy = new HashCodeChecksumStrategy();
   private EncodingStrategy encodingStrategy = new Base64EncodingStrategy();
   private final List<String> params = new ArrayList<String>();
   private final List<String> excludedParams = new ArrayList<String>();
   private boolean inboundCorrection = true;
   private Operation onfailure;

   private EncodeQuery(final String[] params)
   {
      if ((params != null) && (params.length > 0))
         this.params.addAll(Arrays.asList(params));
   }

   /**
    * Create a new {@link EncodeQuery} operation for the given query-parameter names. Only encode those given
    * parameters. If no parameter names are supplied, this will encode all query-parameters found in the {@link Address}
    * .
    */
   public static EncodeQuery params(final String... params)
   {
      return new EncodeQuery(params);
   }

   /**
    * Exclude the given query-parameter names from encoding.
    */
   public EncodeQuery excluding(final String... params)
   {
      if ((params != null) && (params.length > 0))
         this.excludedParams.addAll(Arrays.asList(params));
      return this;
   }

   /**
    * Use the given {@link EncodingStrategy} when performing encoding.
    */
   public EncodeQuery withEncodingStrategy(final EncodingStrategy strategy)
   {
      this.encodingStrategy = strategy;
      return this;
   }

   /**
    * Use the given {@link ChecksumStrategy} when verifying and embedding checksums.
    */
   public EncodeQuery withChecksumStrategy(final ChecksumStrategy strategy)
   {
      this.checksumStrategy = strategy;
      return this;
   }

   /**
    * Redirect inbound requests to an {@link Address} containing matching query-parameters to the encoded
    * {@link Address}.
    */
   public EncodeQuery withInboundCorrection(final boolean enable)
   {
      inboundCorrection = enable;
      return this;
   }

   /**
    * {@link Operation} to be performed when the current {@link ChecksumStrategy} detects an inbound checksum failure.
    */
   public EncodeQuery onChecksumFailure(final Operation operation)
   {

      this.onfailure = operation;
      return this;
   }

   /**
    * The name of the composite query-parameter to hold the encoded parameters.
    */
   public EncodeQuery to(final String param)
   {
      this.tokenName = param;
      return this;
   }

   @Override
   public void perform(final Rewrite event, final EvaluationContext context)
   {
      if ((event instanceof HttpInboundServletRewrite) && DispatchType.isRequest().evaluate(event, context))
      {
         HttpInboundServletRewrite in = (HttpInboundServletRewrite) event;

         QueryStringBuilder query = QueryStringBuilder.createNew();
         query.addParameters(in.getInboundAddress().getQuery());

         String token = query.decode().getParameter(tokenName);
         if (token != null)
         {
            String decoded = encodingStrategy.decode(token);

            if (checksumStrategy.checksumValid(decoded))
            {
               decoded = checksumStrategy.removeChecksum(decoded);
               query.removeParameter(tokenName);
               QueryStringBuilder queryParams = QueryStringBuilder.createFromEncoded(decoded);
               RewriteWrappedRequest request = RewriteWrappedRequest.getCurrentInstance(in.getRequest());
               for (Entry<String, List<String>> param : queryParams.getParameterMap().entrySet()) {
                  for (String value : param.getValue()) {
                     Maps.addArrayValue(request.getModifiableParameters(), param.getKey(), value);
                  }
               }
            }
            else if (onfailure != null)
            {
               onfailure.perform(event, context);
            }
         }
         else if (!query.isEmpty() && inboundCorrection)
         {
            in.redirectTemporary(in.getAddress().getPathAndQuery());
         }
      }

      else if (event instanceof HttpOutboundServletRewrite)
      {
         HttpOutboundServletRewrite out = (HttpOutboundServletRewrite) event;

         String outboundURL = out.getOutboundAddress().toString();
         URLBuilder url = URLBuilder.createFrom(outboundURL);

         url.getQueryStringBuilder().removeParameter(tokenName);

         QueryStringBuilder newQuery = QueryStringBuilder.createNew();
         for (String param : excludedParams) {
            newQuery.addParameter(param, url.getQueryStringBuilder().removeParameter(param).toArray(new String[] {}));
         }

         if (!params.isEmpty())
         {
            for (String param : url.getQueryStringBuilder().getParameterNames())
            {
               if (!params.contains(param))
               {
                  newQuery.addParameter(param,
                           url.getQueryStringBuilder().removeParameter(param).toArray(new String[] {}));
               }
            }
         }

         if (outboundURL.contains("?") && (outboundURL.startsWith(out.getContextPath()) || outboundURL.startsWith("/")))
         {
            if (!url.getQueryStringBuilder().isEmpty())
            {
               String encoded = checksumStrategy.embedChecksum(url.getQueryStringBuilder().toQueryString());
               encoded = encodingStrategy.encode(encoded);

               newQuery.addParameter(tokenName, encoded);
               out.setOutboundAddress(AddressBuilder.create(url.toPath() + newQuery.toQueryString()));
            }
         }
      }
   }

   @Override
   public String toString()
   {
      return "EncodeQuery.params(\"" + Strings.join(params, "\", \"") + "\")";
   }

}


/*
 * Copyright 2013 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.config;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import org.ocpsoft.common.util.Assert;
import org.ocpsoft.logging.Logger;
import org.ocpsoft.rewrite.bind.Binding;
import org.ocpsoft.rewrite.bind.Evaluation;
import org.ocpsoft.rewrite.context.ContextBase;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.context.RewriteState;
import org.ocpsoft.rewrite.event.Rewrite;
import org.ocpsoft.rewrite.param.ConfigurableParameter;
import org.ocpsoft.rewrite.param.Constraint;
import org.ocpsoft.rewrite.param.DefaultParameter;
import org.ocpsoft.rewrite.param.DefaultParameterStore;
import org.ocpsoft.rewrite.param.Parameter;
import org.ocpsoft.rewrite.param.ParameterStore;
import org.ocpsoft.rewrite.param.ParameterValueStore;
import org.ocpsoft.rewrite.param.Parameterized;
import org.ocpsoft.rewrite.param.Transposition;
import org.ocpsoft.rewrite.util.ParameterUtils;
import org.ocpsoft.rewrite.util.Visitor;

/**
 * An {@link Operation} that allows for conditional evaluation of nested {@link Rule} sets.
 *
 * @author Lincoln Baxter, III
 */
public class Subset extends DefaultOperationBuilder implements CompositeOperation, Parameterized
{
   private static Logger log = Logger.getLogger(Subset.class);
   private final Configuration config;

   private Subset(Configuration config)
   {
      Assert.notNull(config, "Configuration must not be null.");
      this.config = config;
   }

   public static Subset evaluate(Configuration config)
   {
      return new Subset(config);
   }

   /*
    * Executors
    */
   @Override
   public void perform(Rewrite event, EvaluationContext context)
   {
      /*
       * Highly optimized loop - for performance reasons. Think before you change this!
       */
      List<Rule> rules = config.getRules();

      Rule rule = null;
      final EvaluationContextImpl subContext = new EvaluationContextImpl();
      for (int i = 0; i < rules.size(); i++)
      {
         rule = rules.get(i);
         event.getEvaluatedRules().add(rule);

         subContext.clear();
         subContext.put(ParameterStore.class, context.get(ParameterStore.class));
         ParameterValueStore values = (ParameterValueStore) context.get(ParameterValueStore.class);
         subContext.put(ParameterValueStore.class, values);
         subContext.setState(RewriteState.EVALUATING);

         if (rule.evaluate(event, subContext))
         {
            if (handleBindings(event, subContext, values))
            {
               subContext.setState(RewriteState.PERFORMING);

               if (log.isDebugEnabled())
                  log.debug("Rule [" + rule + "] matched and will be performed.");

               List<Operation> preOperations = subContext.getPreOperations();
               for (int k = 0; k < preOperations.size(); k++) {
                  preOperations.get(k).perform(event, subContext);
               }

               if (event.getFlow().isHandled())
               {
                  break;
               }

               rule.perform(event, subContext);

               if (event.getFlow().isHandled())
               {
                  break;
               }

               List<Operation> postOperations = subContext.getPostOperations();
               for (int k = 0; k < postOperations.size(); k++) {
                  postOperations.get(k).perform(event, subContext);
               }

               if (event.getFlow().isHandled())
               {
                  break;
               }
            }
         }
         else
         {
            event.getEvaluatedRules().remove(rule);
         }
      }
   }

   private boolean handleBindings(final Rewrite event, final EvaluationContextImpl context,
            ParameterValueStore values)
   {
      boolean result = true;
      ParameterStore store = (ParameterStore) context.get(ParameterStore.class);

      for (Entry<String, Parameter<?>> entry : store) {
         Parameter<?> parameter = entry.getValue();
         String value = values.retrieve(parameter);

         if (!ParameterUtils.enqueueSubmission(event, context, parameter, value))
         {
            result = false;
            break;
         }
      }
      return result;
   }

   /*
    * Getters
    */

   @Override
   public List<Operation> getOperations()
   {
      return Collections.emptyList();
   }

   class EvaluationContextImpl extends ContextBase implements EvaluationContext
   {
      private final List<Operation> preOperations = new ArrayList<Operation>();
      private final List<Operation> postOperations = new ArrayList<Operation>();
      private RewriteState state;

      public EvaluationContextImpl()
      {
         put(ParameterStore.class, new DefaultParameterStore());
      }

      @Override
      public void addPreOperation(final Operation operation)
      {
         this.preOperations.add(operation);
      }

      @Override
      public void addPostOperation(final Operation operation)
      {
         this.preOperations.add(operation);
      }

      /**
       * Get an immutable view of the added pre-{@link Operation} instances.
       */
      public List<Operation> getPreOperations()
      {
         return Collections.unmodifiableList(preOperations);
      }

      /**
       * Get an immutable view of the added post-{@link Operation} instances.
       */
      public List<Operation> getPostOperations()
      {
         return Collections.unmodifiableList(postOperations);
      }

      @Override
      public String toString()
      {
         return "EvaluationContextImpl [preOperations=" + preOperations + ", postOperations=" + postOperations + "]";
      }

      /**
       * Clears the state of this context so that it may be reused, saving instantiation cost during rule iteration.
       */
      @Override
      public void clear()
      {
         this.postOperations.clear();
         this.postOperations.clear();
         super.clear();
      }

      @Override
      public RewriteState getState()
      {
         return state;
      }

      public void setState(RewriteState state)
      {
         this.state = state;
      }
   }

   @Override
   public Set<String> getRequiredParameterNames()
   {
      return Collections.emptySet();
   }

   @Override
   public void setParameterStore(final ParameterStore parent)
   {
      for (final Rule rule : config.getRules()) {
         if (rule instanceof RuleBuilder) {
            ParameterizedCallback callback = new ParameterizedCallback() {
               @Override
               public void call(Parameterized parameterized)
               {
                  Set<String> names = parameterized.getRequiredParameterNames();
                  if (rule instanceof RuleBuilder)
                  {
                     ParameterStore store = ((RuleBuilder) rule).getParameterStore();

                     for (Entry<String, Parameter<?>> entry : parent) {
                        String name = entry.getKey();
                        Parameter<?> parentParam = entry.getValue();

                        if (!store.contains(name)) {
                           store.get(name, parentParam);
                        }
                        else
                        {
                           Parameter<?> parameter = store.get(name);
                           for (Binding binding : parameter.getBindings()) {
                              if (!parentParam.getBindings().contains(binding))
                                 throwRedefinitionError(rule, name);
                           }

                           for (Constraint<?> constraint : parameter.getConstraints()) {
                              if (!parentParam.getConstraints().contains(constraint))
                                 throwRedefinitionError(rule, name);
                           }

                           for (Transposition<?> transposition : parameter.getTranspositions()) {
                              if (!parentParam.getTranspositions().contains(transposition))
                                 throwRedefinitionError(rule, name);
                           }

                           if (parentParam.getConverter() != null
                                    && !parentParam.getConverter().equals(parameter.getConverter()))
                              throwRedefinitionError(rule, name);

                           if (parentParam.getValidator() != null
                                    && !parentParam.getValidator().equals(parameter.getValidator()))
                              throwRedefinitionError(rule, name);
                        }
                     }

                     for (String name : names) {
                        Parameter<?> parameter = store.get(name, new DefaultParameter(name));
                        if (parameter instanceof ConfigurableParameter<?>)
                           ((ConfigurableParameter<?>) parameter).bindsTo(Evaluation.property(name));
                     }
                     parameterized.setParameterStore(store);
                  }

               }

               private void throwRedefinitionError(Rule rule, String name)
               {
                  throw new IllegalStateException("Subset cannot re-configure parameter [" + name
                           + "] that was configured in parent Configuration. Re-definition was attempted at ["
                           + rule + "] ");
               }
            };

            Visitor<Condition> conditionVisitor = new ParameterizedConditionVisitor(callback);
            new ConditionVisit(rule).accept(conditionVisitor);

            Visitor<Operation> operationVisitor = new ParameterizedOperationVisitor(callback);
            new OperationVisit(rule).accept(operationVisitor);
         }
      }
   }

   @Override
   public String toString()
   {
      return "Subset.evaluate(" + config + ")";
   }
}


package org.ocpsoft.urlbuilder.util;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;

/**
 * Utility class to encode URL path and query parts.
 *
 * @author Lincoln Baxter, III
 */
public class Encoder
{

   private static final Charset UTF8 = Charset.forName("UTF-8");

   /**
    * Encodes the given string using HTML form encoding as described in RFC 2396.
    *
    * @throws IllegalArgumentException when illegal URI syntax is attempted.
    */
   public static String path(CharSequence s) throws IllegalArgumentException
   {
      try
      {
         final URI uri = new URI("http", "0.0.0.0", "/" + s, null);
         return uri.toASCIIString().substring(15);
      }
      catch (URISyntaxException e)
      {
         throw new IllegalArgumentException(e);
      }
   }

   /**
    * Encodes the given string using HTML form encoding as described in RFC 2396.
    *
    * @throws IllegalArgumentException when illegal URI syntax is attempted.
    */
   public static String query(CharSequence s) throws IllegalArgumentException
   {
      try {
         return URLEncoder.encode(s.toString(), UTF8.name());
      }
      catch (UnsupportedEncodingException e) {
         throw new IllegalArgumentException(e);
      }
   }

}


/*
 * 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.beans;

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

import javax.el.ELException;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

import com.ocpsoft.pretty.PrettyException;
import com.ocpsoft.pretty.faces.config.mapping.PathParameter;
import com.ocpsoft.pretty.faces.config.mapping.QueryParameter;
import com.ocpsoft.pretty.faces.config.mapping.UrlMapping;
import com.ocpsoft.pretty.faces.url.QueryString;
import com.ocpsoft.pretty.faces.url.URL;
import com.ocpsoft.pretty.faces.url.URLPatternParser;
import com.ocpsoft.pretty.faces.util.FacesElUtils;
import com.ocpsoft.pretty.faces.util.NullComponent;

/**
 * @author Lincoln Baxter, III <lincoln@ocpsoft.com>
 */
public class ExtractedValuesURLBuilder
{
   private static final FacesElUtils elUtils = new FacesElUtils();

   /**
    * For all required values of the given PrettyUrlMapping, extract values from their mapped backing beans and create a
    * URL based on the url-pattern.
    *
    * @param mapping Mapping for which to extract values and generate URL
    * @return The fully constructed URL
    */
   public URL buildURL(final UrlMapping mapping)
   {
      URL result = null;

      String expression = "";
      Object value = null;
      try
      {
         FacesContext context = FacesContext.getCurrentInstance();

         URLPatternParser parser = mapping.getPatternParser();
         List<PathParameter> parameters = parser.getPathParameters();
         List<String> parameterValues = new ArrayList<String>();
         for (PathParameter injection : parameters)
         {
            // read value of the path parameter
            expression = injection.getExpression().getELExpression();
            value = elUtils.getValue(context, expression);
            if (value == null)
            {
               throw new PrettyException("PrettyFaces: Exception occurred while building URL for MappingId < "
                        + mapping.getId() + " >, Required value " + " < " + expression + " > was null");
            }

            // convert the value to a string using the correct converter
            Converter converter = context.getApplication().createConverter(value.getClass());
            if (converter != null)
            {
               String convertedValue = converter.getAsString(context, new NullComponent(), value);
               if (convertedValue == null)
               {
                  throw new PrettyException("PrettyFaces: The converter <" + converter.getClass().getName()
                           + "> returned null while converting the object <" + value.toString() + ">!");
               }
               value = convertedValue;
            }

            parameterValues.add(value.toString());
         }

         result = parser.getMappedURL(parameterValues).encode();
      }
      catch (ELException e)
      {
         throw new PrettyException("PrettyFaces: Exception occurred while building URL for MappingId < "
                  + mapping.getId() + " >, Error occurred while extracting values from backing bean" + " < "
                  + expression + ":" + value + " >", e);
      }

      return result;
   }

   public QueryString buildQueryString(final UrlMapping mapping)
   {
      QueryString result = new QueryString();

      String expression = "";
      Object value = null;
      try
      {
         FacesContext context = FacesContext.getCurrentInstance();

         List<QueryParameter> queryParams = mapping.getQueryParams();
         List<QueryParameter> queryParameterValues = new ArrayList<QueryParameter>();

         for (QueryParameter injection : queryParams)
         {
            String name = injection.getName();

            expression = injection.getExpression().getELExpression();
            value = elUtils.getValue(context, expression);

            if ((name != null) && (value != null))
            {
               if (value.getClass().isArray())
               {
                  Object[] values = (Object[]) value;
                  for (Object temp : values)
                  {
                     queryParameterValues.add(new QueryParameter(name, temp.toString()));
                  }
               }
               else
               {
                  queryParameterValues.add(new QueryParameter(name, value.toString()));
               }
            }
         }

         result = QueryString.build(queryParameterValues);
      }
      catch (ELException e)
      {
         throw new PrettyException("PrettyFaces: Exception occurred while building QueryString for MappingId < "
                  + mapping.getId() + " >, Error occurred while extracting values from backing bean" + " < "
                  + expression + ":" + value + " >", e);
      }

      return result;
   }

}