Управление cookie при помощи класса CookieHandler

На Java платформе доступ к объектам через унифицированный указатель информационного ресурса (URL) управляется набором обработчиков протоколов. Первая часть URL определяет используемый протокол. Например, при указании в начале URL строки file:, вы можете обращаться к ресурсам на локальном диске. В случае использования http: в URL доступ к ресурсу осуществляется через Internet. J2SE 5.0 определяет следующие типы обработчиков протоколов: http, https, file, ftp и jar.

В качестве части реализации обработчика протокола http в J2SE 5.0 добавлен класс CookieHandler. Данный класс позволяет управлять состоянием системы при помощи cookie. Cookie представляет собой данные, сохраняемые в кеше браузера. Если вы посетили web сайт, то при повторном посещении данные cookie используются для идентификации вас как пользователя ранее бывшего на данном сайте. Cookie позволяют сохранять пользовательскую информацию как, например, состояние корзины покупателя в Internet магазине. Cookie могут быть краткосрочными, содержащими данные в течение одной сессии, до тех пор пока не закрыть браузер, или долгосрочные, содержащими данные в течение недели или года.

По умолчанию J2SE 5.0 не предоставляет стандартный обработчик. Однако вы можете зарегистрировать обработчик для приложения, позволяя ему создавать и получать cookie.

Вернемся к обсуждению класса CookieHandler, он является абстрактным классом и имеет две пары связанных методов. Первая пара позволяет вам определить установленный обработчик или установить свой собственный:

  • getDefault()
  • setDefault(CookieHandler)

Для приложений с установленным менеджером безопасности, получение и установка обработчика требует наличия специального разрешения. Для удаления текущего обработчика, установите значение обработчика как null. Как было ранее замечено, по умолчанию обработчик не создается.

Вторая пара методов позволяет вам записывать и получать cookie из кеша.

  • get(URI uri, Map<String, List<String >> requestHeaders)
  • put(URI uri, Map<String, List<String>> responseHeaders)

Метод get() получает значения из cookie, хранящихся в кеше, и добавляет их к списку requestHeaders. Метод put() получает значения из списка responseHeaders и сохраняет их в кеше в виде cookie.

Все выглядит просто и действительно создание обработчика достаточно легкая процедура. А вот определение кеша требует немного большей работы. Для примера, давайте рассмотрим произвольный экземпляр CookieHandler, сookie кеш и тестовая программа. Вот как выглядит тестовая программа:

import java.io.*;
import java.net.*;
import java.util.*;

public class Fetch {
 
public static void main(String args[]) throws Exception {
   
if (args.length == 0) {
     
System.err.println("URL missing");
      System.exit
(-1);
   
}
   
String urlString = args[0];
    CookieHandler.setDefault
(new ListCookieHandler());
    URL url =
new URL(urlString);
    URLConnection connection = url.openConnection
();
    Object obj = connection.getContent
();
    url =
new URL(urlString);
    connection = url.openConnection
();
    obj = connection.getContent
();
 
}
}

Данная программа изначально создает и устанавливает объект класса ListCookieHandler, который будет определен далее. Затем она устанавливает соединение с адресом URL (переданном как параметр командной строки) и чиатет данные. Затем программа устанавливает другое соединение с адресом URL и читает те же самые данные. При первом чтении данных, ответ содержит cookie, которые будут сохранены. Второй запрос включает в себя сохраненные cookie.

Давайте рассмотрим реализацию вышесказанного при помощи класса URLConnection. Ресурсы Web адресуются при помощи URL. При создании объекта URL вы получаете доступ к входящему и исходящему потокам для связи с сайтом при помощи класса URLConnection.

String urlString = ...;
URL url =
new URL(urlString);
URLConnection connection = url.openConnection
();
InputStream is = connection.getInputStream
();

// .. чтение данных из потока

Частью информации получаемой при соединении является набор заголовков. Данные заголовки зависят от используемого протокола. Для определения заголовков вы можете использовать класс URLConnection. Данный класс содержит набор методов для получения информации о заголовках:

  • getHeaderFields() – отображает доступные поля;
  • getHeaderField(String name) – отображает заголовок с заданным именем;
  • getHeaderFieldDate(String name, long default) – отображает дату поля заголовка;
  • getHeaderFieldInt(String name, int default) – отображает номер поля заголовка;
  • getHeaderFieldKey(int n) или getHeaderField(int n) – отображает поле заголовка с заданным положением.

Например, следующая программа отображает все заголовки для заданного URL.

import java.net.*;
import java.util.*;

public class ListHeaders {
 
public static void main(String args[]) throws Exception {
   
if (args.length == 0) {
     
System.err.println("URL missing");
   
}
   
String urlString = args[0];
    URL url =
new URL(urlString);
    URLConnection connection = url.openConnection
();
    Map<String, List<String>> headerFields = connection.getHeaderFields
();
    Set<String> set = headerFields.keySet
();
    Iterator<String> itor = set.iterator
();
   
while (itor.hasNext()) {
     
String key = itor.next();
      System.out.println
("Key: " + key + " / " + headerFields.get(key));
   
}
  }
}

Программа ListHeaders, принимает в качестве аргумента URL, например, http://java.sun.com и выводит все заголовки, полученные с данного сайта. Каждый заголовок отображается в формате:

Key: <key> / [<value>]

Таким образом, набрав

>> java ListHeaders http://java.sun.com

На экране вы увидите следующие строки:

   Key: Set-Cookie / [SUN_ID=192.168.0.1:269421125489956;
   EXPIRES=Wednesday,
31- Dec-2025 23:59:59 GMT;
   DOMAIN=.sun.com; PATH=/
]
  
Key: Set-cookie /
  
[JSESSIONID=688047FA45065E07D8792CF650B8F0EA;Path=/]
  
Key: null / [HTTP/1.1 200 OK]
  
Key: Transfer-encoding / [chunked]
  
Key: Date / [Wed, 31 Aug 2005 12:05:56 GMT]
  
Key: Server / [Sun-ONE-Web-Server/6.1]
  
Key: Content-type / [text/html;charset=ISO-8859-1]

(Длинные строки, в данном примере были разбиты вручную).

Данный пример отображает только заголовки для введенного URL и не отображает страницу, расположенную по данному адресу. Заметьте, что данная информация содержит тип web сервера, используемого на сайте и дату и время локальной системы. Также, обратите внимание на две строки с ключом "Set-Cookie". Данные строки относятся к cookie. Cookie могут быть сохранены из данных заголовков, а затем переданы при следующем запросе.

Теперь давайте создадим класс CookieHandler. Для этого нужно реализовать два абстрактных метода get() и put()класса CookieHandler.

public void put( URI uri, Map<String, List<String>> responseHeaders) throws IOException

public Map<String, List<String>> get( URI uri, Map<String, List<String>> requestHeaders) throws IOException

Начнем с метода put(). Он сохраняет в кеш все cookie, включенные в заголовках ответа. Для реализации данного метода, получим список заголовков "Set-Cookie". Данный метод может быть расширен для получения других подходящих заголовков, как Set-cookie и Set-Cookie2.

List<String> setCookieList = responseHeaders.get("Set-Cookie");

Теперь обработаем список cookie в цикле и сохраним каждое значение. В случае наличия cookie, заменим существующее значение новым.

if (setCookieList != null) {
 
for (String item : setCookieList) {
   
Cookie cookie = new Cookie(uri, item);
   
// Замените существующие значения cookie в кеше на новые
   
for (Cookie existingCookie : cache) {
     
...
   
}
   
System.out.println("Adding to cache: " + cookie);
    cache.add
(cookie);
 
}
}

В данном методе "cache" может быть реализован по разному, через базу данных или с помощью элементов списка. Класс Cookie, не является предопределенным классом и будет описан позднее.

По существу, для реализации метода put() больше ничего не требуется. Каждый cookie в заголовке ответа сохраняется методом в кеш.

Метод get() действует в обратном направлении. Каждый cookie, хранящийся в кеше и соответствующий, указанному URI, добавляется методом get() в заголовок ответа. Для нескольких cookie создайте список, разделенный запятыми. В результате выполнения метод get() возвращает таблицу. Он получает параметр Map, содержащий набор заголовков. Вам необходимо добавить к данному параметру соответствующие cookie из кеша. Но данный параметр является фиксированной таблицей, таким образом вам необходимо вернуть другую фиксированную таблицу. Таким образом вы должны создать рабочую копию существующей таблицы и после внесения изменений вернуть фиксированную таблицу.

Для реализации метода get(), найдите в кеше соответствующие cookie, а затем удалите просроченные cookie.

// получите все cookie, соответствующие данному URI
// запишите их в список, разделенный запятыми
StringBuilder cookies = new StringBuilder();
for (Cookie cookie : cache) {
 
// удалите просроченные cookie
 
if (cookie.hasExpired()) {
   
cache.remove(cookie);
 
} else if (cookie.matches(uri)) {
   
if (cookies.length() > 0) {
     
cookies.append(", ");
   
}
   
cookies.append(cookie.toString());
 
}
}

Класс Cookie, будет определен далее. Здесь также используется два других метода: hasExpired() и matches(). Метод hasExpired(), определяет не является ли данный cookie просроченным. Метод matches(), проверяет соответствие cookie и URI, переданному методу.

На следующем этапе метод get(), помещает объект StringBuilder в фиксированную таблицу, с соответствующими ключами "Cookie:"

// Возвращаемая таблица
Map<String, List<String>> cookieMap = new HashMap<String, List<String>>(requestHeaders);

// Преобразование StringBuilder в List для сохранения в таблице
if (cookies.length() > 0) {
 
List<String> list = Collections.singletonList(cookies.toString());
  cookieMap.put
("Cookie", list);
}
return Collections.unmodifiableMap(cookieMap);

Ниже представлено определение класса CookieHandler, с несколькими добавленными операторами println(), выводящими информацию о выполнении.

import java.io.*;
  
import java.net.*;
  
import java.util.*;

  
public class ListCookieHandler extends CookieHandler {

  
private List cache = new LinkedList();

    
/**
     * Сохранение всех cookie, находящихся в заголовках ответа в кеше.
     *
     *
@param uri
     *            URI источник cookie
     *
@param responseHeaders
     *            Неизменяемая таблица, преобразованная в список,
     *            представляющий поля заголовка ответа
     */

    
public void put(
        
URI uri,
         Map> responseHeaders
)
          
throws IOException {

      
System.out.println("Cache: " + cache);
       List setCookieList =
         responseHeaders.get
("Set-Cookie");
      
if (setCookieList != null) {
        
for (String item : setCookieList) {
          
Cookie cookie = new Cookie(uri, item);
          
// Удаления уже существующих cookie
           // Установка новых щначений
          
for (Cookie existingCookie : cache) {
            
if((cookie.getURI().equals(
              
existingCookie.getURI())) &&
               
(cookie.getName().equals(
                 
existingCookie.getName()))) {
             
cache.remove(existingCookie);
             
break;
           
}
          }
         
System.out.println("Adding to cache: " + cookie);
          cache.add
(cookie);
       
}
      }
    }

   
/**
   * Получение всех cookie из кеша, соответствующих uri в заголовке запроса.
   *
   *
@param uri
   *            URI для отправки cookie
   *
@param requestHeaders
   *            Таблица заголовков запроса, преобразованная список полей,
   *            содержащих заголовки запроса
   */

   
public Map> get(
       
URI uri,
        Map> requestHeaders
)
         
throws IOException {

     
// Получение cookie для соответствующего URI
      // Сохранение в списке, разделенном запятыми
     
StringBuilder cookies = new StringBuilder();
     
for (Cookie cookie : cache) {
       
// Удаление просроченных cookie
       
if (cookie.hasExpired()) {
         
cache.remove(cookie);
       
} else if (cookie.matches(uri)) {
         
if (cookies.length() > 0) {
           
cookies.append(", ");
         
}
         
cookies.append(cookie.toString());
       
}
      }

     
// Возвращаемая таблица
     
Map> cookieMap =
       
new HashMap>(requestHeaders);

     
// Преобразование StringBuilder в List, для сохранения в таблице
     
if (cookies.length() > 0) {
       
List list =
          Collections.singletonList
(cookies.toString());
        cookieMap.put
("Cookie", list);
     
}
       
System.out.println("Cookies: " + cookieMap);
   
return Collections.unmodifiableMap(cookieMap);
   
}
  }

Остается только написать реализацию класса Cookie. Основная логика реализуется в конструкторе класса. В конструкторе вам необходимо проанализировать информацию об URI и заголовках. Дата срока действия должна быть представлена в одном формате, хотя различные web-сайты представляют ее по-разному. Здесь нет ничего сложного, просто сохраняйте информацию в виде пути cookie, даты срока действия и домена.

public Cookie(URI uri, String header) {
    
String attributes[] = header.split(";");
     String nameValue = attributes
[0].trim();
    
this.uri = uri;
    
this.name = nameValue.substring(0, nameValue.indexOf('='));
    
this.value = nameValue.substring(nameValue.indexOf('=')+1);
    
this.path = "/";
    
this.domain = uri.getHost();

    
for (int i=1; i < attributes.length; i++) {
      
nameValue = attributes[i].trim();
      
int equals = nameValue.indexOf('=');
      
if (equals == -1) {
        
continue;
      
}
      
String name = nameValue.substring(0, equals);
       String value = nameValue.substring
(equals+1);
      
if (name.equalsIgnoreCase("domain")) {
        
String uriDomain = uri.getHost();
        
if (uriDomain.equals(value)) {
          
this.domain = value;
        
} else {
          
if (!value.startsWith(".")) {
            
value = "." + value;
          
}
          
uriDomain =
             uriDomain.substring
(uriDomain.indexOf('.'));
          
if (!uriDomain.equals(value)) {
            
throw new IllegalArgumentException(
              
"Trying to set foreign cookie");
          
}
          
this.domain = value;
        
}
       }
else if (name.equalsIgnoreCase("path")) {
        
this.path = value;
      
} else if (name.equalsIgnoreCase("expires")) {
        
try {
          
this.expires = expiresFormat1.parse(value);
        
} catch (ParseException e) {
          
try {
            
this.expires = expiresFormat2.parse(value);
          
} catch (ParseException e2) {
            
throw new IllegalArgumentException(
              
"Bad date format in header: " + value);
          
}
         }
       }
     }
  }

Другие методы класса возвращают хранимые данные или проверяют срок действия.

public boolean hasExpired() {
    
if (expires == null) {
      
return false;
    
}
    
Date now = new Date();
    
return now.after(expires);
  
}

  
public String toString() {
    
StringBuilder result = new StringBuilder(name);
     result.append
("=");
     result.append
(value);
    
return result.toString();
  
}

Соответствие не должно быть определено в случае истечения срока действия.

public boolean matches(URI uri) {

 
if (hasExpired()) {
   
return false;
 
}

 
String path = uri.getPath();
 
if (path == null) {
   
path = "/"
 
}
}

Заметьте, что согласно спецификации cookie необходимо проверять соответствие как домен, так и путь cookie. Для упрощения мы проверяем только соответствие пути.

Ниже представлено полное описание класса Cookie

import java.net.*;
import java.text.*;
import java.util.*;

public class Cookie {

 
String name;
  String value;
  URI uri;
  String domain;
  Date expires;
  String path;

 
private static DateFormat expiresFormat1 = new SimpleDateFormat(
     
"E, dd MMM yyyy k:m:s 'GMT'");

 
private static DateFormat expiresFormat2 = new SimpleDateFormat(
     
"E, dd-MMM-yyyy k:m:s 'GMT'");

 
/**
   * Создание cookie из  URI и полей заголовка
   *
   *
@param uri URI для cookie
   *
@param header установка атрибутов заголовка
   */
 
public Cookie(URI uri, String header) {
   
String attributes[] = header.split(";");
    String nameValue = attributes
[0].trim();
   
this.uri = uri;
   
this.name = nameValue.substring(0, nameValue.indexOf('='));
   
this.value = nameValue.substring(nameValue.indexOf('=') + 1);
   
this.path = "/";
   
this.domain = uri.getHost();

   
for (int i = 1; i < attributes.length; i++) {
     
nameValue = attributes[i].trim();
     
int equals = nameValue.indexOf('=');
     
if (equals == -1) {
       
continue;
     
}
     
String name = nameValue.substring(0, equals);
      String value = nameValue.substring
(equals + 1);
     
if (name.equalsIgnoreCase("domain")) {
       
String uriDomain = uri.getHost();
       
if (uriDomain.equals(value)) {
         
this.domain = value;
       
} else {
         
if (!value.startsWith(".")) {
           
value = "." + value;
         
}
         
uriDomain = uriDomain.substring(uriDomain.indexOf('.'));
         
if (!uriDomain.equals(value)) {
           
throw new IllegalArgumentException(
               
"Trying to set foreign cookie");
         
}
         
this.domain = value;
       
}
      }
else if (name.equalsIgnoreCase("path")) {
       
this.path = value;
     
} else if (name.equalsIgnoreCase("expires")) {
       
try {
         
this.expires = expiresFormat1.parse(value);
       
} catch (ParseException e) {
         
try {
           
this.expires = expiresFormat2.parse(value);
         
} catch (ParseException e2) {
           
throw new IllegalArgumentException(
               
"Bad date format in header: " + value);
         
}
        }
      }
    }
  }

 
public boolean hasExpired() {
   
if (expires == null) {
     
return false;
   
}
   
Date now = new Date();
   
return now.after(expires);
 
}

 
public String getName() {
   
return name;
 
}

 
public URI getURI() {
   
return uri;
 
}

 
/**
   * Проверка срока действия cookie и соответствие URI
   *
   *
@param uri URI для проверки
   *
@return true в случае соответствия,  иначе false
   */
 
public boolean matches(URI uri) {

   
if (hasExpired()) {
     
return false;
   
}

   
String path = uri.getPath();
   
if (path == null) {
     
path = "/";
   
}

   
return path.startsWith(this.path);
 
}

 
public String toString() {
   
StringBuilder result = new StringBuilder(name);
    result.append
("=");
    result.append
(value);
   
return result.toString();
 
}
}

Теперь реализовав все компоненты, вы можете запустить пример Fetch.

>> java Fetch http://java.sun.com

Cookies: {Connection=[keep-alive], Host=[java.sun.com], 
User-Agent=[Java/1.5.0_04], GET / HTTP/1.1=[null], 
Content-type=[application/x-www-form-urlencoded], 
Accept=[text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2]}
Cache: []
Adding to cache: SUN_ID=192.168.0.1:235411125667328
Cookies: {Connection=[keep-alive], Host=[java.sun.com], 
User-Agent=[Java/1.5.0_04], GET / HTTP/1.1=[null], 
Cookie=[SUN_ID=192.168.0.1:235411125667328], 
Content-type=[application/x-www-form-urlencoded], 
Accept=[text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2]}
Cache: [SUN_ID=192.168.0.1:235411125667328]

(Длинные строки, в данном примере были разбиты вручную).

Строки, начинающиеся с "Cache:", отображают значения из кеша. Заметьте, что метод get() вызывается перед методом put(), таким образом сохраненные cookie не возвращаются немедленно.

За дополнительной информацией по работе с cookie и соединениями c URL обращайтесь к разделу Networking trail в Java Tutorial. Но поскольку он описывает J2SE 1.4, вы не найдете в нем описания класса CookieHandler. Вы можете найти стандартную реализацию класса CookieHandler в Java SE 6 ("Mustang").

Теги: cookies j2ee