/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.server.dispatch;

import com.caucho.config.ConfigException;
import com.caucho.server.util.CauchoSystem;
import com.caucho.util.CharBuffer;
import com.caucho.util.L10N;

import javax.annotation.PostConstruct;
import javax.servlet.ServletException;
import javax.servlet.DispatcherType;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Configuration for a filter.
 */
public class FilterMapping extends FilterConfigImpl {
  static L10N L = new L10N(FilterMapping.class);

  private String _urlPattern;
  private final LinkedHashSet<String> _urlPatterns
    = new LinkedHashSet<String>();
  
  private final ArrayList<String> _servletNames = new ArrayList<String>();
  
  // The match expressions
  private final ArrayList<Match> _matchList = new ArrayList<Match>();

  private HashSet<DispatcherType> _dispatcher;
  
  /**
   * Creates a new filter mapping object.
   */
  public FilterMapping()
  {
  }

  /**
   * Sets the url pattern
   */
  public URLPattern createUrlPattern()
    throws ServletException
  {
    return new URLPattern();
  }

  /**
   * Gets the url pattern
   */
  public String getURLPattern()
  {
    return _urlPattern;
  }

  public HashSet<String> getURLPatterns()
  {
    return _urlPatterns;
  }

  /**
   * Sets the url regexp
   */
  public void setURLRegexp(String pattern)
    throws ServletException
  {
    Pattern regexp;
    
    if (CauchoSystem.isCaseInsensitive())
      regexp = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
    else
      regexp = Pattern.compile(pattern, 0);

    _matchList.add(Match.createInclude(regexp));
  }

  /**
   * Sets the servlet name
   */
  public void addServletName(String servletName)
  {
    if (servletName == null)
      throw new NullPointerException();
    
    _servletNames.add(servletName);
  }

  public ArrayList<String> getServletNames()
  {
    return _servletNames;
  }

  /**
   * Adds a dispatcher.
   */
  public void addDispatcher(String dispatcher)
    throws ConfigException
  {
    try {
      addDispatcher(DispatcherType.valueOf(dispatcher));
    }
    catch (IllegalArgumentException e) {
      throw new ConfigException(L.l("'{0}' is an unknown value for <dispatcher>  'REQUEST', 'FORWARD', 'INCLUDE', and 'ERROR' are the valid values.",
                                    dispatcher));
    }
  }

  public void addDispatcher(DispatcherType dispather)
  {
    if (_dispatcher == null)
      _dispatcher = new HashSet<DispatcherType>();

    _dispatcher.add(dispather);
  }

  /**
   * True if the dispatcher is for REQUEST.
   */
  public boolean isRequest()
  {
    return _dispatcher == null || _dispatcher.contains(DispatcherType.REQUEST);
  }

  /**
   * True if the dispatcher is for INCLUDE.
   */
  public boolean isInclude()
  {
    return _dispatcher != null && _dispatcher.contains(DispatcherType.INCLUDE);
  }

  /**
   * True if the dispatcher is for FORWARD.
   */
  public boolean isForward()
  {
    return _dispatcher != null && _dispatcher.contains(DispatcherType.FORWARD);
  }

  /**
   * True if the dispatcher is for ERROR.
   */
  public boolean isError()
  {
    return _dispatcher != null && _dispatcher.contains(DispatcherType.ERROR);
  }

  /**
   * Returns true if the filter map matches the invocation URL.
   *
   * @param servletName the servlet name to match
   */
  boolean isMatch(String servletName)
  {
    for (int i = 0; i < _servletNames.size(); i++) {
      String matchName = _servletNames.get(i);

      if (matchName.equals(servletName) || "*".equals(matchName))
        return true;
    }

    return false;
  }

  /**
   * Returns true if the filter map matches the invocation URL.
   *
   * @param invocation the request's invocation
   */
  boolean isMatch(ServletInvocation invocation)
  {
    return isMatch(invocation.getServletPath(), invocation.getPathInfo());
  }

  /**
   * Returns true if the filter map matches the servlet path and path info.
   * */
  public boolean isMatch(String servletPath, String pathInfo)
  {
    String uri;

    if (pathInfo == null)
      uri = servletPath;
    else if (servletPath == null)
      uri = pathInfo;
    else
      uri = servletPath + pathInfo;

    int size = _matchList.size();
    
    for (int i = 0; i < size; i++) {
      Match match = _matchList.get(i);

      int value = match.match(uri);

      switch (value) {
      case Match.INCLUDE:
        return true;
      case Match.EXCLUDE:
        return false;
      }
    }

    return false;
  }

  /**
   * Converts a url-pattern to a regular expression
   *
   * @param pattern the url-pattern to convert
   * @param flags the regexp flags, e.g. "i" for case insensitive
   *
   * @return the equivalent regular expression
   */
  private Pattern urlPatternToRegexp(String pattern, int flags)
    throws ServletException
  {
    if (pattern.length() == 0 ||
        pattern.length() == 1 && pattern.charAt(0) == '/') {
      try {
        return Pattern.compile("^/$", flags);
      } catch (Exception e) {
        throw new ServletException(e);
      }
    }

    int length = pattern.length();
    boolean isExact = true;
      
    if (pattern.charAt(0) != '/' && pattern.charAt(0) != '*') {
      pattern = "/" + pattern;
      length++;
    }

    int prefixLength = -1;
    boolean isShort = false;
    CharBuffer cb = new CharBuffer();
    cb.append("^");
    for (int i = 0; i < length; i++) {
      char ch = pattern.charAt(i);
      
      if (ch == '*' && i + 1 == length && i > 0) {
        isExact = false;

        if (pattern.charAt(i - 1) == '/') {
          cb.setLength(cb.length() - 1);

          if (prefixLength < 0)
            prefixLength = i - 1;
          
        }
        else if (prefixLength < 0)
          prefixLength = i;
        
        if (prefixLength == 0)
          prefixLength = 1;
      }
      else if (ch == '*') {
        isExact = false;
        cb.append(".*");
        if (prefixLength < 0)
          prefixLength = i;
        
        if (i == 0)
          isShort = true;
      }
      else if (ch == '.' || ch == '[' || ch == '^' || ch == '$' ||
               ch == '{' || ch == '}' || ch == '|' ||
               ch == '(' || ch == ')' || ch == '?') {
        cb.append('\\');
        cb.append(ch);
      }
      else
        cb.append(ch);
    }

    if (isExact)
      cb.append('$');
    else
      cb.append("(?=/)|" + cb.toString() + "$");

    try {
      return Pattern.compile(cb.close(), flags);
    } catch (Exception e) {
      throw new ServletException(e);
    }
  }

  /**
   * Returns a printable representation of the filter config object.
   */
  public String toString()
  {
    return "FilterMapping[pattern=" + _urlPattern + ",name=" + getFilterName() + "]";
  }

  public class URLPattern {
    boolean _hasInclude = false;

    /**
     * Sets the singleton url-pattern.
     */
    public URLPattern addText(String pattern)
      throws ServletException
    {
      pattern = pattern.trim();
      
      _urlPattern = pattern;
      _urlPatterns.add(pattern);

      Pattern regexp;
      
      if (CauchoSystem.isCaseInsensitive())
        regexp = urlPatternToRegexp(pattern, Pattern.CASE_INSENSITIVE);
      else
        regexp = urlPatternToRegexp(pattern, 0);

      _hasInclude = true;
      
      _matchList.add(Match.createInclude(regexp));

      return this;
    }

    /**
     * Adds an include pattern.
     */
    public void addIncludePattern(String pattern)
      throws ServletException
    {
      pattern = pattern.trim();

      Pattern regexp;
      
      if (CauchoSystem.isCaseInsensitive())
        regexp = urlPatternToRegexp(pattern, Pattern.CASE_INSENSITIVE);
      else
        regexp = urlPatternToRegexp(pattern, 0);

      _hasInclude = true;
      
      _matchList.add(Match.createInclude(regexp));
    }

    /**
     * Adds an exclude pattern.
     */
    public void addExcludePattern(String pattern)
      throws ServletException
    {
      pattern = pattern.trim();
      
      Pattern regexp;
      
      if (CauchoSystem.isCaseInsensitive())
        regexp = urlPatternToRegexp(pattern, Pattern.CASE_INSENSITIVE);
      else
        regexp = urlPatternToRegexp(pattern, 0);

      _matchList.add(Match.createExclude(regexp));
    }

    /**
     * Adds an include regexp.
     */
    public void addIncludeRegexp(String pattern)
    {
      pattern = pattern.trim();

      Pattern regexp;
      
      if (CauchoSystem.isCaseInsensitive())
        regexp = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
      else
        regexp = Pattern.compile(pattern, 0);

      _hasInclude = true;
      
      _matchList.add(Match.createInclude(regexp));
    }

    /**
     * Adds an exclude regexp.
     */
    public void addExcludeRegexp(String pattern)
    {
      pattern = pattern.trim();

      Pattern regexp;
      
      if (CauchoSystem.isCaseInsensitive())
        regexp = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
      else
        regexp = Pattern.compile(pattern, 0);

      _matchList.add(Match.createExclude(regexp));
    }

    /**
     * Initialize, adding the all-match for exclude patterns.
     */
    @PostConstruct
    public void init()
      throws Exception
    {
      if (_matchList.size() > 0 && ! _hasInclude) {
        Pattern regexp = Pattern.compile("");
        _matchList.add(Match.createInclude(regexp));
      }
    }
  }
    
  static class Match {
    static final int INCLUDE = 1;
    static final int EXCLUDE = -1;
    static final int NO_MATCH = 0;
    
    private final Pattern _regexp;
    private final int _value;

    private Match(Pattern regexp, int value)
    {
      _regexp = regexp;
      _value = value;
    }

    /**
     * Creates an include pattern.
     */
    static Match createInclude(Pattern regexp)
    {
      return new Match(regexp, INCLUDE);
    }

    /**
     * Creates an exclude pattern.
     */
    static Match createExclude(Pattern regexp)
    {
      return new Match(regexp, EXCLUDE);
    }

    /**
     * Returns the match value.
     */
    int match(String uri)
    {
      if (_regexp.matcher(uri).find())
        return _value;
      else
        return NO_MATCH;
    }
  }
}
