/* *******************************************
// Copyright 2010-2013, Anthony Hand
//
// File version 2013.08.01 (August 1, 2013)
// Updates:
// - Updated DetectMobileQuick(). Moved the 'Exclude Tablets' logic to the top of the method to fix a logic bug.
//
// File version 2013.07.13 (July 13, 2013)
// Updates:
// - Added support for Tizen: variable and DetectTizen().
// - Added support for Meego: variable and DetectMeego().
// - Added support for Windows Phone 8: variable and DetectWindowsPhone8().
// - Added a generic Windows Phone method: DetectWindowsPhone().
// - Added support for BlackBerry 10 OS: variable and DetectBlackBerry10Phone().
// - Added support for PlayStation Vita handheld: variable and DetectGamingHandheld().
// - Updated DetectTierIphone(). Added Tizen; updated the Windows Phone, BB10, and PS Vita support.
// - Updated DetectWindowsMobile(). Uses generic DetectWindowsPhone() method rather than WP7.
// - Updated DetectSmartphone(). Uses the detectTierIphone() method.
// - Updated DetectSonyMylo() with more efficient code.
// - Removed DetectGarminNuvifone() from DetectTierIphone(). How many are left in market in 2013? It is detected as a RichCSS Tier device.
// - Removed the deviceXoom variable. It was unused.
// - Added detection support for the Obigo mobile browser to DetectMobileQuick().
//
//
//
// LICENSE INFORMATION
// 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.
//
//
// ABOUT THIS PROJECT
//   Project Owner: Anthony Hand
//   Email: anthony.hand@gmail.com
//   Web Site: http://www.mobileesp.com
//   Source Files: http://code.google.com/p/mobileesp/
//
//   Versions of this code are available for:
//      PHP, JavaScript, Java, ASP.NET (C#), and Ruby
//
// *******************************************
 */
package org.ocpsoft.rewrite.servlet.config;

/**
 * The DetectSmartPhone class encapsulates information about a browser's connection to your web site. You can use it to
 * find out whether the browser asking for your site's content is probably running on a mobile device. The methods were
 * written so you can be as granular as you want. For example, enquiring whether it's as specific as an iPod Touch or as
 * general as a smartphone class device. The object's methods return true, or false.
 */
public class UserAgentUtil
{
   // User-Agent and Accept HTTP request headers

   private String userAgent = "";
   private String httpAccept = "";

   // Let's store values for quickly accessing the same info multiple times.
   public boolean initCompleted = false;
   public boolean isWebkit = false; // Stores the result of DetectWebkit()
   public boolean isMobilePhone = false; // Stores the result of DetectMobileQuick()
   public boolean isIphone = false; // Stores the result of DetectIphone()
   public boolean isAndroid = false; // Stores the result of DetectAndroid()
   public boolean isAndroidPhone = false; // Stores the result of DetectAndroidPhone()
   public boolean isTierTablet = false; // Stores the result of DetectTierTablet()
   public boolean isTierIphone = false; // Stores the result of DetectTierIphone()
   public boolean isTierRichCss = false; // Stores the result of DetectTierRichCss()
   public boolean isTierGenericMobile = false; // Stores the result of DetectTierOtherPhones()

   // Initialize some initial smartphone string variables.
   public static final String engineWebKit = "webkit";

   public static final String deviceIphone = "iphone";
   public static final String deviceIpod = "ipod";
   public static final String deviceIpad = "ipad";
   public static final String deviceMacPpc = "macintosh"; // Used for disambiguation

   public static final String deviceAndroid = "android";
   public static final String deviceGoogleTV = "googletv";
   public static final String deviceHtcFlyer = "htc_flyer"; // HTC Flyer

   public static final String deviceWinPhone7 = "windows phone os 7";
   public static final String deviceWinPhone8 = "windows phone 8";
   public static final String deviceWinMob = "windows ce";
   public static final String deviceWindows = "windows";
   public static final String deviceIeMob = "iemobile";
   public static final String devicePpc = "ppc"; // Stands for PocketPC
   public static final String enginePie = "wm5 pie"; // An old Windows Mobile

   public static final String deviceBB = "blackberry";
   public static final String deviceBB10 = "bb10"; // For the new BB 10 OS
   public static final String vndRIM = "vnd.rim"; // Detectable when BB devices emulate IE or Firefox
   public static final String deviceBBStorm = "blackberry95"; // Storm 1 and 2
   public static final String deviceBBBold = "blackberry97"; // Bold 97x0 (non-touch)
   public static final String deviceBBBoldTouch = "blackberry 99"; // Bold 99x0 (touchscreen)
   public static final String deviceBBTour = "blackberry96"; // Tour
   public static final String deviceBBCurve = "blackberry89"; // Curve 2
   public static final String deviceBBCurveTouch = "blackberry 938"; // Curve Touch 9380
   public static final String deviceBBTorch = "blackberry 98"; // Torch
   public static final String deviceBBPlaybook = "playbook"; // PlayBook tablet

   public static final String deviceSymbian = "symbian";
   public static final String deviceS60 = "series60";
   public static final String deviceS70 = "series70";
   public static final String deviceS80 = "series80";
   public static final String deviceS90 = "series90";

   public static final String devicePalm = "palm";
   public static final String deviceWebOS = "webos"; // For Palm's line of WebOS devices
   public static final String deviceWebOShp = "hpwos"; // For HP's line of WebOS devices
   public static final String engineBlazer = "blazer"; // Old Palm
   public static final String engineXiino = "xiino"; // Another old Palm

   public static final String deviceNuvifone = "nuvifone"; // Garmin Nuvifone
   public static final String deviceBada = "bada"; // Samsung's Bada OS
   public static final String deviceTizen = "tizen"; // Tizen OS
   public static final String deviceMeego = "meego"; // Meego OS

   public static final String deviceKindle = "kindle"; // Amazon Kindle, eInk one
   public static final String engineSilk = "silk-accelerated"; // Amazon's accelerated Silk browser for Kindle Fire

   // Initialize variables for mobile-specific content.
   public static final String vndwap = "vnd.wap";
   public static final String wml = "wml";

   // Initialize variables for other random devices and mobile browsers.
   public static final String deviceTablet = "tablet"; // Generic term for slate and tablet devices
   public static final String deviceBrew = "brew";
   public static final String deviceDanger = "danger";
   public static final String deviceHiptop = "hiptop";
   public static final String devicePlaystation = "playstation";
   public static final String devicePlaystationVita = "vita";
   public static final String deviceNintendoDs = "nitro";
   public static final String deviceNintendo = "nintendo";
   public static final String deviceWii = "wii";
   public static final String deviceXbox = "xbox";
   public static final String deviceArchos = "archos";

   public static final String engineOpera = "opera"; // Popular browser
   public static final String engineNetfront = "netfront"; // Common embedded OS browser
   public static final String engineUpBrowser = "up.browser"; // common on some phones
   public static final String engineOpenWeb = "openweb"; // Transcoding by OpenWave server
   public static final String deviceMidp = "midp"; // a mobile Java technology
   public static final String uplink = "up.link";
   public static final String engineTelecaQ = "teleca q"; // a modern feature phone browser
   public static final String engineObigo = "obigo"; // W 10 is a modern feature phone browser

   public static final String devicePda = "pda"; // some devices report themselves as PDAs
   public static final String mini = "mini"; // Some mobile browsers put "mini" in their names.
   public static final String mobile = "mobile"; // Some mobile browsers put "mobile" in their user agent strings.
   public static final String mobi = "mobi"; // Some mobile browsers put "mobi" in their user agent strings.

   // Use Maemo, Tablet, and Linux to test for Nokia"s Internet Tablets.
   public static final String maemo = "maemo";
   public static final String linux = "linux";
   public static final String qtembedded = "qt embedded"; // for Sony Mylo
   public static final String mylocom2 = "com2"; // for Sony Mylo also

   // In some UserAgents, the only clue is the manufacturer.
   public static final String manuSonyEricsson = "sonyericsson";
   public static final String manuericsson = "ericsson";
   public static final String manuSamsung1 = "sec-sgh";
   public static final String manuSony = "sony";
   public static final String manuHtc = "htc";

   // In some UserAgents, the only clue is the operator.
   public static final String svcDocomo = "docomo";
   public static final String svcKddi = "kddi";
   public static final String svcVodafone = "vodafone";

   // Disambiguation strings.
   public static final String disUpdate = "update"; // pda vs. update

   /**
    * Initialize the userAgent and httpAccept variables
    *
    * @param userAgent the User-Agent header
    * @param httpAccept the Accept header
    */
   public UserAgentUtil(String userAgent, String httpAccept)
   {
      if (userAgent != null) {
         this.userAgent = userAgent.toLowerCase();
      }
      if (httpAccept != null) {
         this.httpAccept = httpAccept.toLowerCase();
      }

      // Intialize key stored values.
      initDeviceScan();
   }

   /**
    * Return the lower case HTTP_USER_AGENT
    *
    * @return userAgent
    */
   public String getUserAgent()
   {
      return userAgent;
   }

   /**
    * Return the lower case HTTP_ACCEPT
    *
    * @return httpAccept
    */
   public String getHttpAccept()
   {
      return httpAccept;
   }

   /**
    * Return whether the device is an Iphone or iPod Touch
    *
    * @return isIphone
    */
   public boolean getIsIphone()
   {
      return isIphone;
   }

   /**
    * Return whether the device is in the Tablet Tier.
    *
    * @return isTierTablet
    */
   public boolean getIsTierTablet()
   {
      return isTierTablet;
   }

   /**
    * Return whether the device is in the Iphone Tier.
    *
    * @return isTierIphone
    */
   public boolean getIsTierIphone()
   {
      return isTierIphone;
   }

   /**
    * Return whether the device is in the 'Rich CSS' tier of mobile devices.
    *
    * @return isTierRichCss
    */
   public boolean getIsTierRichCss()
   {
      return isTierRichCss;
   }

   /**
    * Return whether the device is a generic, less-capable mobile device.
    *
    * @return isTierGenericMobile
    */
   public boolean getIsTierGenericMobile()
   {
      return isTierGenericMobile;
   }

   /**
    * Initialize Key Stored Values.
    */
   public void initDeviceScan()
   {
      // Save these properties to speed processing
      this.isWebkit = detectWebkit();
      this.isIphone = detectIphone();
      this.isAndroid = detectAndroid();
      this.isAndroidPhone = detectAndroidPhone();

      // Generally, these tiers are the most useful for web development
      this.isMobilePhone = detectMobileQuick();
      this.isTierTablet = detectTierTablet();
      this.isTierIphone = detectTierIphone();

      // Optional: Comment these out if you NEVER use them
      this.isTierRichCss = detectTierRichCss();
      this.isTierGenericMobile = detectTierOtherPhones();

      this.initCompleted = true;
   }

   /**
    * Detects if the current device is an iPhone.
    *
    * @return detection of an iPhone
    */
   public boolean detectIphone()
   {
      if ((this.initCompleted == true) ||
               (this.isIphone == true))
         return this.isIphone;

      // The iPad and iPod touch say they're an iPhone! So let's disambiguate.
      if (userAgent.indexOf(deviceIphone) != -1 &&
               !detectIpad() &&
               !detectIpod()) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is an iPod Touch.
    *
    * @return detection of an iPod Touch
    */
   public boolean detectIpod()
   {
      if (userAgent.indexOf(deviceIpod) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is an iPad tablet.
    *
    * @return detection of an iPad
    */
   public boolean detectIpad()
   {
      if (userAgent.indexOf(deviceIpad) != -1
               && detectWebkit()) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is an iPhone or iPod Touch.
    *
    * @return detection of an iPhone or iPod Touch
    */
   public boolean detectIphoneOrIpod()
   {
      // We repeat the searches here because some iPods may report themselves as an iPhone, which would be okay.
      if (userAgent.indexOf(deviceIphone) != -1
               || userAgent.indexOf(deviceIpod) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects *any* iOS device: iPhone, iPod Touch, iPad.
    *
    * @return detection of an Apple iOS device
    */
   public boolean detectIos()
   {
      if (detectIphoneOrIpod() || detectIpad()) {
         return true;
      }
      return false;
   }

   /**
    * Detects *any* Android OS-based device: phone, tablet, and multi-media player. Also detects Google TV.
    *
    * @return detection of an Android device
    */
   public boolean detectAndroid()
   {
      if ((this.initCompleted == true) ||
               (this.isAndroid == true))
         return this.isAndroid;

      if ((userAgent.indexOf(deviceAndroid) != -1) ||
               detectGoogleTV())
         return true;
      // Special check for the HTC Flyer 7" tablet. It should report here.
      if (userAgent.indexOf(deviceHtcFlyer) != -1)
         return true;
      return false;
   }

   /**
    * Detects if the current device is a (small-ish) Android OS-based device used for calling and/or multi-media (like a
    * Samsung Galaxy Player). Google says these devices will have 'Android' AND 'mobile' in user agent. Ignores tablets
    * (Honeycomb and later).
    *
    * @return detection of an Android phone
    */
   public boolean detectAndroidPhone()
   {
      if ((this.initCompleted == true) ||
               (this.isAndroidPhone == true))
         return this.isAndroidPhone;

      if (detectAndroid() && (userAgent.indexOf(mobile) != -1))
         return true;
      // Special check for Android phones with Opera Mobile. They should report here.
      if (detectOperaAndroidPhone())
         return true;
      // Special check for the HTC Flyer 7" tablet. It should report here.
      if (userAgent.indexOf(deviceHtcFlyer) != -1)
         return true;
      return false;
   }

   /**
    * Detects if the current device is a (self-reported) Android tablet. Google says these devices will have 'Android'
    * and NOT 'mobile' in their user agent.
    *
    * @return detection of an Android tablet
    */
   public boolean detectAndroidTablet()
   {
      // First, let's make sure we're on an Android device.
      if (!detectAndroid())
         return false;

      // Special check for Opera Android Phones. They should NOT report here.
      if (detectOperaMobile())
         return false;
      // Special check for the HTC Flyer 7" tablet. It should NOT report here.
      if (userAgent.indexOf(deviceHtcFlyer) != -1)
         return false;

      // Otherwise, if it's Android and does NOT have 'mobile' in it, Google says it's a tablet.
      if ((userAgent.indexOf(mobile) > -1))
         return false;
      else
         return true;
   }

   /**
    * Detects if the current device is an Android OS-based device and the browser is based on WebKit.
    *
    * @return detection of an Android WebKit browser
    */
   public boolean detectAndroidWebKit()
   {
      if (detectAndroid() && detectWebkit()) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is a GoogleTV.
    *
    * @return detection of GoogleTV
    */
   public boolean detectGoogleTV()
   {
      if (userAgent.indexOf(deviceGoogleTV) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is based on WebKit.
    *
    * @return detection of a WebKit browser
    */
   public boolean detectWebkit()
   {
      if ((this.initCompleted == true) ||
               (this.isWebkit == true))
         return this.isWebkit;

      if (userAgent.indexOf(engineWebKit) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is EITHER a Windows Phone 7.x OR 8 device
    *
    * @return detection of Windows Phone 7.x OR 8
    */
   public boolean detectWindowsPhone()
   {
      if (detectWindowsPhone7() || detectWindowsPhone8()) {
         return true;
      }
      return false;
   }

   /**
    * Detects a Windows Phone 7.x device (in mobile browsing mode).
    *
    * @return detection of Windows Phone 7
    */
   public boolean detectWindowsPhone7()
   {
      if (userAgent.indexOf(deviceWinPhone7) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects a Windows Phone 8 device (in mobile browsing mode).
    *
    * @return detection of Windows Phone 8
    */
   public boolean detectWindowsPhone8()
   {
      if (userAgent.indexOf(deviceWinPhone8) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is a Windows Mobile device. Excludes Windows Phone 7.x and 8 devices. Focuses on
    * Windows Mobile 6.xx and earlier.
    *
    * @return detection of Windows Mobile
    */
   public boolean detectWindowsMobile()
   {
      if (detectWindowsPhone()) {
         return false;
      }
      // Most devices use 'Windows CE', but some report 'iemobile'
      // and some older ones report as 'PIE' for Pocket IE.
      // We also look for instances of HTC and Windows for many of their WinMo devices.
      if (userAgent.indexOf(deviceWinMob) != -1
               || userAgent.indexOf(deviceWinMob) != -1
               || userAgent.indexOf(deviceIeMob) != -1
               || userAgent.indexOf(enginePie) != -1
               || (userAgent.indexOf(manuHtc) != -1 && userAgent.indexOf(deviceWindows) != -1)
               || (detectWapWml() && userAgent.indexOf(deviceWindows) != -1)) {
         return true;
      }

      // Test for Windows Mobile PPC but not old Macintosh PowerPC.
      if (userAgent.indexOf(devicePpc) != -1 &&
               !(userAgent.indexOf(deviceMacPpc) != -1))
         return true;

      return false;
   }

   /**
    * Detects if the current browser is any BlackBerry. Includes BB10 OS, but excludes the PlayBook.
    *
    * @return detection of Blackberry
    */
   public boolean detectBlackBerry()
   {
      if (userAgent.indexOf(deviceBB) != -1 ||
               httpAccept.indexOf(vndRIM) != -1)
         return true;

      if (detectBlackBerry10Phone())
         return true;

      return false;
   }

   /**
    * Detects if the current browser is a BlackBerry 10 OS phone. Excludes tablets.
    *
    * @return detection of a Blackberry 10 device
    */
   public boolean detectBlackBerry10Phone()
   {
      if (userAgent.indexOf(deviceBB10) != -1 &&
               userAgent.indexOf(mobile) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is on a BlackBerry tablet device. Example: PlayBook
    *
    * @return detection of a Blackberry Tablet
    */
   public boolean detectBlackBerryTablet()
   {
      if (userAgent.indexOf(deviceBBPlaybook) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is a BlackBerry device AND uses a WebKit-based browser. These are signatures for
    * the new BlackBerry OS 6. Examples: Torch. Includes the Playbook.
    *
    * @return detection of a Blackberry device with WebKit browser
    */
   public boolean detectBlackBerryWebKit()
   {
      if (detectBlackBerry() && detectWebkit())
         return true;
      return false;
   }

   /**
    * Detects if the current browser is a BlackBerry Touch device, such as the Storm, Torch, and Bold Touch. Excludes
    * the Playbook.
    *
    * @return detection of a Blackberry touchscreen device
    */
   public boolean detectBlackBerryTouch()
   {
      if (detectBlackBerry() &&
               (userAgent.indexOf(deviceBBStorm) != -1 ||
                        userAgent.indexOf(deviceBBTorch) != -1 ||
                        userAgent.indexOf(deviceBBBoldTouch) != -1 ||
               userAgent.indexOf(deviceBBCurveTouch) != -1)) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is a BlackBerry device AND has a more capable recent browser. Excludes the
    * Playbook. Examples, Storm, Bold, Tour, Curve2 Excludes the new BlackBerry OS 6 and 7 browser!!
    *
    * @return detection of a Blackberry device with a better browser
    */
   public boolean detectBlackBerryHigh()
   {
      // Disambiguate for BlackBerry OS 6 or 7 (WebKit) browser
      if (detectBlackBerryWebKit())
         return false;
      if (detectBlackBerry()) {
         if (detectBlackBerryTouch()
                  || userAgent.indexOf(deviceBBBold) != -1
                  || userAgent.indexOf(deviceBBTour) != -1
                  || userAgent.indexOf(deviceBBCurve) != -1) {
            return true;
         }
         else {
            return false;
         }
      }
      else {
         return false;
      }
   }

   /**
    * Detects if the current browser is a BlackBerry device AND has an older, less capable browser. Examples: Pearl,
    * 8800, Curve1
    *
    * @return detection of a Blackberry device with a poorer browser
    */
   public boolean detectBlackBerryLow()
   {
      if (detectBlackBerry()) {
         // Assume that if it's not in the High tier, then it's Low
         if (detectBlackBerryHigh()
                  || detectBlackBerryWebKit()) {
            return false;
         }
         else {
            return true;
         }
      }
      else {
         return false;
      }
   }

   /**
    * Detects if the current browser is the Symbian S60 Open Source Browser.
    *
    * @return detection of Symbian S60 Browser
    */
   public boolean detectS60OssBrowser()
   {
      // First, test for WebKit, then make sure it's either Symbian or S60.
      if (detectWebkit()
               && (userAgent.indexOf(deviceSymbian) != -1
               || userAgent.indexOf(deviceS60) != -1)) {
         return true;
      }
      return false;
   }

   /**
    *
    * Detects if the current device is any Symbian OS-based device, including older S60, Series 70, Series 80, Series
    * 90, and UIQ, or other browsers running on these devices.
    *
    * @return detection of SymbianOS
    */
   public boolean detectSymbianOS()
   {
      if (userAgent.indexOf(deviceSymbian) != -1
               || userAgent.indexOf(deviceS60) != -1
               || userAgent.indexOf(deviceS70) != -1
               || userAgent.indexOf(deviceS80) != -1
               || userAgent.indexOf(deviceS90) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is on a PalmOS device.
    *
    * @return detection of a PalmOS device
    */
   public boolean detectPalmOS()
   {
      // Make sure it's not WebOS first
      if (detectPalmWebOS())
         return false;

      // Most devices nowadays report as 'Palm', but some older ones reported as Blazer or Xiino.
      if (userAgent.indexOf(devicePalm) != -1
               || userAgent.indexOf(engineBlazer) != -1
               || userAgent.indexOf(engineXiino) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is on a Palm device running the new WebOS.
    *
    * @return detection of a Palm WebOS device
    */
   public boolean detectPalmWebOS()
   {
      if (userAgent.indexOf(deviceWebOS) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is on an HP tablet running WebOS.
    *
    * @return detection of an HP WebOS tablet
    */
   public boolean detectWebOSTablet()
   {
      if (userAgent.indexOf(deviceWebOShp) != -1 &&
               userAgent.indexOf(deviceTablet) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects Opera Mobile or Opera Mini.
    *
    * @return detection of an Opera browser for a mobile device
    */
   public boolean detectOperaMobile()
   {
      if (userAgent.indexOf(engineOpera) != -1
               && (userAgent.indexOf(mini) != -1
               || userAgent.indexOf(mobi) != -1)) {
         return true;
      }
      return false;
   }

   /**
    * Detects Opera Mobile on an Android phone.
    *
    * @return detection of an Opera browser on an Android phone
    */
   public boolean detectOperaAndroidPhone()
   {
      if (userAgent.indexOf(engineOpera) != -1
               && (userAgent.indexOf(deviceAndroid) != -1
               && userAgent.indexOf(mobi) != -1)) {
         return true;
      }
      return false;
   }

   /**
    * Detects Opera Mobile on an Android tablet.
    *
    * @return detection of an Opera browser on an Android tablet
    */
   public boolean detectOperaAndroidTablet()
   {
      if (userAgent.indexOf(engineOpera) != -1
               && (userAgent.indexOf(deviceAndroid) != -1
               && userAgent.indexOf(deviceTablet) != -1)) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is an Amazon Kindle (eInk devices only). Note: For the Kindle Fire, use the normal
    * Android methods.
    *
    * @return detection of a Kindle
    */
   public boolean detectKindle()
   {
      if (userAgent.indexOf(deviceKindle) != -1 &&
               !detectAndroid()) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current Amazon device is using the Silk Browser. Note: Typically used by the the Kindle Fire.
    *
    * @return detection of an Amazon Kindle Fire in Silk mode.
    */
   public boolean detectAmazonSilk()
   {
      if (userAgent.indexOf(engineSilk) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is a Garmin Nuvifone.
    *
    * @return detection of a Garmin Nuvifone
    */
   public boolean detectGarminNuvifone()
   {
      if (userAgent.indexOf(deviceNuvifone) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects a device running the Bada smartphone OS from Samsung.
    *
    * @return detection of a Bada device
    */
   public boolean detectBada()
   {
      if (userAgent.indexOf(deviceBada) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects a device running the Tizen smartphone OS.
    *
    * @return detection of a Tizen device
    */
   public boolean detectTizen()
   {
      if (userAgent.indexOf(deviceTizen) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects a device running the Meego OS.
    *
    * @return detection of a Meego device
    */
   public boolean detectMeego()
   {
      if (userAgent.indexOf(deviceMeego) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects the Danger Hiptop device.
    *
    * @return detection of a Danger Hiptop
    */
   public boolean detectDangerHiptop()
   {
      if (userAgent.indexOf(deviceDanger) != -1
               || userAgent.indexOf(deviceHiptop) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current browser is a Sony Mylo device.
    *
    * @return detection of a Sony Mylo device
    */
   public boolean detectSonyMylo()
   {
      if (userAgent.indexOf(manuSony) != -1
               && (userAgent.indexOf(qtembedded) != -1
               || userAgent.indexOf(mylocom2) != -1)) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is on one of the Maemo-based Nokia Internet Tablets.
    *
    * @return detection of a Maemo OS tablet
    */
   public boolean detectMaemoTablet()
   {
      if (userAgent.indexOf(maemo) != -1) {
         return true;
      }
      else if (userAgent.indexOf(linux) != -1
               && userAgent.indexOf(deviceTablet) != -1
               && !detectWebOSTablet()
               && !detectAndroid()) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is an Archos media player/Internet tablet.
    *
    * @return detection of an Archos media player
    */
   public boolean detectArchos()
   {
      if (userAgent.indexOf(deviceArchos) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is an Internet-capable game console. Includes many handheld consoles.
    *
    * @return detection of any Game Console
    */
   public boolean detectGameConsole()
   {
      if (detectSonyPlaystation()
               || detectNintendo()
               || detectXbox()) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is a Sony Playstation.
    *
    * @return detection of Sony Playstation
    */
   public boolean detectSonyPlaystation()
   {
      if (userAgent.indexOf(devicePlaystation) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is a handheld gaming device with a touchscreen and modern iPhone-class browser.
    * Includes the Playstation Vita.
    *
    * @return detection of a handheld gaming device
    */
   public boolean detectGamingHandheld()
   {
      if ((userAgent.indexOf(devicePlaystation) != -1) &&
               (userAgent.indexOf(devicePlaystationVita) != -1)) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is a Nintendo game device.
    *
    * @return detection of Nintendo
    */
   public boolean detectNintendo()
   {
      if (userAgent.indexOf(deviceNintendo) != -1
               || userAgent.indexOf(deviceWii) != -1
               || userAgent.indexOf(deviceNintendoDs) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device is a Microsoft Xbox.
    *
    * @return detection of Xbox
    */
   public boolean detectXbox()
   {
      if (userAgent.indexOf(deviceXbox) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects whether the device is a Brew-powered device.
    *
    * @return detection of a Brew device
    */
   public boolean detectBrewDevice()
   {
      if (userAgent.indexOf(deviceBrew) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects whether the device supports WAP or WML.
    *
    * @return detection of a WAP- or WML-capable device
    */
   public boolean detectWapWml()
   {
      if (httpAccept.indexOf(vndwap) != -1
               || httpAccept.indexOf(wml) != -1) {
         return true;
      }
      return false;
   }

   /**
    * Detects if the current device supports MIDP, a mobile Java technology.
    *
    * @return detection of a MIDP mobile Java-capable device
    */
   public boolean detectMidpCapable()
   {
      if (userAgent.indexOf(deviceMidp) != -1
               || httpAccept.indexOf(deviceMidp) != -1) {
         return true;
      }
      return false;
   }

   // *****************************
   // Device Classes
   // *****************************

   /**
    * Check to see whether the device is any device in the 'smartphone' category.
    *
    * @return detection of a general smartphone device
    */
   public boolean detectSmartphone()
   {
      // Exclude duplicates from TierIphone
      return (detectTierIphone()
               || detectS60OssBrowser()
               || detectSymbianOS()
               || detectWindowsMobile()
               || detectBlackBerry()
               || detectPalmOS());
   }

   /**
    * Detects if the current device is a mobile device. This method catches most of the popular modern devices. Excludes
    * Apple iPads and other modern tablets.
    *
    * @return detection of any mobile device using the quicker method
    */
   public boolean detectMobileQuick()
   {
      // Let's exclude tablets
      if (detectTierTablet())
         return false;

      if ((initCompleted == true) ||
               (isMobilePhone == true))
         return isMobilePhone;

      // Most mobile browsing is done on smartphones
      if (detectSmartphone())
         return true;

      if (detectWapWml()
               || detectBrewDevice()
               || detectOperaMobile())
         return true;

      if ((userAgent.indexOf(engineObigo) != -1)
               || (userAgent.indexOf(engineNetfront) != -1)
               || (userAgent.indexOf(engineUpBrowser) != -1)
               || (userAgent.indexOf(engineOpenWeb) != -1))
         return true;

      if (detectDangerHiptop()
               || detectMidpCapable()
               || detectMaemoTablet()
               || detectArchos())
         return true;

      if ((userAgent.indexOf(devicePda) != -1) &&
               (userAgent.indexOf(disUpdate) < 0)) // no index found
         return true;

      if (userAgent.indexOf(mobile) != -1)
         return true;

      // We also look for Kindle devices
      if (detectKindle()
               || detectAmazonSilk())
         return true;

      return false;
   }

   /**
    * The longer and more thorough way to detect for a mobile device. Will probably detect most feature phones,
    * smartphone-class devices, Internet Tablets, Internet-enabled game consoles, etc. This ought to catch a lot of the
    * more obscure and older devices, also -- but no promises on thoroughness!
    *
    * @return detection of any mobile device using the more thorough method
    */
   public boolean detectMobileLong()
   {
      if (detectMobileQuick()
               || detectGameConsole()
               || detectSonyMylo()) {
         return true;
      }

      // detect older phones from certain manufacturers and operators.
      if (userAgent.indexOf(uplink) != -1)
         return true;
      if (userAgent.indexOf(manuSonyEricsson) != -1)
         return true;
      if (userAgent.indexOf(manuericsson) != -1)
         return true;
      if (userAgent.indexOf(manuSamsung1) != -1)
         return true;

      if (userAgent.indexOf(svcDocomo) != -1)
         return true;
      if (userAgent.indexOf(svcKddi) != -1)
         return true;
      if (userAgent.indexOf(svcVodafone) != -1)
         return true;

      return false;
   }

   // *****************************
   // For Mobile Web Site Design
   // *****************************

   /**
    * The quick way to detect for a tier of devices. This method detects for the new generation of HTML 5 capable,
    * larger screen tablets. Includes iPad, Android (e.g., Xoom), BB Playbook, WebOS, etc.
    *
    * @return detection of any device in the Tablet Tier
    */
   public boolean detectTierTablet()
   {
      if ((this.initCompleted == true) ||
               (this.isTierTablet == true))
         return this.isTierTablet;

      if (detectIpad()
               || detectAndroidTablet()
               || detectBlackBerryTablet()
               || detectWebOSTablet()) {
         return true;
      }
      return false;
   }

   /**
    * The quick way to detect for a tier of devices. This method detects for devices which can display iPhone-optimized
    * web content. Includes iPhone, iPod Touch, Android, Windows Phone 7 and 8, BB10, WebOS, Playstation Vita, etc.
    *
    * @return detection of any device in the iPhone/Android/Windows Phone/BlackBerry/WebOS Tier
    */
   public boolean detectTierIphone()
   {
      if ((this.initCompleted == true) ||
               (this.isTierIphone == true))
         return this.isTierIphone;

      if (detectIphoneOrIpod()
               || detectAndroidPhone()
               || detectWindowsPhone()
               || detectBlackBerry10Phone()
               || (detectBlackBerryWebKit()
               && detectBlackBerryTouch())
               || detectPalmWebOS()
               || detectBada()
               || detectTizen()
               || detectGamingHandheld()) {
         return true;
      }
      return false;
   }

   /**
    * The quick way to detect for a tier of devices. This method detects for devices which are likely to be capable of
    * viewing CSS content optimized for the iPhone, but may not necessarily support JavaScript. Excludes all iPhone Tier
    * devices.
    *
    * @return detection of any device in the 'Rich CSS' Tier
    */
   public boolean detectTierRichCss()
   {
      if ((this.initCompleted == true) ||
               (this.isTierRichCss == true))
         return this.isTierRichCss;

      boolean result = false;

      // The following devices are explicitly ok.
      // Note: 'High' BlackBerry devices ONLY
      if (detectMobileQuick()) {

         // Exclude iPhone Tier and e-Ink Kindle devices.
         if (!detectTierIphone() && !detectKindle()) {

            // The following devices are explicitly ok.
            // Note: 'High' BlackBerry devices ONLY
            // Older Windows 'Mobile' isn't good enough for iPhone Tier.
            if (detectWebkit()
                     || detectS60OssBrowser()
                     || detectBlackBerryHigh()
                     || detectWindowsMobile()
                     || userAgent.indexOf(engineTelecaQ) != -1) {
               result = true;
            }
         }
      }
      return result;
   }

   /**
    * The quick way to detect for a tier of devices. This method detects for all other types of phones, but excludes the
    * iPhone and RichCSS Tier devices.
    *
    * @return detection of a mobile device in the less capable tier
    */
   public boolean detectTierOtherPhones()
   {
      if ((this.initCompleted == true) ||
               (this.isTierGenericMobile == true))
         return this.isTierGenericMobile;

      // Exclude devices in the other 2 categories
      if (detectMobileLong()
               && !detectTierIphone()
               && !detectTierRichCss())
         return true;

      return false;
   }
}


/*
 * Copyright 2010 Lincoln Baxter, III
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ocpsoft.pretty.faces.util;

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

import javax.faces.component.UIComponent;
import javax.faces.component.UIParameter;

import com.ocpsoft.pretty.faces.config.mapping.QueryParameter;
import com.ocpsoft.pretty.faces.config.mapping.RequestParameter;
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;

/**
 * A utility class for building Pretty URLs.
 *
 * @author Lincoln Baxter, III <lincoln@ocpsoft.com>
 */
public class PrettyURLBuilder
{
   /**
    * Extract any {@link UIParameter} objects from a given component. These
    * parameters are what PrettyFaces uses to communicate with the JSF component
    * tree
    *
    * @param component
    * @return A list of {@link UIParameter} objects, or an empty list if no
    *         {@link UIParameter}s were contained within the component children.
    */
   public List<UIParameter> extractParameters(final UIComponent component)
   {
      List<UIParameter> results = new ArrayList<UIParameter>();
      for (UIComponent child : component.getChildren())
      {
         if (child instanceof UIParameter)
         {
            final UIParameter param = (UIParameter) child;
            if (!param.isDisable())
            {
               results.add(param);
            }
         }
      }
      return results;
   }

   /**
    * Build a Pretty URL for the given UrlMapping and parameters.
    *
    * @deprecated Use {@link #build(UrlMapping, boolean, Map)} instead.
    */
   @Deprecated
   public String build(final UrlMapping mapping, final Map<String, String[]> parameters)
   {
      return build(mapping, false, parameters);
   }

   /**
    * Build a Pretty URL for the given UrlMapping and parameters.
    *
    * @since 3.2.0
    */
   public String build(final UrlMapping mapping, final boolean encodeUrl, final Map<String, String[]> parameters)
   {
      List<UIParameter> list = new ArrayList<UIParameter>();
      if(parameters != null)
      {
         for (Entry<String, String[]> e : parameters.entrySet())
         {
            UIParameter p = new UIParameter();
            p.setName(e.getKey());
            p.setValue(e.getValue());
            list.add(p);
         }
      }
      return build(mapping, false, list);
   }

   /**
    * Build a Pretty URL for the given UrlMapping and parameters.
    *
    * @deprecated Use {@link #build(UrlMapping, boolean, Object...)} instead.
    */
   @Deprecated
   public String build(final UrlMapping mapping, final Object... parameters)
   {
      return build(mapping, false, parameters);
   }

   /**
    * Build a Pretty URL for the given UrlMapping and parameters.
    *
    * @since 3.2.0
    */
   public String build(final UrlMapping mapping, final boolean encodeUrl, final Object... parameters)
   {
      List<UIParameter> list = new ArrayList<UIParameter>();
      if(parameters != null)
      {
         for (Object e : parameters)
         {
            UIParameter p = new UIParameter();
            if (e != null)
            {
               p.setValue(e.toString());
            }
            list.add(p);
         }
      }
      return build(mapping, encodeUrl, list);
   }

   /**
    * Build a Pretty URL for the given UrlMapping and parameters.
    *
    * @deprecated Use {@link #build(UrlMapping, boolean, RequestParameter...)} instead.
    */
   @Deprecated
   public String build(final UrlMapping mapping, final RequestParameter... parameters)
   {
      return build(mapping, false, parameters);
   }

   /**
    * Build a Pretty URL for the given UrlMapping and parameters.
    *
    * @since 3.2.0
    */
   public String build(final UrlMapping mapping, final boolean encodeUrl, final RequestParameter... parameters)
   {
      List<UIParameter> list = new ArrayList<UIParameter>();
      if(parameters != null)
      {
         for (RequestParameter param : parameters)
         {
            UIParameter p = new UIParameter();
            if (param != null)
            {
               p.setValue(param.getName());
               p.setValue(param.getValue());
            }
            list.add(p);
         }
      }
      return build(mapping, false, list);
   }

   /**
    * Build a Pretty URL for the given Mapping ID and parameters.
    *
    * @deprecated Use {@link #build(UrlMapping, boolean, List)} instead.
    */
   @Deprecated
   public String build(final UrlMapping urlMapping, final List<UIParameter> parameters)
   {
      return build(urlMapping, false, parameters);
   }

   /**
    * Build a Pretty URL for the given Mapping ID and parameters.
    *
    * @since 3.2.0
    */
   public String build(final UrlMapping urlMapping, final boolean encodeUrl, final List<UIParameter> parameters)
   {
      String result = "";
      if (urlMapping != null)
      {
         URLPatternParser parser = urlMapping.getPatternParser();
         List<String> pathParams = new ArrayList<String>();
         List<QueryParameter> queryParams = new ArrayList<QueryParameter>();

         if(parameters != null)
         {
            // TODO this logic should be in the components, not in the builder
            if (parameters.size() == 1)
            {
               UIParameter firstParam = parameters.get(0);
               if (((firstParam.getValue() != null)) && (firstParam.getName() == null))
               {
                  if (firstParam.getValue() instanceof List<?>)
                  {
                     URL url = parser.getMappedURL(firstParam.getValue());
                     return url.toURL();
                  }
                  else if (firstParam.getValue().getClass().isArray())
                  {
                     // The Object[] cast here is required, otherwise Java treats
                     // getValue() as a single Object.
                     List<Object> list = Arrays.asList((Object[]) firstParam.getValue());
                     URL url = parser.getMappedURL(list);
                     return url.toURL();
                  }
               }
            }

            for (UIParameter parameter : parameters)
            {
               String name = parameter.getName();
               Object value = parameter.getValue();

               if ((name == null) && (value != null))
               {
                  pathParams.add(value.toString());
               }
               else
               {
                  List<?> values = null;
                  if ((value != null) && value.getClass().isArray())
                  {
                     values = Arrays.asList((Object[]) value);
                  }
                  else if (value instanceof List<?>)
                  {
                     values = (List<?>) value;
                  }
                  else if (value != null)
                  {
                     values = Arrays.asList(value);
                  }

                  if (values != null)
                  {
                     for (Object object : values)
                     {
                        String tempValue = null;
                        if (object != null)
                        {
                           tempValue = object.toString();
                        }
                        queryParams.add(new QueryParameter(name, tempValue));
                     }
                  }
                  else
                  {
                     queryParams.add(new QueryParameter(name, null));
                  }
               }
            }
         }

         // build URL object for given path parameter set
         URL mappedURL = parser.getMappedURL(pathParams.toArray());

         // create encoded/unicode URL
         String url = encodeUrl ? mappedURL.encode().toURL() : mappedURL.toURL();

         // append query string
         result = url + QueryString.build(queryParams).toQueryString();

      }
      return result;
   }
}


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

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;

import com.ocpsoft.pretty.faces.el.processor.PathParameterProcessor;

public class Expressions
{
   public static final String EL_REGEX;

   static
   {
      List<PathParameterProcessor> processors = new ArrayList<PathParameterProcessor>();
      processors.addAll(ExpressionProcessorRunner.processors);
      processors.addAll(ExpressionProcessorRunner.preProcessors);

      String result = "(";
      for (Iterator<PathParameterProcessor> iter = processors.iterator(); iter.hasNext();)
      {
         PathParameterProcessor p = iter.next();
         if (p.getRegex() != null)
         {
            result += "(" + p.getRegex() + ")";
            if (iter.hasNext())
            {
               result += "|";
            }
         }
      }
      result += ")";
      EL_REGEX = result;
   }

   /**
    * The pattern to match a String EL expression. Regex Group 0 contains the
    * entire expression, no more.
    */
   public static final Pattern elPattern = Pattern.compile(EL_REGEX);

   /**
    * Return true if the value is an EL expression.
    */
   public static boolean isEL(final String value)
   {
      if (value == null)
      {
         return false;
      }
      return elPattern.matcher(value).matches();
   }

   /**
    * Return true if the value contains an EL expression.
    */
   public static boolean containsEL(final String value)
   {
      if (value == null)
      {
         return false;
      }

      return elPattern.matcher(value).find();
   }
}


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

import java.util.Map;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.ocpsoft.rewrite.event.Rewrite;
import org.ocpsoft.rewrite.servlet.spi.RequestParameterProvider;

/**
 * An {@link HttpServletRequestWrapper} for the {@link Rewrite} framework
 *
 * @author Lincoln Baxter, III
 */
public abstract class RewriteWrappedRequest extends HttpServletRequestWrapper
{
   /**
    * Get the current {@link RewriteWrappedRequest}
    */
   public static RewriteWrappedRequest getCurrentInstance(ServletRequest request)
   {
      RewriteWrappedRequest wrapper = (RewriteWrappedRequest) request.getAttribute(RewriteWrappedRequest.class
               .getName());
      return wrapper;
   }

   /**
    * Set the current {@link RewriteWrappedRequest}
    */
   protected static void setCurrentInstance(final RewriteWrappedRequest instance)
   {
      instance.setAttribute(RewriteWrappedRequest.class.getName(), instance);
   }

   /**
    * Create a new {@link RewriteWrappedRequest}
    */
   public RewriteWrappedRequest(HttpServletRequest request)
   {
      super(request);
   }

   /**
    * Get the current {@link Map} of modifiable {@link HttpServletRequest} parameters.
    *
    * @see {@link RequestParameterProvider}
    */
   abstract public Map<String, String[]> getModifiableParameters();
}


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

import java.util.Collections;
import java.util.List;

import org.ocpsoft.common.pattern.WeightedComparator;
import org.ocpsoft.common.services.ServiceLoader;
import org.ocpsoft.common.util.Iterators;
import org.ocpsoft.logging.Logger;
import org.ocpsoft.rewrite.spi.InstanceProvider;

/**
 * Utility class for looking up instances of Java types. See also {@link InstanceProvider}.
 *
 * @author Lincoln Baxter, III
 */
public final class Instances
{
   private static Logger log = Logger.getLogger(Instances.class);

   private static List<InstanceProvider> instanceProviders;

   private Instances()
   {}

   /**
    * Lookup an instance of the given {@link Class} type.
    */
   @SuppressWarnings("unchecked")
   public static <T> T lookup(Class<T> type)
   {
      if (instanceProviders == null) {
         instanceProviders = Iterators.asList(ServiceLoader.load(InstanceProvider.class));
         Collections.sort(instanceProviders, new WeightedComparator());
         ServiceLogger.logLoadedServices(log, InstanceProvider.class, instanceProviders);
      }

      T result = null;
      for (InstanceProvider p : instanceProviders) {
         result = (T) p.getInstance(type);
         if (result != null)
         {
            break;
         }
      }
      return result;
   }
}


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

import javax.faces.component.behavior.ClientBehaviorBase;
import javax.faces.component.behavior.ClientBehaviorContext;

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

import com.ocpsoft.pretty.PrettyContext;
import com.ocpsoft.pretty.PrettyException;
import com.ocpsoft.pretty.faces.beans.ExtractedValuesURLBuilder;
import com.ocpsoft.pretty.faces.config.mapping.UrlMapping;

/**
 * Simple JSF2 client behavior that changes the current URL by setting window.location.href.
 *
 * @author Christian Kaltepoth
 */
public class LocationBehavior extends ClientBehaviorBase
{

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

   private String mappingId;

   private String url;

   @Override
   public String getScript(final ClientBehaviorContext behaviorContext)
   {

      // use URL from url attribute if available
      if ((url != null) && (url.trim().length() > 0))
      {
         return buildScriptInternal(url.trim());
      }

      // Is there a mapping id?
      if ((mappingId == null) || (mappingId.trim().length() == 0))
      {
         log.error("Please set either 'mappingId' or 'url' attribute!");
         return null;
      }

      // try to obtain PrettyContext
      PrettyContext prettyContext = PrettyContext.getCurrentInstance(behaviorContext.getFacesContext());
      if (prettyContext == null)
      {
         log.error("Cannot build script because PrettyContext is not available!");
         return null;
      }

      // find UrlMapping in configuration
      UrlMapping mapping = prettyContext.getConfig().getMappingById(mappingId.trim());
      if (mapping == null)
      {
         log.error("Cannot find URL mapping with id: " + mappingId);
         return null;
      }

      try
      {

         // build path to redirect to
         ExtractedValuesURLBuilder builder = new ExtractedValuesURLBuilder();
         String contextPath = prettyContext.getContextPath();
         String path = contextPath + builder.buildURL(mapping) + builder.buildQueryString(mapping);

         // return the script
         return buildScriptInternal(path);

      }
      catch (PrettyException e)
      {
         log.error("Failed to build URL", e);
         return null;
      }
   }

   /**
    * Creates the required script for the supplied URL
    *
    * @param url The URL
    * @return The script
    */
   private String buildScriptInternal(final String url)
   {
      StringBuilder builder = new StringBuilder();
      builder.append("window.location.href = '");
      builder.append(url);
      builder.append("'; return false;");
      return builder.toString();
   }

   public String getMappingId()
   {
      return mappingId;
   }

   public void setMappingId(final String mappingId)
   {
      this.mappingId = mappingId;
   }

   public String getUrl()
   {
      return url;
   }

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

}


/*
 * Copyright 2011 Lincoln Baxter, III
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.ocpsoft.rewrite.param;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.ocpsoft.common.util.Assert;
import org.ocpsoft.rewrite.bind.Binding;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.event.Rewrite;
import org.ocpsoft.rewrite.exception.ParameterizationException;
import org.ocpsoft.rewrite.param.RegexParameterizedPatternParser.RegexGroup;
import org.ocpsoft.rewrite.util.ParameterUtils;
import org.ocpsoft.rewrite.util.ParseTools.CaptureType;
import org.ocpsoft.rewrite.util.ParseTools.CapturingGroup;

/**
 * An {@link org.ocpsoft.rewrite.param.Parameterized} regular expression {@link Pattern}.
 *
 * @author Lincoln Baxter, III
 */
public class RegexParameterizedPatternBuilder implements ParameterizedPatternBuilder
{

   private static final String DEFAULT_PARAMETER_PATTERN = ".*";
   private final String pattern;
   private final char[] chars;
   private final List<RegexGroup> groups = new ArrayList<RegexGroup>();
   private final String defaultParameterPattern;
   private ParameterStore store;

   private RegexParameterizedPatternParser parser = null;

   RegexParameterizedPatternBuilder(String pattern, RegexParameterizedPatternParser parser)
   {
      this(pattern);
      this.parser = parser;
   }

   /**
    * Create a new {@link RegexParameterizedPatternBuilder} instance with the default
    * {@link org.ocpsoft.rewrite.bind.parse.CaptureType#BRACE} and parameter compiledPattern of ".*".
    */
   public RegexParameterizedPatternBuilder(final String pattern)
   {
      this(CaptureType.BRACE, DEFAULT_PARAMETER_PATTERN, pattern);
   }

   /**
    * Create a new {@link RegexParameterizedPatternBuilder} instance with the default {@link CaptureType#BRACE}.
    */
   public RegexParameterizedPatternBuilder(final String parameterPattern, final String pattern)
   {
      this(CaptureType.BRACE, parameterPattern, pattern);
   }

   /**
    * Create a new {@link RegexParameterizedPatternBuilder} instance with the default parameter regex of ".*".
    */
   public RegexParameterizedPatternBuilder(final CaptureType type, final String pattern)
   {
      this(type, DEFAULT_PARAMETER_PATTERN, pattern);
   }

   /**
    * Create a new {@link RegexParameterizedPatternBuilder} instance.
    */
   public RegexParameterizedPatternBuilder(final CaptureType type, final String defaultParameterPattern,
            final String pattern)
   {
      Assert.notNull(pattern, "Pattern must not be null");

      this.defaultParameterPattern = defaultParameterPattern;
      this.pattern = pattern;
      this.chars = pattern.toCharArray();
      this.groups.addAll(RegexParameterizedPatternParser.getGroups(type, chars));
   }

   @Override
   public String build(final Rewrite event, final EvaluationContext context)
            throws ParameterizationException
   {
      return build(event, context, null);
   }

   @Override
   public String build(final Rewrite event, final EvaluationContext context,
            final Transposition<String> transposition)
            throws ParameterizationException
   {
      return build(extractBoundValues(event, context, transposition));
   }

   @Override
   public String build(final List<Object> values) throws ParameterizationException
   {
      if ((values == null && groups.size() != 0) || (groups.size() != values.size()))
      {
         throw new ParameterizationException("Must supply [" + groups.size() + "] values to build output string.");
      }

      StringBuilder builder = new StringBuilder();
      CapturingGroup last = null;

      int index = 0;
      for (RegexGroup param : groups)
      {
         CapturingGroup capture = param.getCapture();

         if ((last != null) && (last.getEnd() < capture.getStart()))
         {
            builder.append(Arrays.copyOfRange(chars, last.getEnd() + 1, capture.getStart()));
         }
         else if ((last == null) && (capture.getStart() > 0))
         {
            builder.append(Arrays.copyOfRange(chars, 0, capture.getStart()));
         }

         builder.append(values.get(index++));

         last = capture;
      }

      if ((last != null) && (last.getEnd() < chars.length))
      {
         builder.append(Arrays.copyOfRange(chars, last.getEnd() + 1, chars.length));
      }
      else if (last == null)
      {
         builder.append(chars);
      }

      String result = builder.toString();

      // if (!getParser().matches(result))
      // {
      // throw new IllegalStateException("Generated a value that does not match constraints.");
      // }

      return result;
   }

   @Override
   public String build(final Map<String, Object> values) throws ParameterizationException
   {
      StringBuilder builder = new StringBuilder();
      CapturingGroup last = null;

      for (RegexGroup param : groups)
      {
         CapturingGroup capture = param.getCapture();

         if ((last != null) && (last.getEnd() < capture.getStart()))
         {
            builder.append(Arrays.copyOfRange(chars, last.getEnd() + 1, capture.getStart()));
         }
         else if ((last == null) && (capture.getStart() > 0))
         {
            builder.append(Arrays.copyOfRange(chars, 0, capture.getStart()));
         }

         if (!values.containsKey(param.getName()))
            throw new ParameterizationException("No value supplied for parameter [" + param.getName()
                     + "] when building pattern [" + getPattern() + "].");

         builder.append(values.get(param.getName()));

         last = capture;
      }

      if ((last != null) && (last.getEnd() < chars.length))
      {
         builder.append(Arrays.copyOfRange(chars, last.getEnd() + 1, chars.length));
      }
      else if (last == null)
      {
         builder.append(chars);
      }

      String result = builder.toString();

      // if (!getParser().matches(result))
      // {
      // throw new IllegalStateException("Generated a value that does not match constraints.");
      // }

      return result;
   }

   /**
    * Extract bound values from configured {@link Binding} instances. Return a {@link Map} of the extracted key-value
    * pairs. Before storing the values in the map, this method applies the supplied {@link Transposition} instance.
    */
   private Map<String, Object> extractBoundValues(final Rewrite event, final EvaluationContext context,
            Transposition<String> transposition)
   {
      Map<String, Object> result = new LinkedHashMap<String, Object>();

      for (RegexGroup group : groups)
      {
         Parameter<?> parameter = store.get(group.getName());
         Object value = null;

         // TODO TEST ME!!!
         if (context.getState().isEvaluating())
            value = ((ParameterValueStore) context.get(ParameterValueStore.class)).retrieve(parameter);

         if (value == null || context.getState().isPerforming())
         {
            Object retrieved = ParameterUtils.performRetrieval(event, context, parameter);
            if (retrieved != null)
               value = retrieved;
         }

         if (value == null)
            throw new ParameterizationException("The value of required parameter [" + group.getName() + "] was null.");

         if (transposition != null)
            value = transposition.transpose(event, context, value.toString());

         result.put(group.getName(), value);
      }
      return result;
   }

   @Override
   public String toString()
   {
      return new String(chars);
   }

   @Override
   public String getPattern()
   {
      return pattern;
   }

   @Override
   public ParameterizedPatternParser getParser()
   {
      if (parser == null)
      {
         parser = new RegexParameterizedPatternParser(this, defaultParameterPattern, pattern);
         parser.setParameterStore(store);
      }
      return parser;
   }

   @Override
   public Set<String> getRequiredParameterNames()
   {
      Set<String> result = new LinkedHashSet<String>();
      for (RegexGroup group : groups)
      {
         result.add(group.getName());
      }
      return result;
   }

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

   @Override
   public boolean isParameterComplete(Rewrite event, EvaluationContext context)
   {
      try
      {
         extractBoundValues(event, context, null);
         return true;
      }
      catch (ParameterizationException e)
      {
         return false;
      }
   }
}


/*
 * 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 java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.ocpsoft.common.pattern.Weighted;
import org.ocpsoft.rewrite.context.Context;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.event.Rewrite;
import org.ocpsoft.rewrite.exception.RewriteException;
import org.ocpsoft.rewrite.param.ConfigurableParameter;
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.Parameterized;
import org.ocpsoft.rewrite.param.ParameterizedRule;
import org.ocpsoft.rewrite.util.Visitor;

/**
 * Builder for fluently defining new composite {@link Rule} instances.
 *
 * @author Lincoln Baxter, III
 */
public final class RuleBuilder implements ParameterizedRule, RelocatableRule, CompositeCondition, CompositeOperation,
         CompositeRule, Context
{
   private final ParameterStore store;

   private Integer priority = null;
   private String id = "";
   private Condition condition;
   private Operation operation;
   protected Map<Object, Object> contextMap = new HashMap<Object, Object>();
   private Rule wrapped;

   private RuleBuilder()
   {
      store = new DefaultParameterStore();
   }

   private RuleBuilder(Rule rule)
   {
      store = new DefaultParameterStore();
      withId(rule.getId());

      if (rule instanceof Weighted)
         withPriority(((Weighted) rule).priority());

      wrapped = rule;
   }

   /**
    * Returns a new {@link RuleBuilder} instance.
    */
   public static RuleBuilder define()
   {
      return new RuleBuilder();
   }

   /**
    * Returns a new {@link RuleBuilder} instance wrapping the given {@link Rule}.
    */
   public static RuleBuilder wrap(final Rule rule)
   {
      return new RuleBuilder(rule);
   }

   /**
    * Returns a new {@link RuleBuilder} instance, set with the given {@link Rule} ID.
    */
   public static RuleBuilder define(final String id)
   {
      return define().withId(id);
   }

   /**
    * Set the ID of this {@link Rule} instance.
    */
   public RuleBuilder withId(final String id)
   {
      this.id = id;
      return this;
   }

   /**
    * Set the priority of this {@link Rule} instance. If {@link #priority()} differs from the priority of the
    * {@link ConfigurationProvider} from which this rule was returned, then relocate this rule to its new priority
    * position in the compiled rule set.
    */
   public RuleBuilder withPriority(int priority)
   {
      this.priority = priority;
      return this;
   }

   /**
    * Set the {@link Condition} of this {@link Rule} instance.
    */
   public RuleBuilder when(final Condition condition)
   {
      if (this.condition == null)
         this.condition = condition;
      else if (condition instanceof ConditionBuilder)
         this.condition = ((ConditionBuilder) this.condition).and(condition);
      else
         this.condition = Conditions.wrap(this.condition).and(condition);
      return this;
   }

   /**
    * Perform the given {@link Operation} when the conditions set in this {@link Rule} are met.
    */
   public RuleBuilder perform(final Operation operation)
   {
      if (this.operation == null)
         this.operation = operation;
      else if (operation instanceof OperationBuilder)
         this.operation = ((OperationBuilder) this.operation).and(operation);
      else
         this.operation = Operations.wrap(this.operation).and(operation);
      return this;
   }

   @Override
   public boolean evaluate(final Rewrite event, final EvaluationContext context)
   {
      context.put(ParameterStore.class, store);

      if (wrapped != null && condition != null)
         return wrapped.evaluate(event, context) && condition.evaluate(event, context);
      else if (wrapped != null)
         return wrapped.evaluate(event, context);
      else if (condition != null)
         return condition.evaluate(event, context);

      return true;
   }

   @Override
   public void perform(final Rewrite event, final EvaluationContext context)
   {
      if (wrapped != null)
         wrapped.perform(event, context);

      if (operation != null)
         operation.perform(event, context);
   }

   @Override
   public String getId()
   {
      return id;
   }

   @Override
   public int priority()
   {
      return priority == null ? 0 : priority;
   }

   @Override
   public boolean isRelocated()
   {
      return priority != null;
   }

   /**
    * Return the underlying {@link ConditionBuilder}
    */
   public DefaultConditionBuilder getConditionBuilder()
   {
      if (condition == null)
         condition = Conditions.create();
      else if (!(condition instanceof DefaultConditionBuilder))
         condition = Conditions.wrap(condition);

      return (DefaultConditionBuilder) condition;
   }

   /**
    * Return the underlying {@link OperationBuilder}
    */
   public DefaultOperationBuilder getOperationBuilder()
   {
      if (operation == null)
         operation = Operations.create();
      else if (!(operation instanceof DefaultOperationBuilder))
         operation = Operations.wrap(operation);

      return (DefaultOperationBuilder) operation;
   }

   /**
    * This method will call the supplied visitor for all conditions attached to the rule builder.
    *
    * @param visitor visitor to process
    */
   public void accept(Visitor<Condition> visitor)
   {
      new ConditionVisit(condition).accept(visitor);
   }

   @Override
   public List<Operation> getOperations()
   {
      if (wrapped != null && operation != null)
         return Arrays.asList(wrapped, operation);

      else if (wrapped != null)
         return Arrays.asList((Operation) wrapped);

      else if (operation != null)
         return Arrays.asList(operation);

      return Collections.emptyList();
   }

   @Override
   public List<Condition> getConditions()
   {
      if (wrapped != null && condition != null)
         return Arrays.asList(wrapped, condition);

      else if (wrapped != null)
         return Arrays.asList((Condition) wrapped);

      else if (condition != null)
         return Arrays.asList(condition);

      return Collections.emptyList();
   }

   @Override
   public List<Rule> getRules()
   {
      return Arrays.asList(wrapped);
   }

   @Override
   public ParameterStore getParameterStore()
   {
      return store;
   }

   public ConfigurableParameter<?> where(String name)
   {
      assertParameterExists(name);
      Parameter<?> result = getParameterStore().get(name, new DefaultParameter(name));
      if (result instanceof ConfigurableParameter)
         return (ConfigurableParameter<?>) result;
      throw new RewriteException("Cannot configure read-only parameter [" + name + "].");
   }

   private void assertParameterExists(String name)
   {
      final Set<String> parameterNames = new LinkedHashSet<String>();

      ParameterizedCallback callback = new ParameterizedCallback()
      {
         @Override
         public void call(Parameterized parameterized)
         {
            Set<String> names = parameterized.getRequiredParameterNames();
            parameterNames.addAll(names);
         }
      };

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

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

      if (!parameterNames.contains(name))
         throw new IllegalArgumentException("Parameter [" + name + "] does not exist in rule [" + this
                  + "] and cannot be configured.");
   }

   @Override
   public void clear()
   {
      if (wrapped instanceof Context)
      {
         ((Context) wrapped).clear();;
      }
      contextMap.clear();;
   }

   @Override
   public Object get(Object key)
   {
      if (wrapped instanceof Context)
      {
         return ((Context) wrapped).get(key);
      }
      return contextMap.get(key);
   }

   @Override
   public void put(Object key, Object value)
   {
      if (wrapped instanceof Context)
      {
         ((Context) wrapped).put(key, value);
      }
      contextMap.put(key, value);
   }

   @Override
   public boolean containsKey(Object key)
   {
      if (wrapped instanceof Context)
      {
         return ((Context) wrapped).containsKey(key);
      }
      return contextMap.containsKey(key);
   }

   @Override
   public Set<String> getRequiredParameterNames()
   {
      Set<String> result = new HashSet<String>();
      if (condition instanceof Parameterized)
      {
         Set<String> names = ((Parameterized) condition).getRequiredParameterNames();
         if (names != null)
            result.addAll(names);
      }
      if (operation instanceof Parameterized)
      {
         Set<String> names = ((Parameterized) operation).getRequiredParameterNames();
         if (names != null)
            result.addAll(names);
      }
      if (wrapped instanceof Parameterized)
      {
         Set<String> names = ((Parameterized) wrapped).getRequiredParameterNames();
         if (names != null)
            result.addAll(names);
      }
      return result;
   }

   @Override
   public void setParameterStore(ParameterStore store)
   {
      if (condition instanceof Parameterized)
         ((Parameterized) condition).setParameterStore(store);
      if (operation instanceof Parameterized)
         ((Parameterized) operation).setParameterStore(store);
      if (wrapped instanceof Parameterized)
         ((Parameterized) wrapped).setParameterStore(store);
   }

   @Override
   public String toString()
   {
      String result = ".addRule(";

      if (wrapped != null && !(wrapped instanceof RuleBuilder))
      {
         result += wrapped + ")";
      }
      else
      {
         result += ")";

         if (condition instanceof RuleBuilder)
         {
            String conditionToString = ((RuleBuilder) condition).conditionToString();
            if (!conditionToString.isEmpty())
               result += ".when(" + conditionToString + ")";
         }
         else if (condition != null)
            result += ".when(" + condition + ")";

         if (operation instanceof RuleBuilder)
         {
            String operationToString = ((RuleBuilder) operation).operationToString();
            if (!operationToString.isEmpty())
               result += ".perform(" + operationToString + ")";
         }
         else if (operation != null)
            result += ".perform(" + operation + ")";
      }

      if (getId() != null && !getId().isEmpty())
         result += ".withId(\"" + getId() + "\")";

      if (priority() != 0)
         result += ".withPriority(" + priority() + ")";

      return result;
   }

   protected String conditionToString()
   {
      if (condition instanceof RuleBuilder)
         return ((RuleBuilder) condition).conditionToString();

      return condition == null ? "" : condition.toString();
   }

   protected String operationToString()
   {
      if (operation instanceof RuleBuilder)
         return ((RuleBuilder) operation).conditionToString();

      return operation == null ? "" : operation.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.urlbuilder.util;

/**
 * @author Lincoln Baxter, III
 *
 */
public abstract class ParseTools
{
   private static final char ESCAPE_CHAR = '\\';

   /**
    * Return the index of the terminating character in the group, excluding markers.
    */
   public static CapturingGroup balancedCapture(final CharSequence path, final int startPos, final int endPos,
            final CaptureType type)
   {
      Assert.notNull(path, "Character input array must not be null.");
      Assert.assertTrue(startPos >= 0, "Start position must be greater than zero.");
      Assert.assertTrue(startPos < (path.length() - 1),
               "Start position must be at least one character before the array upper bound.");
      Assert.assertTrue(endPos > 0, "End position must be greater than zero.");
      Assert.assertTrue(endPos < path.length(), "End position must be less than or equal to the array upper bound.");
      Assert.assertTrue(startPos < endPos, "Start position must be less than end position.");

      Assert.assertTrue(path.charAt(startPos) == type.getBegin(),
               "Character at starting position was [" + path.charAt(startPos)
                        + "] but expected [" + type.getBegin() + "]");

      if ((startPos > 0) && (path.charAt(startPos - 1) == ESCAPE_CHAR))
      {
         if ((startPos == 1) || ((startPos > 1) && (path.charAt(startPos - 2) != ESCAPE_CHAR)))
         {
            throw new IllegalArgumentException(
                     "Character at starting position is escaped, and cannot be used in capturing a group.");
         }
      }

      int depth = 1;

      char begin = type.getBegin();
      char term = type.getEnd();

      int cursor = startPos + 1;
      while ((cursor < endPos) && (depth > 0))
      {
         if (path.charAt(cursor) == term)
         {
            if (!isEscaped(path, cursor))
               depth--;
         }
         else if (path.charAt(cursor) == begin)
         {
            if (!isEscaped(path, cursor))
               depth++;
         }

         if (depth == 0)
         {
            break;
         }
         cursor++;
      }

      return new CapturingGroup(path, startPos, cursor);
   }

   /**
    * Return true if the character at the given cursor is escaped; otherwise, return false.
    */
   public static boolean isEscaped(final CharSequence path, final int cursor)
   {
      Assert.notNull(path, "Character input array must not be null.");
      Assert.assertTrue(cursor >= 0, "Start position must be greater than zero.");
      Assert.assertTrue(cursor < (path.length()),
               "Start position must be within the array upper bound.");

      if ((cursor > 0) && (path.charAt(cursor - 1) == ESCAPE_CHAR))
      {
         if ((cursor == 1) || ((cursor > 1) && (path.charAt(cursor - 2) != ESCAPE_CHAR)))
         {
            return true;
         }
      }
      return false;
   }
}


/*
 * 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.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

import org.ocpsoft.common.util.Streams;
import org.ocpsoft.common.util.Strings;
import org.ocpsoft.rewrite.config.Condition;
import org.ocpsoft.rewrite.config.ConditionBuilder;
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.exception.RewriteException;
import org.ocpsoft.rewrite.servlet.RewriteWrappedResponse;
import org.ocpsoft.rewrite.servlet.config.response.GZipResponseContentInterceptor;
import org.ocpsoft.rewrite.servlet.config.response.GZipResponseStreamWrapper;
import org.ocpsoft.rewrite.servlet.config.response.ResponseContentInterceptor;
import org.ocpsoft.rewrite.servlet.config.response.ResponseStreamWrapper;
import org.ocpsoft.rewrite.servlet.http.event.HttpServletRewrite;

/**
 * Responsible for manipulating properties such as headers, cookies, {@link ResponseContentInterceptor} and
 * {@link ResponseStreamWrapper} instances to the current {@link HttpServletResponse}
 *
 * @author Lincoln Baxter, III
 */
public abstract class Response extends HttpOperation
{
   /**
    * Create an {@link Operation} that adds the given {@link ResponseContentInterceptor} instances to the current
    * {@link ServletResponse}. This will activate response buffering on the current {@link ServletRequest} - meaning
    * that generated output will not be sent to the client until the entire request has completed and all registered
    * {@link ResponseContentInterceptor} instances have been executed on the outbound response content.
    *
    *

    * WARNING: This will cause the ENTIRE response to be buffered in memory, which may cause performance
    * issues on larger responses. Make sure you you really need to buffer the entire response! Favor using a
    * {@link ResponseStreamWrapper} if desired behavior may be performed as a stream operation; this will result in far
    * less memory overhead.
    *
    * @throws IllegalStateException When output has already been written to the client.
    */
   public static OperationBuilder withOutputInterceptedBy(final ResponseContentInterceptor... buffers)
            throws IllegalStateException
   {
      return new Response() {
         @Override
         public void performHttp(HttpServletRewrite event, EvaluationContext context)
         {
            for (ResponseContentInterceptor buffer : buffers) {
               RewriteWrappedResponse.getCurrentInstance(event.getRequest()).addContentInterceptor(buffer);
            }
         }

         @Override
         public String toString()
         {
            return "Response.withOutputInterceptedBy(" + Strings.join(Arrays.asList(buffers), ", ") + ")";
         }
      };
   }

   /**
    * Create an {@link Operation} that adds the given {@link ResponseStreamWrapper} instances to the current
    * {@link ServletResponse}. This will activate response stream wrapping on the current {@link ServletRequest} -
    * meaning response content will be piped through all registered {@link ResponseStreamWrapper} instances as it is
    * written to the client {@link ServletOutputStream}.
    *
    * @throws IllegalStateException When output has already been written to the client.
    */
   public static OperationBuilder withOutputStreamWrappedBy(final ResponseStreamWrapper... wrappers)
            throws IllegalStateException
   {
      return new Response() {
         @Override
         public void performHttp(HttpServletRewrite event, EvaluationContext context)
         {
            for (ResponseStreamWrapper wrapper : wrappers) {
               RewriteWrappedResponse.getCurrentInstance(event.getRequest()).addStreamWrapper(wrapper);
            }
         }

         @Override
         public String toString()
         {
            return "Response.withOutputStreamWrappedBy(" + Strings.join(Arrays.asList(wrappers), ", ") + ")";
         }
      };
   }

   /**
    * Create an {@link Operation} that adds a header to the {@link HttpServletResponse}
    */
   public static OperationBuilder addHeader(final String name, final String value)
   {
      return new Response() {
         @Override
         public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
         {
            event.getResponse().addHeader(name, value);
         }

         @Override
         public String toString()
         {
            return "Response.addHeader(\"" + name + "\", " + value + ")";
         }
      };
   }

   /**
    * Create an {@link Operation} that adds a date header to the {@link HttpServletResponse}
    */
   public static OperationBuilder addDateHeader(final String name, final long value)
   {
      return new Response() {
         @Override
         public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
         {
            event.getResponse().addDateHeader(name, value);
         }

         @Override
         public String toString()
         {
            return "Response.addDateHeader(\"" + name + "\", " + value + ")";
         }
      };
   }

   /**
    * Create an {@link Operation} that sets the content type for the {@link HttpServletResponse}
    */
   public static OperationBuilder setContentType(final String contentType)
   {
      return new Response() {
         @Override
         public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
         {
            event.getResponse().setContentType(contentType);
         }

         @Override
         public String toString()
         {
            return "Response.setContentType(\"" + contentType + "\")";
         }
      };
   }

   /**
    * Create an {@link Operation} that adds an int header to the {@link HttpServletResponse}
    */
   public static OperationBuilder addIntHeader(final String name, final int value)
   {
      return new Response() {
         @Override
         public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
         {
            event.getResponse().addIntHeader(name, value);
         }

         @Override
         public String toString()
         {
            return "Response.addIntHeader(\"" + name + "\", " + value + ")";
         }
      };
   }

   /**
    * Create an {@link Operation} that adds a {@link Cookie} to the {@link HttpServletResponse}
    */
   public static OperationBuilder addCookie(final Cookie cookie)
   {
      return new Response() {
         @Override
         public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
         {
            event.getResponse().addCookie(cookie);
         }

         @Override
         public String toString()
         {
            return "Response.addCookie(" + cookie + ")";
         }
      };
   }

   /**
    * Compress the {@link ServletOutputStream} contents written to the client.
    *

    * WARNING: This causes response content to be buffered in memory in order to properly count the response
    * 'Content-Length'.
    */
   public static OperationBuilder gzipCompression()
   {
      return new Response() {

         @Override
         public void performHttp(HttpServletRewrite event, EvaluationContext context)
         {
            withOutputInterceptedBy(new GZipResponseContentInterceptor()).perform(event, context);
         }

         @Override
         public String toString()
         {
            return "Response.gzipCompression()";
         }

      };
   }

   /**
    * Compress the {@link ServletOutputStream} contents written to the client.
    *

    * WARNING: This means that the HTTP response will not contain a 'Content-Length' header.
    */
   public static OperationBuilder gzipStreamCompression()
   {
      return new Response() {

         @Override
         public void performHttp(HttpServletRewrite event, EvaluationContext context)
         {
            withOutputStreamWrappedBy(new GZipResponseStreamWrapper()).perform(event, context);
         }

         @Override
         public String toString()
         {
            return "Response.gzipCompression()";
         }

      };
   }

   /**
    * Create an {@link Operation} that writes the given bytes to the current {@link HttpServletResponse} upon execution.
    */
   public static OperationBuilder write(final byte... bytes)
   {
      return write(new ByteArrayInputStream(bytes));
   }

   /**
    * Create an {@link Operation} that writes the {@link String} value of the given {@link Object} to the current
    * {@link HttpServletResponse} upon execution. The value to be written is obtained using {@link Object#toString()}.
    */
   public static OperationBuilder write(final Object value)
   {
      return new Response() {
         @Override
         public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
         {
            try {
               if (value != null)
                  event.getResponse().getWriter().write(value.toString());
            }
            catch (IOException e) {
               throw new RewriteException("Could not write value [" + value + "] to response stream.", e);
            }
         }

         @Override
         public String toString()
         {
            return "Response.write(\"" + value + "\")";
         }
      };
   }

   /**
    * Create an {@link Operation} that writes the contents of the given {@link InputStream} current
    * {@link HttpServletResponse} upon execution.
    */
   public static OperationBuilder write(final InputStream stream)
   {
      return new HttpOperation() {
         @Override
         public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
         {
            try {
               Streams.copy(stream, event.getResponse().getOutputStream());
            }
            catch (IOException e) {
               throw new RewriteException("Could not write stream [" + stream + "] to response stream.", e);
            }
         }

         @Override
         public String toString()
         {
            return "Response.write(" + stream + ")";
         }
      };
   }

   /**
    * Create an {@link Operation} that sets the given status code via {@link HttpServletResponse#setStatus(int)}. This
    * does not call {@link HttpServletResponse#flushBuffer()}
    */
   public static OperationBuilder setStatus(final int status)
   {
      return new HttpOperation()
      {
         @Override
         public void performHttp(final HttpServletRewrite event, final EvaluationContext context)
         {
            event.getResponse().setStatus(status);
         }

         @Override
         public String toString()
         {
            return "Response.setStatus(" + status + ")";
         }
      };

   }

   /**
    * Create an {@link Operation} that halts further {@link Rewrite} processing and completes the current
    * {@link HttpServletResponse}.
    */
   public static OperationBuilder complete()
   {
      return Lifecycle.abort();
   }

   /**
    * Create a {@link Condition} that evaluates the status of {@link HttpServletResponse#isCommitted()}.
    */
   public static ConditionBuilder isCommitted()
   {
      return new HttpCondition() {
         @Override
         public boolean evaluateHttp(HttpServletRewrite event, EvaluationContext context)
         {
            return event.getResponse().isCommitted();
         }

         @Override
         public String toString()
         {
            return "Response.isCommitted()";
         }
      };
   }

}