Author: mheath
Date: Thu Mar  6 22:19:15 2008
New Revision: 634555

URL: http://svn.apache.org/viewvc?rev=634555&view=rev
Log:
Added support for decoding HTTP response Set-Cookie headers.

Added:
    
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DateParseException.java
    
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DateUtil.java
    mina/asyncweb/trunk/common/src/test/catalina/webapps/ROOT/cookie.jsp
    mina/asyncweb/trunk/common/src/test/catalina/webapps/ROOT/redirect.jsp
Modified:
    
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DefaultHttpRequest.java
    
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/HttpHeaderConstants.java
    
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/HttpResponseDecodingState.java
    
mina/asyncweb/trunk/common/src/test/java/org/apache/asyncweb/common/integration/ResponseOutput.java
    
mina/asyncweb/trunk/common/src/test/java/org/apache/asyncweb/common/integration/SimpleHttpTest.java

Added: 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DateParseException.java
URL: 
http://svn.apache.org/viewvc/mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DateParseException.java?rev=634555&view=auto
==============================================================================
--- 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DateParseException.java
 (added)
+++ 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DateParseException.java
 Thu Mar  6 22:19:15 2008
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.asyncweb.common;
+
+/**
+ * An exception to indicate an error parsing a date string.
+ *
+ * @see DateUtil
+ * @author Michael Becke
+ */
+public class DateParseException extends Exception {
+
+    /**
+     * Instantiates a new date parse exception.
+     */
+    DateParseException() {
+        super();
+    }
+
+    /**
+     * Instantiates a new date parse exception.
+     *
+     * @param message the message
+     */
+    DateParseException(String message) {
+        super(message);
+    }
+
+}
\ No newline at end of file

Added: 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DateUtil.java
URL: 
http://svn.apache.org/viewvc/mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DateUtil.java?rev=634555&view=auto
==============================================================================
--- 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DateUtil.java
 (added)
+++ 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DateUtil.java
 Thu Mar  6 22:19:15 2008
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.asyncweb.common;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * A utility class for parsing and formatting HTTP dates as used in cookies and
+ * other headers.  This class handles dates as defined by RFC 2616 section
+ * 3.3.1 as well as some other common non-standard formats.
+ *
+ * @author Christopher Brown
+ * @author Michael Becke
+ */
+final class DateUtil {
+
+    /**
+     * Date format pattern used to parse HTTP date headers in RFC 1123 format.
+     */
+    public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss 
zzz";
+
+    /**
+     * Date format pattern used to parse HTTP date headers in RFC 1036 format.
+     */
+    public static final String PATTERN_RFC1036 = "EEEE, dd-MMM-yy HH:mm:ss 
zzz";
+
+    /**
+     * Date format pattern used to parse HTTP date headers in ANSI C
+     * <code>asctime()</code> format.
+     */
+    public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
+
+    private static final Collection<String> DEFAULT_PATTERNS = Arrays.asList(
+        new String[] {PATTERN_ASCTIME, PATTERN_RFC1036, PATTERN_RFC1123});
+
+    private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
+
+    static {
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(2000, Calendar.JANUARY, 1, 0, 0);
+        DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
+    }
+
+    private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
+
+    /**
+     * This class should not be instantiated.
+     */
+    private DateUtil() {
+    }
+
+
+    /**
+     * Parses a date value.  The formats used for parsing the date value are 
retrieved from
+     * the default http params.
+     *
+     * @param dateValue the date value to parse
+     * @return the parsed date
+     * @throws DateParseException if the value could not be parsed using any 
of the
+     *                            supported date formats
+     */
+    public static Date parseDate(String dateValue) throws DateParseException {
+        return parseDate(dateValue, null, null);
+    }
+
+    /**
+     * Parses the date value using the given date formats.
+     *
+     * @param dateValue   the date value to parse
+     * @param dateFormats the date formats to use
+     * @return the parsed date
+     * @throws DateParseException if none of the dataFormats could parse the 
dateValue
+     */
+    public static Date parseDate(String dateValue, Collection<String> 
dateFormats)
+        throws DateParseException {
+        return parseDate(dateValue, dateFormats, null);
+    }
+
+    /**
+     * Parses the date value using the given date formats.
+     *
+     * @param dateValue   the date value to parse
+     * @param dateFormats the date formats to use
+     * @param startDate   During parsing, two digit years will be placed in 
the range
+     *                    <code>startDate</code> to <code>startDate + 100 
years</code>. This value may
+     *                    be <code>null</code>. When <code>null</code> is 
given as a parameter, year
+     *                    <code>2000</code> will be used.
+     * @return the parsed date
+     * @throws DateParseException if none of the dataFormats could parse the 
dateValue
+     */
+    public static Date parseDate(
+        String dateValue,
+        Collection<String> dateFormats,
+        Date startDate
+    ) throws DateParseException {
+
+        if (dateValue == null) {
+            throw new IllegalArgumentException("dateValue is null");
+        }
+        if (dateFormats == null) {
+            dateFormats = DEFAULT_PATTERNS;
+        }
+        if (startDate == null) {
+            startDate = DEFAULT_TWO_DIGIT_YEAR_START;
+        }
+        // trim single quotes around date if present
+        // see issue #5279
+        if (dateValue.length() > 1
+            && dateValue.startsWith("'")
+            && dateValue.endsWith("'")
+            ) {
+            dateValue = dateValue.substring(1, dateValue.length() - 1);
+        }
+
+        SimpleDateFormat dateParser = null;
+        for (String format : dateFormats) {
+            if (dateParser == null) {
+                dateParser = new SimpleDateFormat(format, Locale.US);
+                dateParser.setTimeZone(TimeZone.getTimeZone("GMT"));
+                dateParser.set2DigitYearStart(startDate);
+            } else {
+                dateParser.applyPattern(format);
+            }
+            try {
+                return dateParser.parse(dateValue);
+            } catch (ParseException pe) {
+                // ignore this exception, we will try the next format
+            }
+        }
+
+        // we were unable to parse the date
+        throw new DateParseException("Unable to parse the date " + dateValue);
+    }
+
+    /**
+     * Formats the given date according to the RFC 1123 pattern.
+     *
+     * @param date The date to format.
+     * @return An RFC 1123 formatted date string.
+     * @see #PATTERN_RFC1123
+     */
+    public static String formatDate(Date date) {
+        return formatDate(date, PATTERN_RFC1123);
+    }
+
+    /**
+     * Formats the given date according to the specified pattern.  The pattern
+     * must conform to that used by the [EMAIL PROTECTED] 
java.text.SimpleDateFormat simple date
+     * format} class.
+     *
+     * @param date    The date to format.
+     * @param pattern The pattern to use for formatting the date.
+     * @return A formatted date string.
+     * @throws IllegalArgumentException If the given date pattern is invalid.
+     * @see java.text.SimpleDateFormat
+     */
+    public static String formatDate(Date date, String pattern) {
+        if (date == null) {
+            throw new IllegalArgumentException("date is null");
+        }
+        if (pattern == null) {
+            throw new IllegalArgumentException("pattern is null");
+        }
+
+        SimpleDateFormat formatter = new SimpleDateFormat(pattern, Locale.US);
+        formatter.setTimeZone(GMT);
+        return formatter.format(date);
+    }
+
+}
\ No newline at end of file

Modified: 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DefaultHttpRequest.java
URL: 
http://svn.apache.org/viewvc/mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DefaultHttpRequest.java?rev=634555&r1=634554&r2=634555&view=diff
==============================================================================
--- 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DefaultHttpRequest.java
 (original)
+++ 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/DefaultHttpRequest.java
 Thu Mar  6 22:19:15 2008
@@ -43,7 +43,7 @@
  * A default implementation of [EMAIL PROTECTED] MutableHttpRequest}.
  * 
  * @author The Apache MINA Project ([EMAIL PROTECTED])
- * @version $Rev$, $Date$
+ * @version $Rev: 615489 $, $Date: 2008-01-26 13:59:06 -0700 (Sat, 26 Jan 
2008) $
  */
 public class DefaultHttpRequest extends DefaultHttpMessage implements
         MutableHttpRequest {
@@ -382,7 +382,7 @@
         Set<Cookie> cookies = getCookies();
         if (!cookies.isEmpty()) {
             // Clear previous values.
-            removeHeader(HttpHeaderConstants.KEY_SET_COOKIE);
+            removeHeader(HttpHeaderConstants.KEY_COOKIE);
             
             // And encode.
             for (Cookie c: cookies) {

Modified: 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/HttpHeaderConstants.java
URL: 
http://svn.apache.org/viewvc/mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/HttpHeaderConstants.java?rev=634555&r1=634554&r2=634555&view=diff
==============================================================================
--- 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/HttpHeaderConstants.java
 (original)
+++ 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/HttpHeaderConstants.java
 Thu Mar  6 22:19:15 2008
@@ -23,7 +23,7 @@
  * HTTP Header Constants.
  *
  * @author The Apache MINA Project ([EMAIL PROTECTED])
- * @version $Rev$, $Date$
+ * @version $Rev: 615489 $, $Date: 2008-01-26 13:59:06 -0700 (Sat, 26 Jan 
2008) $
  */
 public class HttpHeaderConstants {
 
@@ -95,12 +95,12 @@
     public static final String KEY_DATE = "Date";
     
     /**
-     * The "cookie" header.
+     * The "cookie" request header.
      */
     public static final String KEY_COOKIE = "Cookie";
 
     /**
-     * The "set-cookie" header.
+     * The "set-cookie" response header.
      */
     public static final String KEY_SET_COOKIE = "Set-Cookie";
 
@@ -108,6 +108,11 @@
      * The "host" header.
      */
     public static final String KEY_HOST = "Host";
+
+    /**
+     * The "location" header.
+     */
+    public static final String KEY_LOCATION = "Location";
 
     private HttpHeaderConstants() {
     }

Modified: 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/HttpResponseDecodingState.java
URL: 
http://svn.apache.org/viewvc/mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/HttpResponseDecodingState.java?rev=634555&r1=634554&r2=634555&view=diff
==============================================================================
--- 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/HttpResponseDecodingState.java
 (original)
+++ 
mina/asyncweb/trunk/common/src/main/java/org/apache/asyncweb/common/HttpResponseDecodingState.java
 Thu Mar  6 22:19:15 2008
@@ -47,15 +47,13 @@
  * each new parse.
  *
  * @author The Apache MINA Project ([EMAIL PROTECTED])
- * @version $Rev$, $Date$
+ * @version $Rev: 615489 $, $Date: 2008-01-26 13:59:06 -0700 (Sat, 26 Jan 
2008) $
  */
 abstract class HttpResponseDecodingState extends DecodingStateMachine {
 
     private static final Logger LOG = LoggerFactory
             .getLogger(HttpResponseDecodingState.class);
 
-    private static final String HEADER_COOKIE = "Cookie";
-
     /**
      * The header which provides a requests transfer coding
      */
@@ -76,21 +74,25 @@
      */
     private static final char EXTENSION_CHAR = ';';
 
+    public static final String COOKIE_COMMENT = "comment";
+    
+    public static final String COOKIE_DOMAIN = "domain";
+    
+    public static final String COOKIE_EXPIRES = "expires";
+    
+    public static final String COOKIE_MAX_AGE = "max-age";
+    
+    public static final String COOKIE_PATH = "path";
+    
+    public static final String COOKIE_SECURE = "secure";
+    
+    public static final String COOKIE_VERSION = "version";
+
     /**
      * The request we are building
      */
     private MutableHttpResponse response;
 
-    private boolean parseCookies = true;
-
-    public boolean isParseCookies() {
-        return parseCookies;
-    }
-
-    public void setParseCookies(boolean parseCookies) {
-        this.parseCookies = parseCookies;
-    }
-
     @Override
     protected DecodingState init() throws Exception {
         response = new DefaultHttpResponse();
@@ -154,17 +156,12 @@
                 ProtocolDecoderOutput out) throws Exception {
             Map<String, List<String>> headers = (Map<String, List<String>>) 
childProducts
                     .get(0);
-            if (parseCookies) {
-                List<String> cookies = headers.remove(HEADER_COOKIE);
-                if (cookies != null && !cookies.isEmpty()) {
-                    if (cookies.size() > 1) {
-                        if (LOG.isWarnEnabled()) {
-                            LOG.warn("Ignoring extra cookie headers: "
-                                    + cookies.subList(1, cookies.size()));
-                        }
-                    }
-                    // FIXME Parse cookies.
-                    //response.setCookies(cookies.get(0));
+
+            // Parse cookies
+            List<String> cookies = 
headers.get(HttpHeaderConstants.KEY_SET_COOKIE);
+            if (cookies != null && !cookies.isEmpty()) {
+                for (String cookie : cookies) {
+                    response.addCookie(parseCookie(cookie));
                 }
             }
             response.setHeaders(headers);
@@ -253,6 +250,43 @@
                 }
             }
             return nextState;
+        }
+
+        private Cookie parseCookie(String cookieHeader) throws 
DateParseException {
+
+            MutableCookie cookie = null;
+
+            String pairs[] = cookieHeader.split(";");
+            for (int i = 0; i < pairs.length; i++) {
+                String nameValue[] = pairs[i].trim().split("=");
+                String name = nameValue[0].trim();
+                String value = (nameValue.length == 2) ? nameValue[1].trim() : 
null;
+
+                //First pair is the cookie name/value
+                if (i == 0) {
+                    cookie = new DefaultCookie(name, value);
+                } else if (name.equalsIgnoreCase(COOKIE_COMMENT)) {
+                    cookie.setComment(value);
+                } else if (name.equalsIgnoreCase(COOKIE_PATH)) {
+                    cookie.setPath(value);
+                } else if (name.equalsIgnoreCase(COOKIE_SECURE)) {
+                    cookie.setSecure(true);
+                } else if (name.equalsIgnoreCase(COOKIE_VERSION)) {
+                    cookie.setVersion(Integer.parseInt(value));
+                } else if (name.equalsIgnoreCase(COOKIE_MAX_AGE)) {
+                    int age = Integer.parseInt(value);
+                    cookie.setMaxAge(age);
+                } else if (name.equalsIgnoreCase(COOKIE_EXPIRES)) {
+                    long createdDate = System.currentTimeMillis();
+                    int age = (int)(DateUtil.parseDate(value).getTime() - 
createdDate) / 1000;
+                    cookie.setCreatedDate(createdDate);
+                    cookie.setMaxAge(age);
+                } else if (name.equalsIgnoreCase(COOKIE_DOMAIN)) {
+                    cookie.setDomain(value);
+                }
+            }
+
+            return cookie;
         }
 
         /**

Added: mina/asyncweb/trunk/common/src/test/catalina/webapps/ROOT/cookie.jsp
URL: 
http://svn.apache.org/viewvc/mina/asyncweb/trunk/common/src/test/catalina/webapps/ROOT/cookie.jsp?rev=634555&view=auto
==============================================================================
--- mina/asyncweb/trunk/common/src/test/catalina/webapps/ROOT/cookie.jsp (added)
+++ mina/asyncweb/trunk/common/src/test/catalina/webapps/ROOT/cookie.jsp Thu 
Mar  6 22:19:15 2008
@@ -0,0 +1,8 @@
+<%
+    Cookie cookie = new Cookie(
+               
org.apache.asyncweb.common.integration.ResponseOutput.COOKIE_NAME,
+               
org.apache.asyncweb.common.integration.ResponseOutput.COOKIE_VALUE
+               );
+    
cookie.setMaxAge(org.apache.asyncweb.common.integration.ResponseOutput.COOKIE_MAX_AGE);
+    response.addCookie(cookie);
+%>
\ No newline at end of file

Added: mina/asyncweb/trunk/common/src/test/catalina/webapps/ROOT/redirect.jsp
URL: 
http://svn.apache.org/viewvc/mina/asyncweb/trunk/common/src/test/catalina/webapps/ROOT/redirect.jsp?rev=634555&view=auto
==============================================================================
--- mina/asyncweb/trunk/common/src/test/catalina/webapps/ROOT/redirect.jsp 
(added)
+++ mina/asyncweb/trunk/common/src/test/catalina/webapps/ROOT/redirect.jsp Thu 
Mar  6 22:19:15 2008
@@ -0,0 +1,3 @@
+<%
+    response.sendRedirect("/helloworld.jsp");
+%>

Modified: 
mina/asyncweb/trunk/common/src/test/java/org/apache/asyncweb/common/integration/ResponseOutput.java
URL: 
http://svn.apache.org/viewvc/mina/asyncweb/trunk/common/src/test/java/org/apache/asyncweb/common/integration/ResponseOutput.java?rev=634555&r1=634554&r2=634555&view=diff
==============================================================================
--- 
mina/asyncweb/trunk/common/src/test/java/org/apache/asyncweb/common/integration/ResponseOutput.java
 (original)
+++ 
mina/asyncweb/trunk/common/src/test/java/org/apache/asyncweb/common/integration/ResponseOutput.java
 Thu Mar  6 22:19:15 2008
@@ -3,5 +3,9 @@
 public class ResponseOutput {
 
     public static final String HELLO_WORLD = "Hello HTTP World";
+
+    public static final String COOKIE_NAME = "test";
+    public static final String COOKIE_VALUE = "value";
+    public static final int COOKIE_MAX_AGE = 3600;
     
 }

Modified: 
mina/asyncweb/trunk/common/src/test/java/org/apache/asyncweb/common/integration/SimpleHttpTest.java
URL: 
http://svn.apache.org/viewvc/mina/asyncweb/trunk/common/src/test/java/org/apache/asyncweb/common/integration/SimpleHttpTest.java?rev=634555&r1=634554&r2=634555&view=diff
==============================================================================
--- 
mina/asyncweb/trunk/common/src/test/java/org/apache/asyncweb/common/integration/SimpleHttpTest.java
 (original)
+++ 
mina/asyncweb/trunk/common/src/test/java/org/apache/asyncweb/common/integration/SimpleHttpTest.java
 Thu Mar  6 22:19:15 2008
@@ -1,8 +1,11 @@
 package org.apache.asyncweb.common.integration;
 
 import java.nio.charset.Charset;
+import java.util.Set;
 
+import org.apache.asyncweb.common.Cookie;
 import org.apache.asyncweb.common.DefaultHttpRequest;
+import org.apache.asyncweb.common.HttpHeaderConstants;
 import org.apache.asyncweb.common.HttpResponse;
 import org.apache.asyncweb.common.HttpResponseStatus;
 import org.apache.asyncweb.common.MutableHttpRequest;
@@ -23,5 +26,51 @@
         assertEquals(HttpResponseStatus.OK, response.getStatus());
         assertEquals(ResponseOutput.HELLO_WORLD, 
response.getContent().getString(Charset.defaultCharset().newDecoder()));
     }
+    
+    public void testRedirectResponse() throws Exception {
+        // Send request
+        MutableHttpRequest request = new DefaultHttpRequest();
+        request.setRequestUri(getBaseURI().resolve("/redirect.jsp"));
+        request.normalize();
+        session.write(request);
+        
+        // Wait for response
+        HttpResponse response = (HttpResponse) 
session.read().await().getMessage();
+        
+        // Test response
+        assertEquals(HttpResponseStatus.FOUND, response.getStatus());
+        assertEquals(getBaseURI().resolve("/helloworld.jsp").toString(), 
response.getHeader(HttpHeaderConstants.KEY_LOCATION));
+    }
+
+    public void testCookieResponse() throws Exception {
+        // Send request
+        MutableHttpRequest request = new DefaultHttpRequest();
+        request.setRequestUri(getBaseURI().resolve("/cookie.jsp"));
+        request.normalize();
+        session.write(request);
+        
+        // Wait for response
+        HttpResponse response = (HttpResponse) 
session.read().await().getMessage();
+        // Test response
+        System.out.println(response.getHeaders());
+        assertEquals(HttpResponseStatus.OK, response.getStatus());
+        Set<Cookie> cookies = response.getCookies();
+        assertEquals(2, cookies.size());
+
+           Cookie cookie = findCookie(ResponseOutput.COOKIE_NAME, cookies);
+               assertNotNull(cookie);
+        assertEquals(ResponseOutput.COOKIE_NAME, cookie.getName());
+        assertEquals(ResponseOutput.COOKIE_VALUE, cookie.getValue());
+           assertTrue(Math.abs(ResponseOutput.COOKIE_MAX_AGE - 
cookie.getMaxAge()) < 100); // Make sure the cookie max age is close (because 
of difference from date decoding
+    }
+
+       private Cookie findCookie(String name, Iterable<Cookie> cookies) {
+               for (Cookie cookie : cookies) {
+                       if (cookie.getName().equals(name)) {
+                               return cookie;
+                       }
+               }
+               return null;
+       }
     
 }


Reply via email to