blob: c33411f9d1cb578d161c7b6edd9a716023a19015 [file] [log] [blame]
// ***************************************************************************************************************************
// * 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.juneau.http.header;
import static org.apache.juneau.assertions.Assertions.*;
import static org.apache.juneau.internal.ExceptionUtils.*;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import org.apache.http.*;
import org.apache.http.util.*;
import org.apache.juneau.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.http.HttpHeaders;
import org.apache.juneau.internal.*;
import org.apache.juneau.svl.*;
/**
* An immutable list of HTTP headers.
* {@review}
*
* <h5 class='figure'>Example</h5>
* <p class='bcode w800'>
* HeaderList <jv>headers</jv> = HeaderList
* .<jsm>create</jsm>()
* .append(Accept.<jsm>of</jsm>("text/xml"))
* .append(<js>"Content-Type"</js>, ()-><jsm>getDynamicContentTypeFromSomewhere</jsm>())
* .build();
* </p>
*
* <p>
* Convenience creators are provided for creating lists with minimal code:
* <p class='bcode w800'>
* HeaderList <jv>headers</jv> = HeaderList.<jsm>of</jsm>(Accept.<jsf>TEXT_XML</jsf>, ContentType.<jsf>TEXT_XML</jsf>);
* </p>
*
* <p>
* Header lists are immutable, but can be appended to using the {@link #copy()} method:
* <p class='bcode w800'>
* headers = headers
* .copy()
* .append(AcceptEncoding.<jsm>of</jsm>(<js>"identity"</js>))
* .build();
* </p>
*
* <p>
* Static methods are provided on {@link HttpHeaders} to further simplify creation of header lists.
* <p class='bcode w800'>
* <jk>import static</jk> org.apache.juneau.http.HttpHeaders.*;
*
* HeaderList <jv>headers</jv> = <jsm>headerList</jsm>(<jsm>accept</jsm>(<js>"text/xml"</js>), <jsm>contentType</jsm>(<js>"text/xml"<js>));
* </p>
*
* <p>
* The builder class supports setting default header values (i.e. add a header to the list if it isn't otherwise in the list).
* Note that this is different from simply setting a value twice as using default values will not overwrite existing
* headers.
* <br>The following example notes the distinction:
*
* <p class='bcode w800'>
* <jv>headers</jv> = HeaderList
* .<jsm>create</jsm>()
* .set(Accept.<jsf>TEXT_PLAIN</jsf>)
* .set(Accept.<jsf>TEXT_XML</jsf>)
* .build();
* <jsm>assertObject</jsm>(<jv>headers</jv>).isString(<js>"[Accept: text/xml]"</js>);
*
* <jv>headers</jv> = HeaderList
* .create()
* .set(Accept.<jsf>TEXT_PLAIN</jsf>)
* .setDefault(Accept.<jsf>TEXT_XML</jsf>)
* .build();
* <jsm>assertObject</jsm>(<jv>headers</jv>).isString(<js>"[Accept: text/plain]"</js>);
* </p>
*
* <p>
* Various methods are provided for iterating over the headers in this list to avoid array copies.
* <ul class='javatree'>
* <li class='jm'>{@link #forEach(Consumer)} / {@link #forEach(String,Consumer)} - Use consumers to process headers.
* <li class='jm'>{@link #iterator()} / {@link #iterator(String)} - Use an {@link HeaderIterator} to process headers.
* <li class='jm'>{@link #stream()} / {@link #stream(String)} - Use a stream.
* </ul>
* <p>
* In general, try to use these over the {@link #getAll()}/{@link #getAll(String)} methods that require array copies.
*
* <p>
* The {@link #get(String)} method is special in that it will collapse multiple headers with the same name into
* a single comma-delimited list (see <a href='https://tools.ietf.org/html/rfc2616#section-4.2'>RFC 2616 Section 4.2</a> for rules).
*
* <p>
* The {@link #get(Class)} and {@link #get(String, Class)} methods are provided for working with {@link org.apache.juneau.http.annotation.Header}-annotated
* beans.
*
* <h5 class='figure'>Example</h5>
* <p class='bcode w800'>
* ContentType <jv>contentType</jv> = <jv>headers</jv>.get(ContentType.<jk>class</jk>);
* </p>
*
* <p>
* By default, header names are treated as case-insensitive. This can be changed using the {@link HeaderListBuilder#caseSensitive()}
* method.
*
* <p>
* A {@link VarResolver} can be associated with this builder to create header values with embedded variables that
* are resolved at runtime.
*
* <h5 class='figure'>Example</h5>
* <p class='bcode w800'>
* <jc>// Create a header list with dynamically-resolving values pulled from a system property.</jc>
*
* System.<jsm>setProperty</jsm>(<js>"foo"</js>, <js>"bar"</js>);
*
* HeaderList <jv>headers</jv> = HeaderList
* .<jsm>create</jsm>()
* .resolving()
* .append(<js>"X1"</js>, <js>"$S{foo}"</js>)
* .append(<js>"X2"</js>, ()-><js>"$S{foo}"</js>)
* .build();
*
* <jsm>assertObject</jsm>(<jv>headers</jv>).isString(<js>"[X1: bar, X2: bar]"</js>);
* </p>
*
* <p>
* The {@link HeaderList} object can be extended to defined pre-packaged lists of headers which can be used in various
* annotations throughout the framework.
*
* <h5 class='figure'>Example</h5>
* <p class='bcode w800'>
* <jc>// A predefined list of headers.</jc>
* <jk>public class</jk> MyHeaderList <jk>extends</jk> HeaderList {
* <jk>public</jk> MyHeaderList() {
* <jk>super</jk>(Accept.<jsf>TEXT_XML</jsf>, ContentType.<jsf>TEXT_XML</jsf>);
* }
* }
*
* <jc>// Use it on a remote proxy to add headers on all requests.</jc>
* <ja>@Remote</ja>(path=<js>"/petstore"</js>, headerList=MyHeaderList.<jk>class</jk>)
* <jk>public interface</jk> PetStore {
*
* <ja>@RemotePost</ja>(<js>"/pets"</js>)
* Pet addPet(
* <ja>@Body</ja> CreatePet <jv>createPet</jv>,
* <ja>@Header</ja>(<js>"E-Tag"</js>) UUID <jv>etag</jv>,
* <ja>@Query</ja>(<js>"debug"</js>) <jk>boolean</jk> <jv>debug</jv>
* );
* }
* </p>
*/
@ThreadSafe
public class HeaderList {
private static final Header[] EMPTY_ARRAY = new Header[0];
/** Represents no header supplier in annotations. */
public static final class Null extends HeaderList {}
/** Predefined instance. */
public static final HeaderList EMPTY = new HeaderList();
final Header[] entries;
final boolean caseSensitive;
/**
* Instantiates a new builder for this bean.
*
* @return A new builder.
*/
public static HeaderListBuilder create() {
return new HeaderListBuilder();
}
/**
* Creates a new {@link HeaderList} initialized with the specified headers.
*
* @param headers
* The headers to add to the list.
* <br>Can be <jk>null</jk>.
* <br><jk>null</jk> entries are ignored.
* @return A new unmodifiable instance, never <jk>null</jk>.
*/
public static HeaderList of(List<Header> headers) {
return headers == null || headers.isEmpty() ? EMPTY : new HeaderList(headers);
}
/**
* Creates a new {@link HeaderList} initialized with the specified headers.
*
* @param headers
* The headers to add to the list.
* <br><jk>null</jk> entries are ignored.
* @return A new unmodifiable instance, never <jk>null</jk>.
*/
public static HeaderList of(Header...headers) {
return headers == null || headers.length == 0 ? EMPTY : new HeaderList(headers);
}
/**
* Creates a new {@link HeaderList} initialized with the specified name/value pairs.
*
* <h5 class='figure'>Example</h5>
* <p class='bcode w800'>
* HeaderList <jv>headers</jv> = HeaderList.<jsm>ofPairs</jsm>(<js>"Accept"</js>, <js>"text/xml"</js>, <js>"Content-Type"</js>, <js>"text/xml"</js>);
* </p>
*
* @param pairs
* Initial list of pairs.
* <br>Must be an even number of parameters representing key/value pairs.
* @throws RuntimeException If odd number of parameters were specified.
* @return A new instance.
*/
public static HeaderList ofPairs(String...pairs) {
if (pairs == null || pairs.length == 0)
return EMPTY;
if (pairs.length % 2 != 0)
throw runtimeException("Odd number of parameters passed into HeaderList.ofPairs()");
ArrayBuilder<Header> b = ArrayBuilder.create(Header.class, pairs.length / 2, true);
for (int i = 0; i < pairs.length; i+=2)
b.add(BasicHeader.of(pairs[i], pairs[i+1]));
return new HeaderList(b.toArray());
}
/**
* Constructor.
*
* @param builder The builder containing the settings for this bean.
*/
public HeaderList(HeaderListBuilder builder) {
if (builder.defaultEntries == null) {
entries = builder.entries.toArray(new Header[builder.entries.size()]);
} else {
ArrayBuilder<Header> l = ArrayBuilder.create(Header.class, builder.entries.size() + builder.defaultEntries.size(), true);
for (int i = 0, j = builder.entries.size(); i < j; i++)
l.add(builder.entries.get(i));
for (int i1 = 0, j1 = builder.defaultEntries.size(); i1 < j1; i1++) {
Header x = builder.defaultEntries.get(i1);
boolean exists = false;
for (int i2 = 0, j2 = builder.entries.size(); i2 < j2 && ! exists; i2++)
exists = eq(builder.entries.get(i2).getName(), x.getName());
if (! exists)
l.add(x);
}
entries = l.toArray();
}
this.caseSensitive = builder.caseSensitive;
}
/**
* Constructor.
*
* @param headers
* The headers to add to the list.
* <br>Can be <jk>null</jk>.
* <br><jk>null</jk> entries are ignored.
*/
protected HeaderList(List<Header> headers) {
ArrayBuilder<Header> l = ArrayBuilder.create(Header.class, headers.size(), true);
for (int i = 0, j = headers.size(); i < j; i++)
l.add(headers.get(i));
entries = l.toArray();
caseSensitive = false;
}
/**
* Constructor.
*
* @param headers
* The headers to add to the list.
* <br><jk>null</jk> entries are ignored.
*/
protected HeaderList(Header...headers) {
ArrayBuilder<Header> l = ArrayBuilder.create(Header.class, headers.length, true);
for (int i = 0; i < headers.length; i++)
l.add(headers[i]);
entries = l.toArray();
caseSensitive = false;
}
/**
* Default constructor.
*/
protected HeaderList() {
entries = EMPTY_ARRAY;
caseSensitive = false;
}
/**
* Returns a builder initialized with the contents of this bean.
*
* @return A new builder object.
*/
public HeaderListBuilder copy() {
return new HeaderListBuilder(this);
}
/**
* Gets a header representing all of the header values with the given name.
*
* <p>
* If more that one header with the given name exists the values will be combined with <js>", "</js> as per
* <a href='https://tools.ietf.org/html/rfc2616#section-4.2'>RFC 2616 Section 4.2</a>.
*
* @param name The header name.
* @return A header with a condensed value, or {@link Optional#empty()} if no headers by the given name are present
*/
public Optional<Header> get(String name) {
Header first = null;
List<Header> rest = null;
for (int i = 0; i < entries.length; i++) {
Header x = entries[i];
if (eq(x.getName(), name)) {
if (first == null)
first = x;
else {
if (rest == null)
rest = new ArrayList<>();
rest.add(x);
}
}
}
if (first == null)
return Optional.empty();
if (rest == null)
return Optional.of(first);
CharArrayBuffer sb = new CharArrayBuffer(128);
sb.append(first.getValue());
for (int i = 0; i < rest.size(); i++) {
sb.append(", ");
sb.append(rest.get(i).getValue());
}
return Optional.of(new BasicHeader(name, sb.toString()));
}
/**
* Gets a header representing all of the header values with the given name.
*
* <p>
* If more that one header with the given name exists the values will be combined with <js>", "</js> as per
* <a href='https://tools.ietf.org/html/rfc2616#section-4.2'>RFC 2616 Section 4.2</a>.
*
* <p>
* The implementation class must have a public constructor taking in one of the following argument lists:
* <ul>
* <li><c>X(String <jv>value</jv>)</c>
* <li><c>X(Object <jv>value</jv>)</c>
* <li><c>X(String <jv>name</jv>, String <jv>value</jv>)</c>
* <li><c>X(String <jv>name</jv>, Object <jv>value</jv>)</c>
* </ul>
*
* <h5 class='figure'>Example</h5>
* <p class='bcode w800'>
* BasicIntegerHeader <jv>age</jv> = headerList.get(<js>"Age"</js>, BasicIntegerHeader.<jk>class</jk>);
* </p>
*
* @param name The header name.
* @param type The header implementation class.
* @return A header with a condensed value or <jk>null</jk> if no headers by the given name are present
*/
public <T> Optional<T> get(String name, Class<T> type) {
Header first = null;
List<Header> rest = null;
for (int i = 0; i < entries.length; i++) {
Header x = entries[i];
if (eq(x.getName(), name)) {
if (first == null)
first = x;
else {
if (rest == null)
rest = new ArrayList<>();
rest.add(x);
}
}
}
if (first == null)
return Optional.empty();
if (rest == null)
return Optional.of(HeaderBeanMeta.of(type).construct(name, first.getValue()));
CharArrayBuffer sb = new CharArrayBuffer(128);
sb.append(first.getValue());
for (int i = 0; i < rest.size(); i++) {
sb.append(", ");
sb.append(rest.get(i).getValue());
}
return Optional.of(HeaderBeanMeta.of(type).construct(name, sb.toString()));
}
/**
* Gets a header representing all of the header values with the given name.
*
* <p>
* Same as {@link #get(String, Class)} but the header name is pulled from the {@link org.apache.juneau.http.annotation.Header#name()} or
* {@link org.apache.juneau.http.annotation.Header#value()} annotations.
*
* <h5 class='figure'>Example</h5>
* <p class='bcode w800'>
* Age <jv>age</jv> = headerList.get(Age.<jk>class</jk>);
* </p>
*
* @param type The header implementation class.
* @return A header with a condensed value or <jk>null</jk> if no headers by the given name are present
*/
public <T> Optional<T> get(Class<T> type) {
assertArgNotNull("type", type);
String name = HeaderBeanMeta.of(type).getSchema().getName();
if (name == null)
throw new BasicIllegalArgumentException("Header name could not be found on bean type ''{0}''", type.getName());
return get(name, type);
}
/**
* Gets all of the headers with the given name.
*
* <p>
* The returned array maintains the relative order in which the headers were added.
*
* <p>
* Header name comparison is case insensitive.
*
* @param name The header name.
*
* @return An array containing all matching headers, or an empty array if none are found.
*/
public Header[] getAll(String name) {
List<Header> l = null;
for (int i = 0; i < entries.length; i++) {
Header x = entries[i];
if (eq(x.getName(), name)) {
if (l == null)
l = new ArrayList<>();
l.add(x);
}
}
return l == null ? EMPTY_ARRAY : l.toArray(new Header[l.size()]);
}
/**
* Returns the number of headers in this list.
*
* @return The number of headers in this list.
*/
public int size() {
return entries.length;
}
/**
* Gets the first header with the given name.
*
* <p>
* Header name comparison is case insensitive.
*
* @param name The header name.
* @return The first matching header, or <jk>null</jk> if not found.
*/
public Optional<Header> getFirst(String name) {
for (int i = 0; i < entries.length; i++) {
Header x = entries[i];
if (eq(x.getName(), name))
return Optional.of(x);
}
return Optional.empty();
}
/**
* Gets the last header with the given name.
*
* <p>
* Header name comparison is case insensitive.
*
* @param name The header name.
* @return The last matching header, or <jk>null</jk> if not found.
*/
public Optional<Header> getLast(String name) {
for (int i = entries.length - 1; i >= 0; i--) {
Header x = entries[i];
if (eq(x.getName(), name))
return Optional.of(x);
}
return Optional.empty();
}
/**
* Gets all of the headers contained within this list.
*
* @return An array of all the headers in this list, or an empty array if no headers are present.
*/
public Header[] getAll() {
return entries.length == 0 ? EMPTY_ARRAY : Arrays.copyOf(entries, entries.length);
}
/**
* Tests if headers with the given name are contained within this list.
*
* <p>
* Header name comparison is case insensitive.
*
* @param name The header name.
* @return <jk>true</jk> if at least one header with the name is present.
*/
public boolean contains(String name) {
for (int i = 0; i < entries.length; i++) {
Header x = entries[i];
if (eq(x.getName(), name))
return true;
}
return false;
}
/**
* Returns an iterator over this list of headers.
*
* @return A new iterator over this list of headers.
*/
public HeaderIterator iterator() {
return new BasicHeaderIterator(entries, null, caseSensitive);
}
/**
* Returns an iterator over the headers with a given name in this list.
*
* @param name The name of the headers over which to iterate, or <jk>null</jk> for all headers
*
* @return A new iterator over the matching headers in this list.
*/
public HeaderIterator iterator(String name) {
return new BasicHeaderIterator(entries, name, caseSensitive);
}
/**
* Performs an operation on the headers of this list.
*
* <p>
* This is the preferred method for iterating over headers as it does not involve
* creation or copy of lists/arrays.
*
* @param c The consumer.
* @return This object (for method chaining).
*/
public HeaderList forEach(Consumer<Header> c) {
for (int i = 0; i < entries.length; i++)
c.accept(entries[i]);
return this;
}
/**
* Performs an operation on the headers with the specified name in this list.
*
* <p>
* This is the preferred method for iterating over headers as it does not involve
* creation or copy of lists/arrays.
*
* @param name The header name.
* @param c The consumer.
* @return This object (for method chaining).
*/
public HeaderList forEach(String name, Consumer<Header> c) {
for (int i = 0; i < entries.length; i++)
if (eq(name, entries[i].getName()))
c.accept(entries[i]);
return this;
}
/**
* Returns a stream of the headers in this list.
*
* <p>
* This does not involve a copy of the underlying array of <c>Header</c> objects so should perform well.
*
* @return This object (for method chaining).
*/
public Stream<Header> stream() {
return Arrays.stream(entries);
}
/**
* Returns a stream of the headers in this list with the specified name.
*
* <p>
* This does not involve a copy of the underlying array of <c>Header</c> objects so should perform well.
*
* @param name The header name.
* @return This object (for method chaining).
*/
public Stream<Header> stream(String name) {
return Arrays.stream(entries).filter(x->eq(name, x.getName()));
}
private boolean eq(String s1, String s2) {
return StringUtils.eq(!caseSensitive, s1, s2);
}
@Override /* Object */
public String toString() {
return Arrays.asList(entries).toString();
}
}