001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2009 Sun Microsystems Inc. 015 * Portions Copyright 2010–2011 ApexIdentity Inc. 016 * Portions Copyright 2011-2016 ForgeRock AS. 017 */ 018 019package org.forgerock.http.protocol; 020 021import static org.forgerock.http.header.HeaderFactory.*; 022 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.TreeMap; 030 031import org.forgerock.http.header.GenericHeader; 032import org.forgerock.http.header.HeaderFactory; 033import org.forgerock.http.header.MalformedHeaderException; 034 035/** 036 * Message headers, a case-insensitive multiple-value map. 037 */ 038public class Headers implements Map<String, Object> { 039 040 private final Map<String, Header> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 041 042 /** 043 * Constructs a {@code Headers} object that is case-insensitive for header names. 044 */ 045 public Headers() { } 046 047 /** 048 * Defensive copy constructor. 049 */ 050 Headers(final Headers headers) { 051 // Force header re-creation 052 for (Map.Entry<String, Header> entry : headers.asMapOfHeaders().entrySet()) { 053 add(entry.getKey(), entry.getValue().getValues()); 054 } 055 } 056 057 /** 058 * Rich-type friendly get method. 059 * @param key The name of the header. 060 * @return The header object. 061 */ 062 @Override 063 public Header get(Object key) { 064 return headers.get(key); 065 } 066 067 /** 068 * Gets the first value of the header, or null if the header does not exist. 069 * @param key The name of the header. 070 * @return The first header value. 071 */ 072 public String getFirst(String key) { 073 final Header header = headers.get(key); 074 return header == null ? null : header.getFirstValue(); 075 } 076 077 /** 078 * Gets the first value of the header, or null if the header does not exist. 079 * @param key The name of the header. 080 * @return The first header value. 081 */ 082 public String getFirst(Class<? extends Header> key) { 083 final Header header = headers.get(getHeaderName(key)); 084 return header == null ? null : header.getFirstValue(); 085 } 086 087 /** 088 * Returns the specified {@link Header} or {code null} if the header is not included in the message. 089 * 090 * @param headerType The type of header. 091 * @param <H> The type of header. 092 * @return The header instance, or null if none exists. 093 * @throws MalformedHeaderException When the header was not well formed, and so could not be parsed as 094 * its richly-typed class. 095 */ 096 public <H extends Header> H get(Class<H> headerType) throws MalformedHeaderException { 097 final Header header = this.get(getHeaderName(headerType)); 098 if (header instanceof GenericHeader) { 099 throw new MalformedHeaderException("Header value(s) are not well formed"); 100 } 101 return headerType.cast(header); 102 } 103 104 private <H extends Header> String getHeaderName(Class<H> headerType) { 105 final String headerName = HEADER_NAMES.get(headerType); 106 if (headerName == null) { 107 throw new IllegalArgumentException("Unknown header type: " + headerType); 108 } 109 return headerName; 110 } 111 112 /** 113 * A script compatible putAll method that will accept {@code Header}, {@code String}, {@code Collection<String>} 114 * and {@code String[]} values. 115 * @param m A map of header names to values. 116 */ 117 @Override 118 public void putAll(Map<? extends String, ? extends Object> m) { 119 for (Map.Entry<? extends String, ? extends Object> entry : m.entrySet()) { 120 put(entry.getKey(), entry.getValue()); 121 } 122 } 123 124 /** 125 * A script compatible put method that will accept a {@code Header}, {@code String}, {@code Collection<String>} 126 * and {@code String[]} value. 127 * @param key The name of the header. 128 * @param value A {@code Header}, {@code String}, {@code Collection<String>} or {@code String[]}. 129 * @return The previous {@code Header} value for this header, or null. 130 */ 131 @Override 132 public Header put(String key, Object value) { 133 if (value == null) { 134 return remove(key); 135 } 136 final HeaderFactory<?> factory = FACTORIES.get(key); 137 if (value instanceof Header) { 138 return putHeader(key, (Header) value, factory); 139 } else if (factory != null) { 140 return putUsingFactory(key, value, factory); 141 } else { 142 return putGenericHeader(key, value); 143 } 144 } 145 146 private Header putGenericHeader(String key, Object value) { 147 if (value instanceof String) { 148 return putGenericString(key, (String) value); 149 } else if (value instanceof List) { 150 return putGenericList(key, (List<?>) value); 151 } else if (value instanceof Collection) { 152 return putGenericList(key, new ArrayList<>((Collection<?>) value)); 153 } else if (value.getClass().isArray()) { 154 return putGenericList(key, Arrays.asList((Object[]) value)); 155 } 156 throw new IllegalArgumentException("Cannot put object for key '" + key + "': " + value); 157 } 158 159 private Header putHeader(String key, Header header, HeaderFactory<?> factory) { 160 if (!hasAnyValue(header)) { 161 return remove(key); 162 } 163 if (HEADER_NAMES.containsValue(key) && !HEADER_NAMES.get(header.getClass()).equals(key)) { 164 if (header instanceof GenericHeader) { 165 return putUsingFactory(key, header.getValues(), factory); 166 } 167 throw new IllegalArgumentException("Header object of incorrect type for header " + key); 168 } 169 return headers.put(key, header); 170 } 171 172 private boolean hasAnyValue(Header header) { 173 boolean hasValue = false; 174 for (String s : header.getValues()) { 175 if (s != null) { 176 hasValue = true; 177 break; 178 } 179 } 180 return hasValue; 181 } 182 183 @SuppressWarnings("unchecked") 184 private Header putGenericList(String key, List<?> value) { 185 if (value.isEmpty()) { 186 return remove(key); 187 } 188 for (Object o : value) { 189 if (!(o instanceof String)) { 190 throw new IllegalArgumentException("Collections must be of strings"); 191 } 192 } 193 return headers.put(key, new GenericHeader(key, (List<String>) value)); 194 } 195 196 private Header putGenericString(String key, String value) { 197 return headers.put(key, new GenericHeader(key, value)); 198 } 199 200 private Header putUsingFactory(String key, Object value, HeaderFactory<?> factory) { 201 final Header parsed; 202 try { 203 parsed = factory.parse(value); 204 } catch (MalformedHeaderException e) { 205 if (value instanceof Header) { 206 value = ((Header) value).getValues(); 207 } 208 return putGenericHeader(key, value); 209 } 210 if (parsed == null) { 211 return remove(key); 212 } 213 return headers.put(key, parsed); 214 } 215 216 /** 217 * Rich-type friendly remove method. Removes the {@code Header} object for the given header name. 218 * @param key The header name. 219 * @return The header value before removal, or null. 220 */ 221 @Override 222 public Header remove(Object key) { 223 return headers.remove(key); 224 } 225 226 /** 227 * A put method to add a particular {@code Header} instance. Will overwrite any existing value for this 228 * header name. 229 * @param header The header instance. 230 * @return The previous {@code Header} value for the header with the same name, or null. 231 */ 232 public Header put(Header header) { 233 return put(header.getName(), header); 234 } 235 236 /** 237 * An add method to add a particular {@code Header} instance. Existing values for the header will be added to. 238 * 239 * @param header The header instance. 240 */ 241 public void add(Header header) { 242 add(header.getName(), header); 243 } 244 245 /** 246 * A script compatible add method that will accept a {@code Header}, {@code String}, {@code Collection<String>} 247 * and {@code String[]} value. Existing values for the header will be added to. 248 * @param key The name of the header. 249 * @param value A {@code Header}, {@code String}, {@code Collection<String>} or {@code String[]}. 250 */ 251 @SuppressWarnings("unchecked") 252 public void add(String key, Object value) { 253 if (value == null) { 254 return; 255 } 256 List<String> values = containsKey(key) ? new ArrayList<>(get(key).getValues()) : new ArrayList<String>(); 257 if (value instanceof Header) { 258 for (String s : ((Header) value).getValues()) { 259 addNonNullStringValue(values, s); 260 } 261 } else if (value instanceof String) { 262 values.add((String) value); 263 } else if (value instanceof Collection) { 264 final Collection<String> collection = (Collection<String>) value; 265 for (String s : collection) { 266 addNonNullStringValue(values, s); 267 } 268 } else if (value.getClass().isArray()) { 269 String[] array = (String[]) value; 270 for (String s : array) { 271 addNonNullStringValue(values, s); 272 } 273 } else { 274 throw new IllegalArgumentException("Cannot add values for key '" + key + "': " + value); 275 } 276 if (values.isEmpty()) { 277 return; 278 } 279 final HeaderFactory<?> factory = FACTORIES.get(key); 280 if (factory != null) { 281 Header parsed; 282 try { 283 parsed = factory.parse(values); 284 } catch (MalformedHeaderException e) { 285 parsed = new GenericHeader(key, values); 286 } 287 if (parsed == null) { 288 return; 289 } 290 headers.put(key, parsed); 291 } else { 292 headers.put(key, new GenericHeader(key, values)); 293 } 294 } 295 296 private void addNonNullStringValue(List<String> values, String s) { 297 if (s != null) { 298 values.add(s); 299 } 300 } 301 302 /** 303 * A script compatible addAll method that will accept a {@code Header}, {@code String}, {@code Collection<String>} 304 * and {@code String[]} value. Existing values for the headers will be added to. 305 * @param map A map of header names to values. 306 */ 307 public void addAll(Map<? extends String, ? extends Object> map) { 308 for (Map.Entry<? extends String, ? extends Object> entry : map.entrySet()) { 309 add(entry.getKey(), entry.getValue()); 310 } 311 } 312 313 @Override 314 public int size() { 315 return headers.size(); 316 } 317 318 @Override 319 public boolean isEmpty() { 320 return headers.isEmpty(); 321 } 322 323 @Override 324 public boolean containsKey(Object key) { 325 return headers.containsKey(key); 326 } 327 328 @Override 329 public boolean containsValue(Object value) { 330 return headers.containsValue(value); 331 } 332 333 @Override 334 public void clear() { 335 headers.clear(); 336 } 337 338 @Override 339 public Set<String> keySet() { 340 return headers.keySet(); 341 } 342 343 @Override 344 @SuppressWarnings("unchecked") 345 public Collection<Object> values() { 346 return (Collection) headers.values(); 347 } 348 349 @Override 350 @SuppressWarnings("unchecked") 351 public Set<Entry<String, Object>> entrySet() { 352 return (Set) headers.entrySet(); 353 } 354 355 /** 356 * The {@code Headers} class extends {@code Map<String, Object>} to support flexible parameters in scripting. This 357 * method allows access to the underlying {@code Map<String, Header>}. 358 * @return The map of header names to {@link Header} objects. 359 */ 360 public Map<String, Header> asMapOfHeaders() { 361 return headers; 362 } 363 364 /** 365 * Returns a copy of these headers as a multi-valued map of strings. Changes to the returned map will not be 366 * reflected in these headers, nor will changes in these headers be reflected in the returned map. 367 * 368 * @return a copy of these headers as a multi-valued map of strings. 369 */ 370 public Map<String, List<String>> copyAsMultiMapOfStrings() { 371 Map<String, List<String>> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 372 for (Header header : headers.values()) { 373 result.put(header.getName(), new ArrayList<>(header.getValues())); 374 } 375 return result; 376 } 377 378}