1/* Licensed to the Apache Software Foundation (ASF) under one or more
2 * contributor license agreements.  See the NOTICE file distributed with
3 * this work for additional information regarding copyright ownership.
4 * The ASF licenses this file to You under the Apache License, Version 2.0
5 * (the "License"); you may not use this file except in compliance with
6 * the License.  You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package java.net;
18
19import java.io.IOException;
20import java.util.ArrayList;
21import java.util.Collections;
22import java.util.List;
23import java.util.Map;
24
25/**
26 * This class provides a concrete implementation of CookieHandler. It separates
27 * the storage of cookies from the policy which decides to accept or deny
28 * cookies. The constructor can have two arguments: a CookieStore and a
29 * CookiePolicy. The former is in charge of cookie storage and the latter makes
30 * decision on acceptance/rejection.
31 *
32 * CookieHandler is in the center of cookie management. User can make use of
33 * CookieHandler.setDefault to set a CookieManager as the default one used.
34 *
35 * CookieManager.put uses CookiePolicy.shouldAccept to decide whether to put
36 * some cookies into a cookie store. Three built-in CookiePolicy is defined:
37 * ACCEPT_ALL, ACCEPT_NONE and ACCEPT_ORIGINAL_SERVER. Users can also customize
38 * the policy by implementing CookiePolicy. Any accepted HTTP cookie is stored
39 * in CookieStore and users can also have their own implementation. Up to now,
40 * Only add(URI, HttpCookie) and get(URI) are used by CookieManager. Other
41 * methods in this class may probably be used in a more complicated
42 * implementation.
43 *
44 * There are many ways to customize user's own HTTP cookie management:
45 *
46 * First, call CookieHandler.setDefault to set a new CookieHandler
47 * implementation. Second, call CookieHandler.getDefault to use CookieManager.
48 * The CookiePolicy and CookieStore used are customized. Third, use the
49 * customized CookiePolicy and the CookieStore.
50 *
51 * This implementation conforms to <a href="http://www.ietf.org/rfc/rfc2965.txt">RFC 2965</a> section 3.3.
52 *
53 * @since 1.6
54 */
55public class CookieManager extends CookieHandler {
56    private CookieStore store;
57
58    private CookiePolicy policy;
59
60    private static final String VERSION_ZERO_HEADER = "Set-cookie";
61
62    private static final String VERSION_ONE_HEADER = "Set-cookie2";
63
64    /**
65     * Constructs a new cookie manager.
66     *
67     * The invocation of this constructor is the same as the invocation of
68     * CookieManager(null, null).
69     *
70     */
71    public CookieManager() {
72        this(null, null);
73    }
74
75    /**
76     * Constructs a new cookie manager using a specified cookie store and a
77     * cookie policy.
78     *
79     * @param store
80     *            a CookieStore to be used by cookie manager. The manager will
81     *            use a default one if the arg is null.
82     * @param cookiePolicy
83     *            a CookiePolicy to be used by cookie manager
84     *            ACCEPT_ORIGINAL_SERVER will be used if the arg is null.
85     */
86    public CookieManager(CookieStore store, CookiePolicy cookiePolicy) {
87        this.store = store == null ? new CookieStoreImpl() : store;
88        policy = cookiePolicy == null ? CookiePolicy.ACCEPT_ORIGINAL_SERVER
89                : cookiePolicy;
90    }
91
92    /**
93     * Searches and gets all cookies in the cache by the specified uri in the
94     * request header.
95     *
96     * @param uri
97     *            the specified uri to search for
98     * @param requestHeaders
99     *            a list of request headers
100     * @return a map that record all such cookies, the map is unchangeable
101     * @throws IOException
102     *             if some error of I/O operation occurs
103     */
104    @Override
105    public Map<String, List<String>> get(URI uri,
106            Map<String, List<String>> requestHeaders) throws IOException {
107        if (uri == null || requestHeaders == null) {
108            throw new IllegalArgumentException();
109        }
110
111        List<HttpCookie> result = new ArrayList<HttpCookie>();
112        for (HttpCookie cookie : store.get(uri)) {
113            if (HttpCookie.pathMatches(cookie, uri)
114                    && HttpCookie.secureMatches(cookie, uri)
115                    && HttpCookie.portMatches(cookie, uri)) {
116                result.add(cookie);
117            }
118        }
119
120        return cookiesToHeaders(result);
121    }
122
123    private static Map<String, List<String>> cookiesToHeaders(List<HttpCookie> cookies) {
124        if (cookies.isEmpty()) {
125            return Collections.emptyMap();
126        }
127
128        StringBuilder result = new StringBuilder();
129
130        // If all cookies are version 1, add a version 1 header. No header for version 0 cookies.
131        int minVersion = 1;
132        for (HttpCookie cookie : cookies) {
133            minVersion = Math.min(minVersion, cookie.getVersion());
134        }
135        if (minVersion == 1) {
136            result.append("$Version=\"1\"; ");
137        }
138
139        result.append(cookies.get(0).toString());
140        for (int i = 1; i < cookies.size(); i++) {
141            result.append("; ").append(cookies.get(i).toString());
142        }
143
144        return Collections.singletonMap("Cookie", Collections.singletonList(result.toString()));
145    }
146
147    /**
148     * Sets cookies according to uri and responseHeaders
149     *
150     * @param uri
151     *            the specified uri
152     * @param responseHeaders
153     *            a list of request headers
154     * @throws IOException
155     *             if some error of I/O operation occurs
156     */
157    @Override
158    public void put(URI uri, Map<String, List<String>> responseHeaders) throws IOException {
159        if (uri == null || responseHeaders == null) {
160            throw new IllegalArgumentException();
161        }
162
163        // parse and construct cookies according to the map
164        List<HttpCookie> cookies = parseCookie(responseHeaders);
165        for (HttpCookie cookie : cookies) {
166
167            // if the cookie doesn't have a domain, set one. The policy will do validation.
168            if (cookie.getDomain() == null) {
169                cookie.setDomain(uri.getHost());
170            }
171
172            // if the cookie doesn't have a path, set one. If it does, validate it.
173            if (cookie.getPath() == null) {
174                cookie.setPath(pathToCookiePath(uri.getPath()));
175            } else if (!HttpCookie.pathMatches(cookie, uri)) {
176                continue;
177            }
178
179            // if the cookie has the placeholder port list "", set the port. Otherwise validate it.
180            if ("".equals(cookie.getPortlist())) {
181                cookie.setPortlist(Integer.toString(uri.getEffectivePort()));
182            } else if (cookie.getPortlist() != null && !HttpCookie.portMatches(cookie, uri)) {
183                continue;
184            }
185
186            // if the cookie conforms to the policy, add it into the store
187            if (policy.shouldAccept(uri, cookie)) {
188                store.add(uri, cookie);
189            }
190        }
191    }
192
193    /**
194     * Returns a cookie-safe path by truncating everything after the last "/".
195     * When request path like "/foo/bar.html" yields a cookie, that cookie's
196     * default path is "/foo/".
197     */
198    static String pathToCookiePath(String path) {
199        if (path == null) {
200            return "/";
201        }
202        int lastSlash = path.lastIndexOf('/'); // -1 yields the empty string
203        return path.substring(0, lastSlash + 1);
204    }
205
206    private static List<HttpCookie> parseCookie(Map<String, List<String>> responseHeaders) {
207        List<HttpCookie> cookies = new ArrayList<HttpCookie>();
208        for (Map.Entry<String, List<String>> entry : responseHeaders.entrySet()) {
209            String key = entry.getKey();
210            // Only "Set-cookie" and "Set-cookie2" pair will be parsed
211            if (key != null && (key.equalsIgnoreCase(VERSION_ZERO_HEADER)
212                    || key.equalsIgnoreCase(VERSION_ONE_HEADER))) {
213                // parse list elements one by one
214                for (String cookieStr : entry.getValue()) {
215                    try {
216                        for (HttpCookie cookie : HttpCookie.parse(cookieStr)) {
217                            cookies.add(cookie);
218                        }
219                    } catch (IllegalArgumentException ignored) {
220                        // this string is invalid, jump to the next one.
221                    }
222                }
223            }
224        }
225        return cookies;
226    }
227
228    /**
229     * Sets the cookie policy of this cookie manager.
230     *
231     * ACCEPT_ORIGINAL_SERVER is the default policy for CookieManager.
232     *
233     * @param cookiePolicy
234     *            the cookie policy. if null, the original policy will not be
235     *            changed.
236     */
237    public void setCookiePolicy(CookiePolicy cookiePolicy) {
238        if (cookiePolicy != null) {
239            policy = cookiePolicy;
240        }
241    }
242
243    /**
244     * Gets current cookie store.
245     *
246     * @return the cookie store currently used by cookie manager.
247     */
248    public CookieStore getCookieStore() {
249        return store;
250    }
251}
252