View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.core5.http.message;
29  
30  import java.io.Serializable;
31  import java.util.ArrayList;
32  import java.util.Collections;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Locale;
36  
37  import org.apache.hc.core5.http.Header;
38  import org.apache.hc.core5.http.MessageHeaders;
39  import org.apache.hc.core5.http.ProtocolException;
40  import org.apache.hc.core5.util.CharArrayBuffer;
41  import org.apache.hc.core5.util.LangUtils;
42  
43  /**
44   * A class for combining a set of headers. This class allows for multiple headers with the same name
45   * and keeps track of the order in which headers were added.
46   *
47   * @since 4.0
48   */
49  public class HeaderGroup implements MessageHeaders, Serializable {
50  
51  
52      // HTTPCORE-361 : we don't use the for-each syntax, when iterating headers
53      // as that creates an Iterator that needs to be garbage-collected
54  
55      private static final long serialVersionUID = 2608834160639271617L;
56  
57      private final Header[] EMPTY = new Header[] {};
58  
59      /** The list of headers for this group, in the order in which they were added */
60      private final List<Header> headers;
61  
62      /**
63       * Constructor for HeaderGroup.
64       */
65      public HeaderGroup() {
66          this.headers = new ArrayList<>(16);
67      }
68  
69      /**
70       * Removes any contained headers.
71       */
72      public void clear() {
73          headers.clear();
74      }
75  
76      /**
77       * Adds the given header to the group.  The order in which this header was
78       * added is preserved.
79       *
80       * @param header the header to add
81       */
82      public void addHeader(final Header header) {
83          if (header == null) {
84              return;
85          }
86          headers.add(header);
87      }
88  
89      /**
90       * Removes the first given header.
91       *
92       * @param header the header to remove
93       * @return <code>true</code> if a header was removed as a result of this call.
94       */
95      public boolean removeHeader(final Header header) {
96          if (header == null) {
97              return false;
98          }
99          for (int i = 0; i < this.headers.size(); i++) {
100             final Header current = this.headers.get(i);
101             if (headerEquals(header, current)) {
102                 this.headers.remove(current);
103                 return true;
104             }
105         }
106         return false;
107     }
108 
109     private boolean headerEquals(final Header header1, final Header header2) {
110         return header2 == header1 || header2.getName().equalsIgnoreCase(header1.getName())
111                 && LangUtils.equals(header1.getValue(), header2.getValue());
112     }
113 
114     /**
115      * Removes all headers that match the given header.
116      *
117      * @param header the header to remove
118      * @return <code>true</code> if any header was removed as a result of this call.
119      *
120      * @since 5.0
121      */
122     public boolean removeHeaders(final Header header) {
123         if (header == null) {
124             return false;
125         }
126         boolean removed = false;
127         for (final Iterator<Header> iterator = headerIterator(); iterator.hasNext();) {
128             final Header current = iterator.next();
129             if (headerEquals(header, current)) {
130                 iterator.remove();
131                 removed = true;
132             }
133         }
134         return removed;
135     }
136 
137     /**
138      * Replaces the first occurrence of the header with the same name. If no header with
139      * the same name is found the given header is added to the end of the list.
140      *
141      * @param header the new header that should replace the first header with the same
142      * name if present in the list.
143      *
144      * @since 5.0
145      */
146     public void setHeader(final Header header) {
147         if (header == null) {
148             return;
149         }
150         for (int i = 0; i < this.headers.size(); i++) {
151             final Header current = this.headers.get(i);
152             if (current.getName().equalsIgnoreCase(header.getName())) {
153                 this.headers.set(i, header);
154                 return;
155             }
156         }
157         this.headers.add(header);
158     }
159 
160     /**
161      * Sets all of the headers contained within this group overriding any
162      * existing headers. The headers are added in the order in which they appear
163      * in the array.
164      *
165      * @param headers the headers to set
166      */
167     public void setHeaders(final Header... headers) {
168         clear();
169         if (headers == null) {
170             return;
171         }
172         Collections.addAll(this.headers, headers);
173     }
174 
175     /**
176      * Gets a header representing all of the header values with the given name.
177      * If more that one header with the given name exists the values will be
178      * combined with a "," as per RFC 2616.
179      *
180      * <p>Header name comparison is case insensitive.
181      *
182      * @param name the name of the header(s) to get
183      * @return a header with a condensed value or {@code null} if no
184      * headers by the given name are present
185      */
186     public Header getCondensedHeader(final String name) {
187         final Header[] hdrs = getHeaders(name);
188 
189         if (hdrs.length == 0) {
190             return null;
191         } else if (hdrs.length == 1) {
192             return hdrs[0];
193         } else {
194             final CharArrayBuffer valueBuffer = new CharArrayBuffer(128);
195             valueBuffer.append(hdrs[0].getValue());
196             for (int i = 1; i < hdrs.length; i++) {
197                 valueBuffer.append(", ");
198                 valueBuffer.append(hdrs[i].getValue());
199             }
200 
201             return new BasicHeader(name.toLowerCase(Locale.ROOT), valueBuffer.toString());
202         }
203     }
204 
205     /**
206      * Gets all of the headers with the given name.  The returned array
207      * maintains the relative order in which the headers were added.
208      *
209      * <p>Header name comparison is case insensitive.
210      *
211      * @param name the name of the header(s) to get
212      *
213      * @return an array of length &ge; 0
214      */
215     @Override
216     public Header[] getHeaders(final String name) {
217         List<Header> headersFound = null;
218         for (int i = 0; i < this.headers.size(); i++) {
219             final Header header = this.headers.get(i);
220             if (header.getName().equalsIgnoreCase(name)) {
221                 if (headersFound == null) {
222                     headersFound = new ArrayList<>();
223                 }
224                 headersFound.add(header);
225             }
226         }
227         return headersFound != null ? headersFound.toArray(new Header[headersFound.size()]) : EMPTY;
228     }
229 
230     /**
231      * Gets the first header with the given name.
232      *
233      * <p>Header name comparison is case insensitive.
234      *
235      * @param name the name of the header to get
236      * @return the first header or {@code null}
237      */
238     @Override
239     public Header getFirstHeader(final String name) {
240         for (int i = 0; i < this.headers.size(); i++) {
241             final Header header = this.headers.get(i);
242             if (header.getName().equalsIgnoreCase(name)) {
243                 return header;
244             }
245         }
246         return null;
247     }
248 
249     /**
250      * Gets single first header with the given name.
251      *
252      * <p>Header name comparison is case insensitive.
253      *
254      * @param name the name of the header to get
255      * @return the first header or {@code null}
256      * @throws ProtocolException in case multiple headers with the given name are found.
257      */
258     @Override
259     public Header getSingleHeader(final String name) throws ProtocolException {
260         int count = 0;
261         Header singleHeader = null;
262         for (int i = 0; i < this.headers.size(); i++) {
263             final Header header = this.headers.get(i);
264             if (header.getName().equalsIgnoreCase(name)) {
265                 singleHeader = header;
266                 count++;
267             }
268         }
269         if (count > 1) {
270             throw new ProtocolException("Multiple headers '%s'' found", name);
271         }
272         return singleHeader;
273     }
274 
275     /**
276      * Gets the last header with the given name.
277      *
278      * <p>Header name comparison is case insensitive.
279      *
280      * @param name the name of the header to get
281      * @return the last header or {@code null}
282      */
283     @Override
284     public Header getLastHeader(final String name) {
285         // start at the end of the list and work backwards
286         for (int i = headers.size() - 1; i >= 0; i--) {
287             final Header header = headers.get(i);
288             if (header.getName().equalsIgnoreCase(name)) {
289                 return header;
290             }
291         }
292 
293         return null;
294     }
295 
296     /**
297      * Gets all of the headers contained within this group.
298      *
299      * @return an array of length &ge; 0
300      */
301     @Override
302     public Header[] getAllHeaders() {
303         return headers.toArray(new Header[headers.size()]);
304     }
305 
306     /**
307      * Tests if headers with the given name are contained within this group.
308      *
309      * <p>Header name comparison is case insensitive.
310      *
311      * @param name the header name to test for
312      * @return {@code true} if at least one header with the name is
313      * contained, {@code false} otherwise
314      */
315     @Override
316     public boolean containsHeader(final String name) {
317         // HTTPCORE-361 : we don't use the for-each syntax, i.e.
318         //     for (Header header : headers)
319         // as that creates an Iterator that needs to be garbage-collected
320         for (int i = 0; i < this.headers.size(); i++) {
321             final Header header = this.headers.get(i);
322             if (header.getName().equalsIgnoreCase(name)) {
323                 return true;
324             }
325         }
326 
327         return false;
328     }
329 
330     /**
331      * Checks if a certain header is present in this message and how many times.
332      * <p>Header name comparison is case insensitive.
333      *
334      * @param name the header name to check for.
335      * @return number of occurrences of the header in the message.
336      */
337     @Override
338     public int containsHeaders(final String name) {
339         // HTTPCORE-361 : we don't use the for-each syntax, i.e.
340         //     for (Header header : headers)
341         // as that creates an Iterator that needs to be garbage-collected
342         int count = 0;
343         for (int i = 0; i < this.headers.size(); i++) {
344             final Header header = this.headers.get(i);
345             if (header.getName().equalsIgnoreCase(name)) {
346                 count++;
347             }
348         }
349         return count;
350     }
351 
352     /**
353      * Returns an iterator over this group of headers.
354      *
355      * @return iterator over this group of headers.
356      *
357      * @since 5.0
358      */
359     @Override
360     public Iterator<Header> headerIterator() {
361         return new BasicListHeaderIterator(this.headers, null);
362     }
363 
364     /**
365      * Returns an iterator over the headers with a given name in this group.
366      *
367      * @param name      the name of the headers over which to iterate, or
368      *                  {@code null} for all headers
369      *
370      * @return iterator over some headers in this group.
371      *
372      * @since 5.0
373      */
374     @Override
375     public Iterator<Header> headerIterator(final String name) {
376         return new BasicListHeaderIterator(this.headers, name);
377     }
378 
379     /**
380      * Removes all headers with a given name in this group.
381      *
382      * @param name      the name of the headers to be removed.
383      * @return <code>true</code> if any header was removed as a result of this call.
384      *
385      * @since 5.0
386      */
387     public boolean removeHeaders(final String name) {
388         if (name == null) {
389             return false;
390         }
391         boolean removed = false;
392         for (final Iterator<Header> iterator = headerIterator(); iterator.hasNext(); ) {
393             final Header header = iterator.next();
394             if (header.getName().equalsIgnoreCase(name)) {
395                 iterator.remove();
396                 removed = true;
397             }
398         }
399         return removed;
400     }
401 
402     @Override
403     public String toString() {
404         return this.headers.toString();
405     }
406 
407 }