001package com.hfg.webapp;
002
003import java.text.ParseException;
004import java.text.SimpleDateFormat;
005import java.util.ArrayList;
006import java.util.Date;
007import java.util.List;
008import java.util.regex.Matcher;
009import java.util.regex.Pattern;
010import javax.servlet.http.Cookie;
011
012import com.hfg.util.StringUtil;
013
014//------------------------------------------------------------------------------
015/**
016 Cookie extension that handles encoding/decoding of cookie values.
017 <div>
018  @author J. Alex Taylor, hairyfatguy.com
019 </div>
020*/
021//------------------------------------------------------------------------------
022// com.hfg XML/HTML Coding Library
023//
024// This library is free software; you can redistribute it and/or
025// modify it under the terms of the GNU Lesser General Public
026// License as published by the Free Software Foundation; either
027// version 2.1 of the License, or (at your option) any later version.
028//
029// This library is distributed in the hope that it will be useful,
030// but WITHOUT ANY WARRANTY; without even the implied warranty of
031// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
032// Lesser General Public License for more details.
033//
034// You should have received a copy of the GNU Lesser General Public
035// License along with this library; if not, write to the Free Software
036// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
037//
038// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
039// jataylor@hairyfatguy.com
040//------------------------------------------------------------------------------
041// TODO: Add a toString().
042
043public class HfgCookie extends Cookie
044{
045   private Date   mExpires;
046   private String mDecodedValue;
047
048
049   private static final Pattern COOKIE_NAME_VALUE_PATTERN = Pattern.compile("([^=]+)=(.+)");
050
051   //  Mon, 09-Dec-2002 13:46:00 GMT
052   private static final SimpleDateFormat EXPIRES_FORMAT = new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss z");
053
054   //###########################################################################
055   // CONSTRUCTORS
056   //###########################################################################
057
058   //---------------------------------------------------------------------------
059   public HfgCookie(String inName, String inValue)
060   {
061      super(inName, inValue);
062
063      setValue(inValue);
064   }
065
066   //---------------------------------------------------------------------------
067   public HfgCookie(Cookie inCookie)
068   {
069      this(inCookie.getName(), inCookie.getValue());
070
071      if (inCookie.getPath() != null)
072      {
073         setPath(inCookie.getPath());
074      }
075
076      if (inCookie.getComment() != null)
077      {
078         setComment(inCookie.getComment());
079      }
080
081      if (inCookie.getDomain() != null)
082      {
083         setDomain(inCookie.getDomain());
084      }
085
086      setMaxAge(inCookie.getMaxAge());
087      setHttpOnly(inCookie.isHttpOnly());
088      setSecure(inCookie.getSecure());
089      setVersion(inCookie.getVersion());
090   }
091
092   //###########################################################################
093   // PUBLIC METHODS
094   //###########################################################################
095
096   //---------------------------------------------------------------------------
097   /**
098    * Returns one or more cookies from an http response's "Set-Cookie" or
099    * "Set-Cookie2" header field.
100    * <div>
101    * "Set-Cookie" is covered by RFC2109.
102    * RFC 2965 section 3.2.2 says that one "Set-Cookie2" header line
103    * can contain more than one cookie definition.
104    * </div>
105    * @param inSetCookieValue the value of the "Set-Cookie" or "Set-Cookie2" header
106    * @return one or more parsed cookies
107    */
108   public static List<HfgCookie> parse(String inSetCookieValue)
109   {
110      List<HfgCookie> cookies = null;
111
112      // Ex:   JSESSIONID=3AF5DD38614E80FCCC0632902E3C8E18; Path=/; Secure; HttpOnly
113      if (StringUtil.isSet(inSetCookieValue))
114      {
115         List<String> cookieStrings = splitIntoCookieStrings(inSetCookieValue);
116
117         cookies = new ArrayList<>(cookieStrings.size());
118         for (String cookieString : cookieStrings)
119         {
120            HfgCookie currentCookie = null;
121
122            String[] pieces = cookieString.split(";");
123            for (String piece : pieces)
124            {
125               piece = piece.trim();
126
127               Matcher m = COOKIE_NAME_VALUE_PATTERN.matcher(piece);
128               if (m.matches())
129               {
130                  String name = m.group(1).trim();
131                  String value = m.group(2).trim();
132
133                  if (name.equalsIgnoreCase("Path"))
134                  {
135                     if (currentCookie != null)
136                     {
137                        currentCookie.setPath(value);
138                     }
139                  }
140                  else if (name.equalsIgnoreCase("Domain"))
141                  {
142                     if (currentCookie != null)
143                     {
144                        currentCookie.setDomain(value);
145                     }
146                  }
147                  else if (name.equalsIgnoreCase("Version"))
148                  {
149                     if (currentCookie != null)
150                     {
151                        currentCookie.setVersion(Integer.parseInt(value));
152                     }
153                  }
154                  else if (name.equalsIgnoreCase("Max-Age")) // Seconds to keep the cookie
155                  {
156                     if (currentCookie != null)
157                     {
158                        currentCookie.setMaxAge(Integer.parseInt(value));
159                     }
160                  }
161                  else if (name.equalsIgnoreCase("Expires"))
162                  {
163                     if (currentCookie != null)
164                     {
165                        try
166                        {
167                           //  Mon, 09-Dec-2002 13:46:00 GMT
168                           currentCookie.setExpires(EXPIRES_FORMAT.parse(value));
169                        }
170                        catch (ParseException e)
171                        {
172                           throw new RuntimeException(e);
173                        }
174                     }
175                  }
176                  else
177                  {
178                     currentCookie = new HfgCookie(name, value);
179                     cookies.add(currentCookie);
180                  }
181               }
182               else if (piece.equalsIgnoreCase("Secure"))
183               {
184                  if (currentCookie != null)
185                  {
186                     currentCookie.setSecure(true);
187                  }
188               }
189               else if (piece.equalsIgnoreCase("HttpOnly"))
190               {
191                  if (currentCookie != null)
192                  {
193                     currentCookie.setHttpOnly(true);
194                  }
195               }
196
197            }
198         }
199
200      }
201
202      return cookies;
203   }
204
205   //---------------------------------------------------------------------------
206   @Override
207   public String toString()
208   {
209      return getName() + "=" + getValue();
210   }
211
212   //---------------------------------------------------------------------------
213   @Override
214   public void setValue(String inValue)
215   {
216      super.setValue(inValue);
217      // The incoming value could be encoded or decoded.
218      if (inValue != null)
219      {
220         if (WebappUtil.isCookieEncoded(this))
221         {
222            mDecodedValue = WebappUtil.decodeCookieValue(getValue());
223            // Sometimes we have double-encoded values. Try unwinding further...
224            while (WebappUtil.isCookieEncoded(mDecodedValue))
225            {
226               mDecodedValue = WebappUtil.decodeCookieValue(mDecodedValue);
227            }
228         }
229         else if (WebappUtil.cookieNeedsEncoding(this))
230         {
231            mDecodedValue = getValue();
232            setValue(WebappUtil.encodeCookieValue(getValue()));
233         }
234         else
235         {
236            mDecodedValue = getValue();
237         }
238      }
239   }
240
241   //---------------------------------------------------------------------------
242   public HfgCookie setExpires(Date inValue)
243   {
244      mExpires = inValue;
245      return this;
246   }
247
248   //---------------------------------------------------------------------------
249   public Date getExpires()
250   {
251      return mExpires;
252   }
253
254   //---------------------------------------------------------------------------
255   public String getDecodedValue()
256   {
257      return mDecodedValue;
258   }
259
260
261   //###########################################################################
262   // PRIVATE METHODS
263   //###########################################################################
264
265   //---------------------------------------------------------------------------
266   private static List<String> splitIntoCookieStrings(String inValue)
267   {
268      List<String> cookies;
269
270      // Only bother looking closely if the is a comma somewhere in the string
271      if (inValue.indexOf(",") > 0)
272      {
273         cookies = new ArrayList<>(2);
274
275         // Make sure the comma isn't inside of quotes
276         boolean inQuotes = false;
277         int startIndex = 0;
278         for (int i = 0; i < inValue.length(); i++)
279         {
280            char currentChar = inValue.charAt(i);
281            if (currentChar == '"')
282            {
283               inQuotes = ! inQuotes;
284            }
285            else if (currentChar == ','
286                  && ! inQuotes)
287            {
288               cookies.add(inValue.substring(startIndex, i));
289               startIndex = i + 1;
290            }
291         }
292
293         // Add whatever is remaining
294         cookies.add(inValue.substring(startIndex));
295      }
296      else
297      {
298         cookies = new ArrayList<>(1);
299         cookies.add(inValue);
300      }
301
302      return cookies;
303   }
304}