You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by pe...@apache.org on 2011/07/01 01:49:50 UTC

svn commit: r1141755 - in /wicket/trunk: wicket-core/src/main/java/org/apache/wicket/mock/ wicket-core/src/main/java/org/apache/wicket/protocol/http/ wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ wicket-core/src/main/java/org/apach...

Author: pete
Date: Thu Jun 30 23:49:49 2011
New Revision: 1141755

URL: http://svn.apache.org/viewvc?rev=1141755&view=rev
Log:
WICKET-3845 There is not a good way to use custom response headers with AbstractResource:

- added abstract method WebResponse#addHeader(name) and the implementations in concrete subclasses of WebResponse 
- as a side-effect, use HttpHeadersCollection in MockWebResponse to support multi-value headers
- moved check for forbidden header values in AbstractResource to better place
- added tests for HttpHeaderCollection
- added proper support for dates in HttpHeaderCollection

Added:
    wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/HttpHeaderCollection.java
      - copied, changed from r1141729, wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/HeaderCollection.java
Removed:
    wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/HeaderCollection.java
Modified:
    wicket/trunk/wicket-core/src/main/java/org/apache/wicket/mock/MockWebResponse.java
    wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedWebResponse.java
    wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/HeaderBufferingWebResponse.java
    wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletWebResponse.java
    wicket/trunk/wicket-core/src/main/java/org/apache/wicket/request/resource/AbstractResource.java
    wicket/trunk/wicket-examples/src/main/java/org/apache/wicket/examples/resourcedecoration/MergedResourcesResource.java
    wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/http/WebResponse.java
    wicket/trunk/wicket-request/src/test/java/org/apache/wicket/request/HeadersCollectionTest.java

Modified: wicket/trunk/wicket-core/src/main/java/org/apache/wicket/mock/MockWebResponse.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-core/src/main/java/org/apache/wicket/mock/MockWebResponse.java?rev=1141755&r1=1141754&r2=1141755&view=diff
==============================================================================
--- wicket/trunk/wicket-core/src/main/java/org/apache/wicket/mock/MockWebResponse.java (original)
+++ wicket/trunk/wicket-core/src/main/java/org/apache/wicket/mock/MockWebResponse.java Thu Jun 30 23:49:49 2011
@@ -20,14 +20,13 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 import javax.servlet.http.Cookie;
 
 import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.request.HttpHeaderCollection;
 import org.apache.wicket.request.http.WebResponse;
 import org.apache.wicket.util.lang.Args;
 import org.apache.wicket.util.time.Time;
@@ -127,13 +126,13 @@ public class MockWebResponse extends Web
 		return contentType;
 	}
 
-	private final Map<String, Object> headers = new HashMap<String, Object>();
+	private final HttpHeaderCollection headers = new HttpHeaderCollection();
 
 	@Override
 	public void setDateHeader(String name, Time date)
 	{
 		Args.notNull(date, "date");
-		headers.put(name, date);
+		headers.setDateHeader(name, date);
 	}
 
 	/**
@@ -143,31 +142,32 @@ public class MockWebResponse extends Web
 	 */
 	public Time getDateHeader(String name)
 	{
-		Object value = headers.get(name);
-		if (value == null)
+		final Time time = headers.getDateHeader(name);
+
+		if (time == null)
 		{
 			throw new WicketRuntimeException("Date header '" + name + "' is not set.");
 		}
-		else if (value instanceof Time == false)
-		{
-			throw new WicketRuntimeException("Header '" + name + "' is not date type.");
-		}
-		else
-		{
-			return (Time)value;
-		}
+		return time;
 	}
 
 	@Override
 	public void setHeader(String name, String value)
 	{
-		headers.put(name, value);
+		headers.setHeader(name, value);
+		
 		if (name.equals("Content-Type"))
 		{
 			setContentType(value);
 		}
 	}
 
+	@Override
+	public void addHeader(String name, String value)
+	{
+		headers.addHeader(name, value);
+	}
+
 	/**
 	 * @param name
 	 * 
@@ -175,8 +175,7 @@ public class MockWebResponse extends Web
 	 */
 	public String getHeader(String name)
 	{
-		Object value = headers.get(name);
-		return value != null ? value.toString() : null;
+		return headers.getHeader(name);
 	}
 
 	/**
@@ -186,7 +185,7 @@ public class MockWebResponse extends Web
 	 */
 	public boolean hasHeader(String name)
 	{
-		return headers.containsKey(name);
+		return headers.containsHeader(name);
 	}
 
 	/**
@@ -194,7 +193,7 @@ public class MockWebResponse extends Web
 	 */
 	public Set<String> getHeaderNames()
 	{
-		return Collections.unmodifiableSet(headers.keySet());
+		return headers.getHeaderNames();
 	}
 
 	Integer status;

Modified: wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedWebResponse.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedWebResponse.java?rev=1141755&r1=1141754&r2=1141755&view=diff
==============================================================================
--- wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedWebResponse.java (original)
+++ wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedWebResponse.java Thu Jun 30 23:49:49 2011
@@ -251,6 +251,24 @@ public class BufferedWebResponse extends
 		}
 	}
 
+	private static class AddHeaderAction extends MetaDataAction
+	{
+		private final String name;
+		private final String value;
+
+		public AddHeaderAction(String name, String value)
+		{
+			this.name = name;
+			this.value = value;
+		}
+
+		@Override
+		protected void invoke(WebResponse response)
+		{
+			response.addHeader(name, value);
+		}
+	}
+
 	private static class SetDateHeaderAction extends MetaDataAction
 	{
 		private final String name;
@@ -410,6 +428,12 @@ public class BufferedWebResponse extends
 	}
 
 	@Override
+	public void addHeader(String name, String value)
+	{
+		actions.add(new AddHeaderAction(name, value));
+	}
+
+	@Override
 	public void write(CharSequence sequence)
 	{
 		if (dataAction != null)

Modified: wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/HeaderBufferingWebResponse.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/HeaderBufferingWebResponse.java?rev=1141755&r1=1141754&r2=1141755&view=diff
==============================================================================
--- wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/HeaderBufferingWebResponse.java (original)
+++ wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/HeaderBufferingWebResponse.java Thu Jun 30 23:49:49 2011
@@ -143,6 +143,13 @@ class HeaderBufferingWebResponse extends
 	}
 
 	@Override
+	public void addHeader(String name, String value)
+	{
+		checkHeader();
+		bufferedResponse.addHeader(name, value);
+	}
+
+	@Override
 	public void setStatus(int sc)
 	{
 		bufferedResponse.setStatus(sc);

Modified: wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletWebResponse.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletWebResponse.java?rev=1141755&r1=1141754&r2=1141755&view=diff
==============================================================================
--- wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletWebResponse.java (original)
+++ wicket/trunk/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletWebResponse.java Thu Jun 30 23:49:49 2011
@@ -96,6 +96,12 @@ public class ServletWebResponse extends 
 	}
 
 	@Override
+	public void addHeader(String name, String value)
+	{
+		httpServletResponse.addHeader(name, value);
+	}
+
+	@Override
 	public void write(CharSequence sequence)
 	{
 		try

Modified: wicket/trunk/wicket-core/src/main/java/org/apache/wicket/request/resource/AbstractResource.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-core/src/main/java/org/apache/wicket/request/resource/AbstractResource.java?rev=1141755&r1=1141754&r2=1141755&view=diff
==============================================================================
--- wicket/trunk/wicket-core/src/main/java/org/apache/wicket/request/resource/AbstractResource.java (original)
+++ wicket/trunk/wicket-core/src/main/java/org/apache/wicket/request/resource/AbstractResource.java Thu Jun 30 23:49:49 2011
@@ -26,7 +26,7 @@ import javax.servlet.http.HttpServletRes
 
 import org.apache.wicket.Application;
 import org.apache.wicket.WicketRuntimeException;
-import org.apache.wicket.request.HeaderCollection;
+import org.apache.wicket.request.HttpHeaderCollection;
 import org.apache.wicket.request.Response;
 import org.apache.wicket.request.http.WebRequest;
 import org.apache.wicket.request.http.WebResponse;
@@ -46,6 +46,24 @@ public abstract class AbstractResource i
 {
 	private static final long serialVersionUID = 1L;
 
+	/** header values that are managed internally and must not be set directly */
+	public static final Set<String> INTERNAL_HEADERS; 
+		
+	static
+	{
+		INTERNAL_HEADERS = new HashSet<String>();
+		INTERNAL_HEADERS.add("server");
+		INTERNAL_HEADERS.add("date");
+		INTERNAL_HEADERS.add("expires");
+		INTERNAL_HEADERS.add("last-modified");
+		INTERNAL_HEADERS.add("content-type");
+		INTERNAL_HEADERS.add("content-length");
+		INTERNAL_HEADERS.add("content-disposition");
+		INTERNAL_HEADERS.add("transfer-encoding");
+		INTERNAL_HEADERS.add("connection");
+		INTERNAL_HEADERS.add("content-disposition");
+	}
+	
 	/**
 	 * Construct.
 	 */
@@ -69,9 +87,6 @@ public abstract class AbstractResource i
 	 */
 	public static class ResourceResponse
 	{
-		/** header values that are managed internally and must not be set directly */
-		public static final Set<String> INTERNAL_HEADERS; 
-		
 		private Integer errorCode;
 		private String errorMessage;
 		private String fileName = null;
@@ -83,23 +98,8 @@ public abstract class AbstractResource i
 		private WriteCallback writeCallback;
 		private Duration cacheDuration;
 		private WebResponse.CacheScope cacheScope;
-		private final HeaderCollection headers;
+		private final HttpHeaderCollection headers;
 
-		static
-		{
-			INTERNAL_HEADERS = new HashSet<String>();
-			INTERNAL_HEADERS.add("server");
-			INTERNAL_HEADERS.add("date");
-			INTERNAL_HEADERS.add("expires");
-			INTERNAL_HEADERS.add("last-modified");
-			INTERNAL_HEADERS.add("content-type");
-			INTERNAL_HEADERS.add("content-length");
-			INTERNAL_HEADERS.add("content-disposition");
-			INTERNAL_HEADERS.add("transfer-encoding");
-			INTERNAL_HEADERS.add("connection");
-			INTERNAL_HEADERS.add("content-disposition");
-		}
-		
 		/**
 		 * Construct.
 		 */
@@ -111,7 +111,7 @@ public abstract class AbstractResource i
 			cacheScope = WebResponse.CacheScope.PRIVATE;
 			
 			// collection of directly set response headers
-			headers = new HeaderCollection();
+			headers = new HttpHeaderCollection();
 		}
 
 		/**
@@ -423,84 +423,11 @@ public abstract class AbstractResource i
 		}
 
 		/**
-		 * add a response header value
-		 * 
-		 * you can only set header values that or not already modified by
-		 * the other methods of this class like 'Content-Length', 'Last-Modified', etc.
-		 *  
-		 * @param name
-		 *          header name
-		 * @param value
-		 *          header value
-		 */
-		public void addHeader(String name, String value)
-		{
-			// check if header can be directly access
-			checkHeaderAccess(name);
-			
-			//set header value
-			headers.addHeader(name, value);
-		}
-
-		/**
-		 * get header value
-		 * <p/>
-		 * you can only get header values that or not already handled by
-		 * the other methods of this class like 'Content-Length', 'Last-Modified', etc.
-		 *  
-		 *
-		 * @param name
-		 *          header name
-		 *          
-		 * @return array of header values
+		 * get custom headers
 		 */
-		public String[] getHeaderValues(final String name)
+		public HttpHeaderCollection getHeaders()
 		{
-			// check if header can be directly access
-			checkHeaderAccess(name);
-
-			// get header value
-			return headers.getValues(name);
-		}
-
-		/**
-		 * remove header value
-		 * 
-		 * you can only access header values that or not already modified by
-		 * the other methods of this class like 'Content-Length', 'Last-Modified', etc.
-		 *  
-		 * @param name
-		 *          header name
-		 */
-		public void removeHeader(String name)
-		{
-			// check if header can be directly access
-			checkHeaderAccess(name);
-
-			// remove header value
-			headers.removeHeaderValues(name);
-		}
-
-		/**
-		 * check if header is directly modifyable 
-		 * 
-		 * @param name
-		 *         header name
-		 *
-		 * @throws IllegalArgumentException 
-		 *         if access is forbidden 
-		 */
-		private void checkHeaderAccess(String name)
-		{
-			name = Args.notEmpty(name.trim().toLowerCase(), "name");
-
-			if (INTERNAL_HEADERS.contains(name))
-			{
-				throw new IllegalArgumentException(
-					"you are not allowed to directly access header [" + name + "], " +
-					"use one of the other specialized methods of " + getClass().getSimpleName() +
-					" to get or modify its value");
-			}
+			return headers;
 		}
 	}
 
@@ -555,6 +482,28 @@ public abstract class AbstractResource i
 	}
 
 	/**
+	 * check if header is directly modifyable 
+	 * 
+	 * @param name
+	 *         header name
+	 *
+	 * @throws IllegalArgumentException 
+	 *         if access is forbidden 
+	 */
+	private void checkHeaderAccess(String name)
+	{
+		name = Args.notEmpty(name.trim().toLowerCase(), "name");
+
+		if (INTERNAL_HEADERS.contains(name))
+		{
+			throw new IllegalArgumentException(
+				"you are not allowed to directly access header [" + name + "], " +
+				"use one of the other specialized methods of " + getClass().getSimpleName() +
+				" to get or modify its value");
+		}
+	}
+	
+	/**
 	 * @param data
 	 * @param attributes
 	 */
@@ -628,15 +577,19 @@ public abstract class AbstractResource i
 				webResponse.setContentLength(contentLength);
 			}
 
-			// set additional response headers
-			for (HeaderCollection.Entry entry : data.headers)
+			// add custom headers and values
+			final HttpHeaderCollection headers = data.getHeaders();
+			
+			for (String name : headers.getHeaderNames())
 			{
-				for (String value : entry.getValues())
+				checkHeaderAccess(name);
+				
+				for (String value : headers.getHeaderValues(name))
 				{
-					webResponse.setHeader(entry.getName(), value);
+					webResponse.addHeader(name, value);
 				}
 			}
-
+			
 			// 6. Flush the response
 			// This is necessary for firefox if this resource is an image, otherwise it messes up
 			// other images on page

Modified: wicket/trunk/wicket-examples/src/main/java/org/apache/wicket/examples/resourcedecoration/MergedResourcesResource.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-examples/src/main/java/org/apache/wicket/examples/resourcedecoration/MergedResourcesResource.java?rev=1141755&r1=1141754&r2=1141755&view=diff
==============================================================================
--- wicket/trunk/wicket-examples/src/main/java/org/apache/wicket/examples/resourcedecoration/MergedResourcesResource.java (original)
+++ wicket/trunk/wicket-examples/src/main/java/org/apache/wicket/examples/resourcedecoration/MergedResourcesResource.java Thu Jun 30 23:49:49 2011
@@ -19,6 +19,7 @@ package org.apache.wicket.examples.resou
 import java.io.BufferedReader;
 import java.io.InputStreamReader;
 
+import org.apache.wicket.request.HttpHeaderCollection;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.AbstractResource;
 import org.apache.wicket.util.lang.WicketObjects;
@@ -45,6 +46,14 @@ public class MergedResourcesResource ext
 
 		ResourceResponse resourceResponse = new ResourceResponse();
 
+		HttpHeaderCollection headers = resourceResponse.getHeaders();
+		
+		headers.addHeader(" blA  ", "xx ");
+		headers.addHeader("BLA", "   12xx ");
+		headers.addHeader("xx", "xX12x ");
+		String[] val = headers.getHeaderValues("bla");
+//		resourceResponse.removeHeader("bla");
+		
 		if (resourceResponse.dataNeedsToBeWritten(attributes))
 		{
 			if (isCss)

Copied: wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/HttpHeaderCollection.java (from r1141729, wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/HeaderCollection.java)
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/HttpHeaderCollection.java?p2=wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/HttpHeaderCollection.java&p1=wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/HeaderCollection.java&r1=1141729&r2=1141755&rev=1141755&view=diff
==============================================================================
--- wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/HeaderCollection.java (original)
+++ wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/HttpHeaderCollection.java Thu Jun 30 23:49:49 2011
@@ -16,31 +16,95 @@
  */
 package org.apache.wicket.request;
 
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
 
 import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.time.Time;
 
 /**
  * a multivalue map of headers names and header values suitable for 
- * processing request and response headers.
+ * processing http request and response headers.
  *
  * @author Peter Ertl
  *
  * @since 1.5
  */
-public class HeaderCollection implements Iterable<HeaderCollection.Entry>
+public class HttpHeaderCollection
 {
+	private final Map<HeaderKey, List<Object>> headers;
+
+	/** Greenwich Mean Time (GMT) timezone */
+	private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
+
+	/** rfc 1123 compliant time stamp for headers */
+	private static final String RFC_1123_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
+
+	/** date format for date headers */
+	public static final DateFormat DATE_FORMAT;
+	
+	/** returned in case no header values were found */
 	private static final String[] NO_VALUES = new String[0];
 
-	private final Map<String, List<String>> headers;
+	static
+	{
+		DATE_FORMAT = new SimpleDateFormat(RFC_1123_DATE_FORMAT, Locale.US);
+		DATE_FORMAT.setTimeZone(GMT);
+	}
 
-	public HeaderCollection()
+	public HttpHeaderCollection()
 	{
-		headers = new HashMap<String, List<String>>();
+		headers = new HashMap<HeaderKey, List<Object>>();
+	}
+
+	/**
+	 * internally add new object to header values
+	 *
+	 * @param name
+	 *          header name
+	 * @param object
+	 *          header value (can be a string or a {@link Time} object
+	 */
+	private void internalAdd(String name, Object object)
+	{
+		final HeaderKey key = new HeaderKey(name);
+
+		List<Object> values = headers.get(key);
+
+		if (values == null)
+		{
+			values = new ArrayList<Object>();
+			headers.put(key, values);
+		}
+		values.add(object);
+	}
+
+	/**
+	 * set header value (and remove previous values)
+	 *
+	 * @param name
+	 *          header name
+	 * @param value
+	 *          header value
+	 */
+	public void setHeader(String name, String value)
+	{
+		// remove previous values
+		removeHeader(name);
+
+		// add new values
+		addHeader(name, value);
 	}
 
 	/**
@@ -54,98 +118,178 @@ public class HeaderCollection implements
 	public void addHeader(String name, String value)
 	{
 		// be lenient and strip leading / trailing blanks
-		name = Args.notEmpty(name, "name").trim();
 		value = Args.notEmpty(value, "value").trim();
 
-		List<String> values = headers.get(name);
+		internalAdd(name, value);
+	}
 
-		if (values == null)
-		{
-			values = new ArrayList<String>();
-			headers.put(name, values);
-		}
-		values.add(value);
+	/**
+	 * add date header value
+	 *
+	 * @param name
+	 *          header name
+	 * @param time
+	 *          timestamp
+	 */
+	public void addDateHeader(String name, Time time)
+	{
+		internalAdd(name, time);
 	}
 
 	/**
-	 * remove header values for header name
+	 * add date header value
 	 *
 	 * @param name
 	 *          header name
+	 * @param time
+	 *          timestamp
 	 */
-	public void removeHeaderValues(String name)
+	public void setDateHeader(String name, Time time)
 	{
-		name = Args.notEmpty(name, "name").trim();
+		// remove previous values
+		removeHeader(name);
 
-		final Iterator<Map.Entry<String, List<String>>> it = headers.entrySet().iterator();
+		// add time object to values
+		addDateHeader(name, time);
+	}
+
+	/**
+	 * remove header values for header name
+	 *
+	 * @param name
+	 *          header name
+	 */
+	public void removeHeader(String name)
+	{
+		final HeaderKey key = new HeaderKey(name);
+		final Iterator<Map.Entry<HeaderKey, List<Object>>> it = headers.entrySet().iterator();
 
 		while (it.hasNext())
 		{
-			Map.Entry<String, List<String>> header = it.next();
+			final Map.Entry<HeaderKey, List<Object>> header = it.next();
 
-			if (header.getKey().equalsIgnoreCase(name))
+			if (header.getKey().equals(key))
 			{
 				it.remove();
 			}
 		}
 	}
 
+	private String valueToString(Object value)
+	{
+		if (value instanceof Time)
+		{
+			synchronized(DATE_FORMAT)
+			{
+				return DATE_FORMAT.format(new Date(((Time)value).getMilliseconds()));
+			}
+		}
+		else
+		{
+			return value.toString();
+		}
+	}
+
 	/**
-	 * get header values
-	 *
-	 *
+	 * check if header is defined
 	 *
 	 * @param name
-	 *          header name
-	 *
-	 * @return header value or <code>null</code> if not found
+	 *        header name
+	 * @return <code>true</code> if header has one or more values
 	 */
-	public String[] getValues(String name)
+	public boolean containsHeader(String name)
 	{
-		Args.notEmpty(name, "name");
+		final HeaderKey searchKey = new HeaderKey(name);
 
 		// get the header value (case might differ)
-		for (Map.Entry<String, List<String>> header : headers.entrySet())
+		for (HeaderKey key : headers.keySet())
 		{
-			if (header.getKey().equalsIgnoreCase(name))
+			if (key.equals(searchKey))
 			{
-				return header.getValue().toArray(new String[header.getValue().size()]);
+				return true;
 			}
 		}
-		return NO_VALUES;
+		return false;
 	}
 
 	/**
-	 * get iterator over header values
+	 * returns names of headers
 	 *
-	 * @return iterator
+	 * @return set of header names
 	 */
-	public Iterator<Entry> iterator()
+	public Set<String> getHeaderNames()
 	{
-		final Iterator<Map.Entry<String,List<String>>> iterator = headers.entrySet().iterator();
+		if (headers.isEmpty())
+		{
+			return Collections.emptySet();
+		}
 
-		return new Iterator<Entry>()
+		final Set<String> names = new HashSet<String>(headers.size());
+
+		for (HeaderKey key : headers.keySet())
 		{
-			public boolean hasNext()
-			{
-				return iterator.hasNext();
-			}
+			names.add(key.getName());
+		}
+		return names;
+	}
 
-			public Entry next()
-			{
-				return new Entry(iterator.next());
-			}
+	/**
+	 * get header values (dates will be converted into strings)
+	 *
+	 * @param name
+	 *          header name
+	 *
+	 * @return array of header values or empty array if not found
+	 */
+	public String[] getHeaderValues(String name)
+	{
+		final List<Object> objects = headers.get(new HeaderKey(name));
 
-			public void remove()
-			{
-				throw new UnsupportedOperationException();
-			}
-		};
+		if (objects == null)
+		{
+			return NO_VALUES;
+		}
+
+		final String[] values = new String[objects.size()];
+
+		for (int i = 0; i < values.length; i++)
+		{
+			values[i] = valueToString(objects.get(i));
+		}
+		return values;
+	}
+
+	public String getHeader(String name)
+	{
+		final List<Object> objects = headers.get(new HeaderKey(name));
+
+		if (objects.isEmpty())
+		{
+			return null;
+		}
+		return valueToString(objects.get(0));
+	}
+
+	public Time getDateHeader(String name)
+	{
+		final List<Object> objects = headers.get(new HeaderKey(name));
+
+		if (objects.isEmpty())
+		{
+			return null;
+		}
+		Object object = objects.get(0);
+
+		if ((object instanceof Time) == false)
+		{
+			throw new IllegalStateException("header value is not of type date");
+		}
+		return (Time)object;
 	}
 
 	/**
 	 * check if collection is empty
-	 * 
+	 *
 	 * @return <code>true</code> if collection is empty, <code>false</code> otherwise
 	 */
 	public boolean isEmpty()
@@ -155,7 +299,7 @@ public class HeaderCollection implements
 
 	/**
 	 * get number of headers
-	 * 
+	 *
 	 * @return count
 	 */
 	public int getCount()
@@ -164,25 +308,53 @@ public class HeaderCollection implements
 	}
 
 	/**
-	 * read-only header entry
+	 * clear all headers
+	 */
+	public void clear()
+	{
+		headers.clear();
+	}
+
+	/**
+	 * key for header collection
 	 */
-	public static class Entry
+	private static class HeaderKey
 	{
-		private final Map.Entry<String, List<String>> header;
+		private final String key;
+		private final String name;
 
-		public Entry(Map.Entry<String, List<String>> header)
+		private HeaderKey(String name)
 		{
-			this.header = header;
+			this.name = Args.notEmpty(name, "name").trim();
+			this.key = this.name.toLowerCase(Locale.US);
 		}
 
 		public String getName()
 		{
-			return header.getKey();
+			return name;
+		}
+
+		@Override
+		public boolean equals(Object o)
+		{
+			if (this == o)
+				return true;
+
+			if (!(o instanceof HeaderKey))
+				return false;
+
+			HeaderKey that = (HeaderKey)o;
+
+			if (!key.equals(that.key))
+				return false;
+
+			return true;
 		}
 
-		public String[] getValues()
+		@Override
+		public int hashCode()
 		{
-			return header.getValue().toArray(new String[header.getValue().size()]);
+			return key.hashCode();
 		}
 	}
 }

Modified: wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/http/WebResponse.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/http/WebResponse.java?rev=1141755&r1=1141754&r2=1141755&view=diff
==============================================================================
--- wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/http/WebResponse.java (original)
+++ wicket/trunk/wicket-request/src/main/java/org/apache/wicket/request/http/WebResponse.java Thu Jun 30 23:49:49 2011
@@ -62,6 +62,14 @@ public abstract class WebResponse extend
 	public abstract void setHeader(String name, String value);
 
 	/**
+	 * Add a value to the servlet response stream.
+	 * 
+	 * @param name
+	 * @param value
+	 */
+	public abstract void addHeader(String name, String value);
+	
+	/**
 	 * Set a header to the date value in the servlet response stream.
 	 * 
 	 * @param name
@@ -109,7 +117,7 @@ public abstract class WebResponse extend
 	public void setAttachmentHeader(final String filename)
 	{
 		setHeader("Content-Disposition", "attachment" +
-			((!Strings.isEmpty(filename)) ? ("; filename=\"" + filename + "\"") : ""));
+		                                 ((!Strings.isEmpty(filename))? ("; filename=\"" + filename + "\"") : ""));
 	}
 
 	/**
@@ -206,7 +214,8 @@ public abstract class WebResponse extend
 		setDateHeader("Expires", now.add(duration));
 
 		// Enable caching and set max age
-		setHeader("Cache-Control", scope.cacheControl + ", max-age=" + duration.getMilliseconds());
+		setHeader("Cache-Control", scope.cacheControl);
+		addHeader("Cache-Control", "max-age=" + duration.getMilliseconds());
 	}
 
 	/**

Modified: wicket/trunk/wicket-request/src/test/java/org/apache/wicket/request/HeadersCollectionTest.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket-request/src/test/java/org/apache/wicket/request/HeadersCollectionTest.java?rev=1141755&r1=1141754&r2=1141755&view=diff
==============================================================================
--- wicket/trunk/wicket-request/src/test/java/org/apache/wicket/request/HeadersCollectionTest.java (original)
+++ wicket/trunk/wicket-request/src/test/java/org/apache/wicket/request/HeadersCollectionTest.java Thu Jun 30 23:49:49 2011
@@ -16,6 +16,10 @@
  */
 package org.apache.wicket.request;
 
+import java.util.Locale;
+import java.util.Set;
+
+import org.apache.wicket.util.time.Time;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
@@ -25,25 +29,83 @@ public class HeadersCollectionTest
 	@Test
 	public void testHeaderCollection()
 	{
-		HeaderCollection headers = new HeaderCollection();
+		HttpHeaderCollection headers = new HttpHeaderCollection();
 		assertTrue(headers.isEmpty());
 
 		headers.addHeader("X-Test", "foo");
 		headers.addHeader("X-Test", "bar");
-		assertArrayEquals(new String[]{"foo", "bar"}, headers.getValues("X-Test"));
+		assertArrayEquals(new String[]{"foo", "bar"}, headers.getHeaderValues("X-Test"));
 
-		headers.removeHeaderValues("x-test");
+		headers.removeHeader("x-test");
 		assertTrue(headers.isEmpty());
 
 		headers.addHeader("   X-Image    ", "    jpeg     ");
 		headers.addHeader("X-Image    ", "    gif     ");
-		assertArrayEquals(new String[]{"jpeg", "gif"}, headers.getValues("X-IMAGE"));
+		assertArrayEquals(new String[]{"jpeg", "gif"}, headers.getHeaderValues("X-IMAGE"));
 		assertEquals(1, headers.getCount());
 
 		headers.addHeader("X-Test", "123");
 		assertEquals(2, headers.getCount());
 
-		headers.removeHeaderValues(" x-tesT ");
+		headers.removeHeader(" x-tesT ");
 		assertEquals(1, headers.getCount());
 	}
+
+	@Test
+	public void getHeaderNames()
+	{
+		final HttpHeaderCollection headers = new HttpHeaderCollection();
+
+		headers.addHeader("key1", "a");
+		headers.addHeader("Key1", "b");
+		headers.addHeader("key2", "c");
+
+		Set<String> names = headers.getHeaderNames();
+		assertTrue(names.contains("key1"));
+		assertFalse(names.contains("Key1"));
+		assertTrue(names.contains("key2"));
+	}
+
+	@Test
+	public void dateValues()
+	{
+		final HttpHeaderCollection headers = new HttpHeaderCollection();
+
+		final Time time1 = Time.millis(1000000);
+		final Time time2 = Time.millis(2000000);
+
+		headers.setDateHeader("date", time1);
+		headers.addDateHeader("date", time2);
+		headers.addHeader("date", "not-a-date");
+
+		assertEquals(time1, headers.getDateHeader("date"));
+		assertEquals("Thu, 01 Jan 1970 00:16:40 GMT", headers.getHeader("date"));
+
+		// a change of the locale must not affect the date format
+		final Locale defaultLocale = Locale.getDefault();
+
+		try
+		{
+			Locale.setDefault(Locale.CHINESE);
+			assertEquals("Thu, 01 Jan 1970 00:16:40 GMT", headers.getHeader("date"));
+		}
+		finally
+		{
+			Locale.setDefault(defaultLocale);
+		}
+
+		assertArrayEquals(new String[]{"Thu, 01 Jan 1970 00:16:40 GMT", "Thu, 01 Jan 1970 00:33:20 GMT", "not-a-date"},
+		                  headers.getHeaderValues("date"));
+		
+		headers.setHeader("date", "foobar");
+		try
+		{
+			Time date = headers.getDateHeader("date");
+			fail();
+		}
+		catch (IllegalStateException e)
+		{
+			// ok
+		}
+	}
 }