1 /* 2 * Copyright 2005-2008 Sun Microsystems, Inc. All Rights Reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Sun designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Sun in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, 22 * CA 95054 USA or visit www.sun.com if you need additional information or 23 * have any questions. 24 */ 25 26 package java.net; 27 28 import java.net.URI; 29 import java.net.CookieStore; 30 import java.net.HttpCookie; 31 import java.net.URISyntaxException; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.ArrayList; 35 import java.util.HashMap; 36 import java.util.Collections; 37 import java.util.Iterator; 38 import java.util.concurrent.locks.ReentrantLock; 39 40 /** 41 * A simple in-memory java.net.CookieStore implementation 42 * 43 * @author Edward Wang 44 * @since 1.6 45 */ 46 class InMemoryCookieStore implements CookieStore { 47 // the in-memory representation of cookies 48 private List<HttpCookie> cookieJar = null; 49 50 // the cookies are indexed by its domain and associated uri (if present) 51 // CAUTION: when a cookie removed from main data structure (i.e. cookieJar), 52 // it won't be cleared in domainIndex & uriIndex. Double-check the 53 // presence of cookie when retrieve one form index store. 54 private Map<String, List<HttpCookie>> domainIndex = null; 55 private Map<URI, List<HttpCookie>> uriIndex = null; 56 57 // use ReentrantLock instead of syncronized for scalability 58 private ReentrantLock lock = null; 59 60 61 /** 62 * The default ctor 63 */ 64 public InMemoryCookieStore() { 65 cookieJar = new ArrayList<HttpCookie>(); 66 domainIndex = new HashMap<String, List<HttpCookie>>(); 67 uriIndex = new HashMap<URI, List<HttpCookie>>(); 68 69 lock = new ReentrantLock(false); 70 } 71 72 /** 73 * Add one cookie into cookie store. 74 */ 75 public void add(URI uri, HttpCookie cookie) { 76 // pre-condition : argument can't be null 77 if (cookie == null) { 78 throw new NullPointerException("cookie is null"); 79 } 80 81 82 lock.lock(); 83 try { 84 // remove the ole cookie if there has had one 85 cookieJar.remove(cookie); 86 87 // add new cookie if it has a non-zero max-age 88 if (cookie.getMaxAge() != 0) { 89 cookieJar.add(cookie); 90 // and add it to domain index 91 if (cookie.getDomain() != null) { 92 addIndex(domainIndex, cookie.getDomain(), cookie); 93 } 94 // add it to uri index, too 95 addIndex(uriIndex, getEffectiveURI(uri), cookie); 96 } 97 } finally { 98 lock.unlock(); 99 } 100 } 101 102 103 /** 104 * Get all cookies, which: 105 * 1) given uri domain-matches with, or, associated with 106 * given uri when added to the cookie store. 107 * 3) not expired. 108 * See RFC 2965 sec. 3.3.4 for more detail. 109 */ 110 public List<HttpCookie> get(URI uri) { 111 // argument can't be null 112 if (uri == null) { 113 throw new NullPointerException("uri is null"); 114 } 115 116 List<HttpCookie> cookies = new ArrayList<HttpCookie>(); 117 boolean secureLink = "https".equalsIgnoreCase(uri.getScheme()); 118 lock.lock(); 119 try { 120 // check domainIndex first 121 getInternal1(cookies, domainIndex, uri.getHost(), secureLink); 122 // check uriIndex then 123 getInternal2(cookies, uriIndex, getEffectiveURI(uri), secureLink); 124 } finally { 125 lock.unlock(); 126 } 127 128 return cookies; 129 } 130 131 /** 132 * Get all cookies in cookie store, except those have expired 133 */ 134 public List<HttpCookie> getCookies() { 135 List<HttpCookie> rt; 136 137 lock.lock(); 138 try { 139 Iterator<HttpCookie> it = cookieJar.iterator(); 140 while (it.hasNext()) { 141 if (it.next().hasExpired()) { 142 it.remove(); 143 } 144 } 145 } finally { 146 rt = Collections.unmodifiableList(cookieJar); 147 lock.unlock(); 148 } 149 150 return rt; 151 } 152 153 /** 154 * Get all URIs, which are associated with at least one cookie 155 * of this cookie store. 156 */ 157 public List<URI> getURIs() { 158 List<URI> uris = new ArrayList<URI>(); 159 160 lock.lock(); 161 try { 162 Iterator<URI> it = uriIndex.keySet().iterator(); 163 while (it.hasNext()) { 164 URI uri = it.next(); 165 List<HttpCookie> cookies = uriIndex.get(uri); 166 if (cookies == null || cookies.size() == 0) { 167 // no cookies list or an empty list associated with 168 // this uri entry, delete it 169 it.remove(); 170 } 171 } 172 } finally { 173 uris.addAll(uriIndex.keySet()); 174 lock.unlock(); 175 } 176 177 return uris; 178 } 179 180 181 /** 182 * Remove a cookie from store 183 */ 184 public boolean remove(URI uri, HttpCookie ck) { 185 // argument can't be null 186 if (ck == null) { 187 throw new NullPointerException("cookie is null"); 188 } 189 190 boolean modified = false; 191 lock.lock(); 192 try { 193 modified = cookieJar.remove(ck); 194 } finally { 195 lock.unlock(); 196 } 197 198 return modified; 199 } 200 201 202 /** 203 * Remove all cookies in this cookie store. 204 */ 205 public boolean removeAll() { 206 lock.lock(); 207 try { 208 cookieJar.clear(); 209 domainIndex.clear(); 210 uriIndex.clear(); 211 } finally { 212 lock.unlock(); 213 } 214 215 return true; 216 } 217 218 219 /* ---------------- Private operations -------------- */ 220 221 222 /* 223 * This is almost the same as HttpCookie.domainMatches except for 224 * one difference: It won't reject cookies when the 'H' part of the 225 * domain contains a dot ('.'). 226 * I.E.: RFC 2965 section 3.3.2 says that if host is x.y.domain.com 227 * and the cookie domain is .domain.com, then it should be rejected. 228 * However that's not how the real world works. Browsers don't reject and 229 * some sites, like yahoo.com do actually expect these cookies to be 230 * passed along. 231 * And should be used for 'old' style cookies (aka Netscape type of cookies) 232 */ 233 private boolean netscapeDomainMatches(String domain, String host) 234 { 235 if (domain == null || host == null) { 236 return false; 237 } 238 239 // if there's no embedded dot in domain and domain is not .local 240 boolean isLocalDomain = ".local".equalsIgnoreCase(domain); 241 int embeddedDotInDomain = domain.indexOf('.'); 242 if (embeddedDotInDomain == 0) { 243 embeddedDotInDomain = domain.indexOf('.', 1); 244 } 245 if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) { 246 return false; 247 } 248 249 // if the host name contains no dot and the domain name is .local 250 int firstDotInHost = host.indexOf('.'); 251 if (firstDotInHost == -1 && isLocalDomain) { 252 return true; 253 } 254 255 int domainLength = domain.length(); 256 int lengthDiff = host.length() - domainLength; 257 if (lengthDiff == 0) { 258 // if the host name and the domain name are just string-compare euqal 259 return host.equalsIgnoreCase(domain); 260 } else if (lengthDiff > 0) { 261 // need to check H & D component 262 String H = host.substring(0, lengthDiff); 263 String D = host.substring(lengthDiff); 264 265 return (D.equalsIgnoreCase(domain)); 266 } else if (lengthDiff == -1) { 267 // if domain is actually .host 268 return (domain.charAt(0) == '.' && 269 host.equalsIgnoreCase(domain.substring(1))); 270 } 271 272 return false; 273 } 274 275 private void getInternal1(List<HttpCookie> cookies, Map<String, List<HttpCookie>> cookieIndex, 276 String host, boolean secureLink) { 277 // Use a separate list to handle cookies that need to be removed so 278 // that there is no conflict with iterators. 279 ArrayList<HttpCookie> toRemove = new ArrayList<HttpCookie>(); 280 for (Map.Entry<String, List<HttpCookie>> entry : cookieIndex.entrySet()) { 281 String domain = entry.getKey(); 282 List<HttpCookie> lst = entry.getValue(); 283 for (HttpCookie c : lst) { 284 if ((c.getVersion() == 0 && netscapeDomainMatches(domain, host)) || 285 (c.getVersion() == 1 && HttpCookie.domainMatches(domain, host))) { 286 if ((cookieJar.indexOf(c) != -1)) { 287 // the cookie still in main cookie store 288 if (!c.hasExpired()) { 289 // don't add twice and make sure it's the proper 290 // security level 291 if ((secureLink || !c.getSecure()) && 292 !cookies.contains(c)) { 293 cookies.add(c); 294 } 295 } else { 296 toRemove.add(c); 297 } 298 } else { 299 // the cookie has beed removed from main store, 300 // so also remove it from domain indexed store 301 toRemove.add(c); 302 } 303 } 304 } 305 // Clear up the cookies that need to be removed 306 for (HttpCookie c : toRemove) { 307 lst.remove(c); 308 cookieJar.remove(c); 309 310 } 311 toRemove.clear(); 312 } 313 } 314 315 // @param cookies [OUT] contains the found cookies 316 // @param cookieIndex the index 317 // @param comparator the prediction to decide whether or not 318 // a cookie in index should be returned 319 private <T> void getInternal2(List<HttpCookie> cookies, 320 Map<T, List<HttpCookie>> cookieIndex, 321 Comparable<T> comparator, boolean secureLink) 322 { 323 for (T index : cookieIndex.keySet()) { 324 if (comparator.compareTo(index) == 0) { 325 List<HttpCookie> indexedCookies = cookieIndex.get(index); 326 // check the list of cookies associated with this domain 327 if (indexedCookies != null) { 328 Iterator<HttpCookie> it = indexedCookies.iterator(); 329 while (it.hasNext()) { 330 HttpCookie ck = it.next(); 331 if (cookieJar.indexOf(ck) != -1) { 332 // the cookie still in main cookie store 333 if (!ck.hasExpired()) { 334 // don't add twice 335 if ((secureLink || !ck.getSecure()) && 336 !cookies.contains(ck)) 337 cookies.add(ck); 338 } else { 339 it.remove(); 340 cookieJar.remove(ck); 341 } 342 } else { 343 // the cookie has beed removed from main store, 344 // so also remove it from domain indexed store 345 it.remove(); 346 } 347 } 348 } // end of indexedCookies != null 349 } // end of comparator.compareTo(index) == 0 350 } // end of cookieIndex iteration 351 } 352 353 // add 'cookie' indexed by 'index' into 'indexStore' 354 private <T> void addIndex(Map<T, List<HttpCookie>> indexStore, 355 T index, 356 HttpCookie cookie) 357 { 358 if (index != null) { 359 List<HttpCookie> cookies = indexStore.get(index); 360 if (cookies != null) { 361 // there may already have the same cookie, so remove it first 362 cookies.remove(cookie); 363 364 cookies.add(cookie); 365 } else { 366 cookies = new ArrayList<HttpCookie>(); 367 cookies.add(cookie); 368 indexStore.put(index, cookies); 369 } 370 } 371 } 372 373 374 // 375 // for cookie purpose, the effective uri should only be http://host 376 // the path will be taken into account when path-match algorithm applied 377 // 378 private URI getEffectiveURI(URI uri) { 379 URI effectiveURI = null; 380 try { 381 effectiveURI = new URI("http", 382 uri.getHost(), 383 null, // path component 384 null, // query component 385 null // fragment component 386 ); 387 } catch (URISyntaxException ignored) { 388 effectiveURI = uri; 389 } 390 391 return effectiveURI; 392 } 393 }