NonBlockingCookieStore.java
/**
* Copyright (C) 2017 HttpBuilder-NG Project
*
* 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 groovyx.net.http;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
class NonBlockingCookieStore implements CookieStore {
//public cookie store api
public void add(final URI uri, final HttpCookie cookie) {
if(cookie.getMaxAge() == 0) {
return;
}
if(cookie.getDomain() != null) {
add(new DomainKey(cookie), cookie);
}
if(uri != null) {
add(new UriKey(uri, cookie), cookie);
}
}
public List<HttpCookie> get(final URI uri) {
List<HttpCookie> ret = (all.entrySet()
.stream()
.filter(entry -> entryValid(entry) && matches(entry, uri))
.map(Map.Entry::getValue)
.distinct()
.collect(Collectors.toList()));
return ret;
}
public List<HttpCookie> getCookies() {
return (all.entrySet()
.stream()
.filter(this::entryValid)
.map(Map.Entry::getValue)
.collect(Collectors.toList()));
}
public List<URI> getURIs() {
return (all.entrySet()
.stream()
.filter(entry -> entry.getKey() instanceof UriKey)
.filter(this::entryValid)
.map(entry -> ((UriKey) entry.getKey()).getURI())
.distinct()
.collect(Collectors.toList()));
}
public boolean remove(final URI uri, final HttpCookie cookie) {
boolean domainRemoved = false;
boolean uriRemoved = false;
if(cookie.getDomain() != null) {
domainRemoved = remove(new DomainKey(cookie));
}
if(uri != null) {
uriRemoved = remove(new UriKey(uri, cookie));
}
return domainRemoved || uriRemoved;
}
public boolean removeAll() {
int initialSize = all.size();
all.clear();
return initialSize > 0;
}
protected abstract static class Key {
final String name;
final Instant createdAt;
public Key(final String name) {
this.name = name;
this.createdAt = Instant.now();
}
abstract public String getKeyType();
public static boolean specified(final String val) {
return (val != null && !"".equals(val.trim()));
}
static Key make(final URI uri, final HttpCookie cookie) {
if(!specified(cookie.getDomain())) {
return new UriKey(uri, cookie);
}
else {
return new DomainKey(cookie);
}
}
public static String forStorage(final String str) {
return str == null ? str : str.toLowerCase();
}
}
protected static class UriKey extends Key {
public static final String TYPE = "uri";
final String host;
public UriKey(final URI uri, final HttpCookie cookie) {
super(cookie.getName());
this.host = forStorage(uri.getHost());
}
public static boolean uriKey(final String type) {
return TYPE.equals(type);
}
public String getKeyType() {
return TYPE;
}
public URI getURI() {
try {
return new URI("http", host, null, null);
}
catch(URISyntaxException e) {
//it's safe to ignore this, host already came from a valid
//uri, so constructing a new one from the host is always valid
return null;
}
}
@Override
public int hashCode() {
return 37 * name.hashCode() + host.hashCode();
}
@Override
public boolean equals(final Object o) {
if(!(o instanceof UriKey)) {
return false;
}
final UriKey rhs = (UriKey) o;
return host.equals(rhs.host) && name.equals(rhs.name);
}
@Override
public String toString() {
return String.format("UriKey(name: %s, host: %s)", name, host);
}
}
protected static class DomainKey extends Key {
public static final String TYPE = "domain";
final String domain;
final String path;
public DomainKey(final HttpCookie cookie) {
super(cookie.getName());
this.domain = cookie.getDomain();
this.path = cookie.getPath();
}
public static boolean domainKey(final String type) {
return TYPE.equals(type);
}
public String getKeyType() {
return TYPE;
}
private boolean pathEquals(final DomainKey rhs) {
if(path == null && rhs.path == null) {
return true;
}
else if(path == null && rhs.path != null) {
return false;
}
else if(path != null && rhs.path == null) {
return false;
}
else {
return path.equalsIgnoreCase(rhs.path);
}
}
@Override
public boolean equals(final Object o) {
if(!(o instanceof DomainKey)) {
return false;
}
final DomainKey rhs = (DomainKey) o;
return (name.equalsIgnoreCase(rhs.name) &&
domain.equalsIgnoreCase(rhs.domain) &&
pathEquals(rhs));
}
@Override
public int hashCode() {
return 37 * (37 * name.hashCode() + domain.hashCode()) + (path == null ? 0 : path.hashCode());
}
@Override
public String toString() {
return String.format("DomainKey(name: %s, domain: %s, path: %s", name, domain, path);
}
}
protected ConcurrentMap<Key,HttpCookie> all = new ConcurrentHashMap<>(100, 0.75f, 2);
private static URI makeURI(final String domain) {
try {
return new URI("http", domain, null, null, null);
}
catch(URISyntaxException ex) {
return null;
}
}
public boolean entryValid(final Map.Entry<Key,HttpCookie> entry) {
if(entry.getValue().hasExpired()) {
remove(entry.getKey());
return false;
}
else {
return true;
}
}
//shamelessly copied from jdk8 source code for InMemoryCookieStore
private boolean netscapeDomainMatches(final String domain, final String host) {
if (domain == null || host == null) {
return false;
}
// if there's no embedded dot in domain and domain is not .local
boolean isLocalDomain = ".local".equalsIgnoreCase(domain);
int embeddedDotInDomain = domain.indexOf('.');
if (embeddedDotInDomain == 0) {
embeddedDotInDomain = domain.indexOf('.', 1);
}
if (!isLocalDomain && (embeddedDotInDomain == -1 || embeddedDotInDomain == domain.length() - 1)) {
return false;
}
// if the host name contains no dot and the domain name is .local
int firstDotInHost = host.indexOf('.');
if (firstDotInHost == -1 && isLocalDomain) {
return true;
}
int domainLength = domain.length();
int lengthDiff = host.length() - domainLength;
if (lengthDiff == 0) {
// if the host name and the domain name are just string-compare euqal
return host.equalsIgnoreCase(domain);
}
else if (lengthDiff > 0) {
// need to check H & D component
String H = host.substring(0, lengthDiff);
String D = host.substring(lengthDiff);
return (D.equalsIgnoreCase(domain));
}
else if (lengthDiff == -1) {
// if domain is actually .host
return (domain.charAt(0) == '.' &&
host.equalsIgnoreCase(domain.substring(1)));
}
return false;
}
private boolean matches(final Map.Entry<Key,HttpCookie> entry, final URI uri) {
final HttpCookie cookie = entry.getValue();
final boolean secureLink = "https".equalsIgnoreCase(uri.getScheme());
if(!secureLink && cookie.getSecure()) {
return false;
}
final String host = uri.getHost();
if(entry.getKey() instanceof UriKey) {
return ((UriKey) entry.getKey()).host.equalsIgnoreCase(host);
}
else {
final String domain = cookie.getDomain();
if(cookie.getVersion() == 0) {
return netscapeDomainMatches(domain, host);
}
else {
return HttpCookie.domainMatches(domain, host);
}
}
}
protected void add(final Key key, final HttpCookie cookie) {
all.put(key, cookie);
}
protected boolean remove(final Key key) {
return all.remove(key) != null;
}
}