001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.client;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.net.*;
022import java.time.*;
023import java.util.*;
024
025import org.apache.juneau.commons.http.*;
026import org.apache.juneau.http.header.*;
027
028/**
029 * A fluent accessor for a named HTTP response header.
030 *
031 * <p>
032 * Provides convenient typed-access methods for reading header values.
033 * Returns {@code null} or empty results when the named header is absent from the response.
034 *
035 * <p>
036 * Obtain instances via {@link RestResponse#header(String)}.
037 *
038 * <h5 class='section'>See Also:</h5><ul>
039 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a>
040 * </ul>
041 *
042 * @since 9.2.1
043 */
044@SuppressWarnings({
045   "resource" // Eclipse resource analysis: response is borrowed; caller closes it
046})
047public final class ResponseHeader {
048
049   private final String name;
050
051   private final RestResponse response;
052
053   ResponseHeader(String name, RestResponse response) {
054      this.name = name;
055      this.response = response;
056   }
057
058   /**
059    * Returns the header name.
060    *
061    * @return The header name. Never <jk>null</jk>.
062    */
063   public String getName() {
064      return name;
065   }
066
067   /**
068    * Returns <jk>true</jk> if this header is present in the response.
069    *
070    * @return <jk>true</jk> if present.
071    */
072   public boolean isPresent() {
073      return response.getFirstHeader(name) != null;
074   }
075
076   /**
077    * Returns the first value of this header, or <jk>null</jk> if absent.
078    *
079    * @return The header value, possibly <jk>null</jk>.
080    */
081   public String getValue() {
082      var h = response.getFirstHeader(name);
083      return h != null ? h.value() : null;
084   }
085
086   /**
087    * Returns the first value of this header, or the given default if absent.
088    *
089    * @param defaultValue The default value to return when the header is absent.
090    * @return The header value, or {@code defaultValue} if absent.
091    */
092   public String orElse(String defaultValue) {
093      var value = getValue();
094      return or(value, defaultValue);
095   }
096
097   /**
098    * Returns the first value of this header as an {@link Optional}.
099    *
100    * @return An optional containing the first header value, or empty if absent.
101    */
102   public Optional<String> asOptional() {
103      return o(getValue());
104   }
105
106   /**
107    * Returns the first value of this header parsed as an {@code int}, or {@code -1} if absent or not parseable.
108    *
109    * @return The integer value, or {@code -1} if absent or not an integer.
110    */
111   public int asInteger() {
112      var value = getValue();
113      if (value == null)
114         return -1;
115      try {
116         return Integer.parseInt(value.trim());
117      } catch (NumberFormatException e) {
118         return -1;
119      }
120   }
121
122   /**
123    * Returns the first value of this header parsed as a {@code long}, or {@code -1L} if absent or not parseable.
124    *
125    * @return The long value, or {@code -1L} if absent or not a long.
126    */
127   public long asLong() {
128      var value = getValue();
129      if (value == null)
130         return -1L;
131      try {
132         return Long.parseLong(value.trim());
133      } catch (NumberFormatException e) {
134         return -1L;
135      }
136   }
137
138   /**
139    * Returns all values of this header.
140    *
141    * <p>
142    * For repeated headers, this returns each header value in encounter order.
143    *
144    * @return An unmodifiable list of header values. Never <jk>null</jk>, but possibly empty.
145    */
146   public List<String> getValues() {
147      return response.getHeaders().stream()
148         .filter(h -> eqic(name, h.name()))
149         .map(TransportHeader::value)
150         .toList();
151   }
152
153   /**
154    * Returns the first value as a comma-split list of individual tokens.
155    *
156    * <p>
157    * Useful for headers like {@code Accept} or {@code Allow} that combine multiple values with commas.
158    *
159    * @return A list of trimmed tokens. Never <jk>null</jk>, but possibly empty.
160    */
161   public List<String> asCsvList() {
162      var value = getValue();
163      if (value == null)
164         return List.of();
165      return Arrays.stream(value.split(","))
166         .map(String::trim)
167         .filter(s -> !s.isEmpty())
168         .toList();
169   }
170
171   /**
172    * Returns the first value of this header parsed as a {@link Boolean}.
173    *
174    * @return The boolean value, or empty if absent.
175    */
176   public Optional<Boolean> asBoolean() {
177      var value = getValue();
178      return value == null ? oe() : o(Boolean.parseBoolean(value.trim()));
179   }
180
181   /**
182    * Returns the first value of this header parsed as an RFC-1123 HTTP date.
183    *
184    * @return The parsed date, or empty if absent or not parseable.
185    */
186   public Optional<ZonedDateTime> asZonedDateTime() {
187      var value = getValue();
188      if (value == null)
189         return oe();
190      try {
191         return HttpDateHeader.of(name, value).asZonedDateTime();
192      } catch (DateTimeException e) {
193         return oe();
194      }
195   }
196
197   /**
198    * Returns the first value of this header parsed as a {@link URI}.
199    *
200    * @return The parsed URI, or empty if absent or not a valid URI.
201    */
202   public Optional<URI> asUri() {
203      var value = getValue();
204      if (value == null)
205         return oe();
206      try {
207         return o(new URI(value));
208      } catch (URISyntaxException e) {
209         return oe();
210      }
211   }
212
213   /**
214    * Returns the first value of this header parsed as a single {@link EntityTag} validator (e.g. {@code ETag}).
215    *
216    * @return The parsed entity tag, or empty if absent.
217    */
218   public Optional<EntityTag> asEntityTag() {
219      try {
220         return o(EntityTag.of(getValue()));
221      } catch (IllegalArgumentException e) {
222         return oe();
223      }
224   }
225
226   /**
227    * Returns the first value of this header parsed as a comma-delimited list of {@link EntityTag} validators
228    * (e.g. {@code If-Match}).
229    *
230    * @return The parsed entity tags, or {@link EntityTags#EMPTY} if absent or not parseable. Never <jk>null</jk>.
231    */
232   public EntityTags asEntityTags() {
233      try {
234         return EntityTags.of(getValue());
235      } catch (IllegalArgumentException e) {
236         return EntityTags.EMPTY;
237      }
238   }
239
240   /**
241    * Returns the first value of this header parsed as a q-valued list of string ranges (e.g. {@code Accept-Encoding}).
242    *
243    * @return The parsed string ranges. Never <jk>null</jk>, but possibly empty.
244    */
245   public StringRanges asStringRanges() {
246      return StringRanges.of(getValue());
247   }
248
249   @Override /* Object */
250   public String toString() {
251      var value = getValue();
252      return value != null ? name + ": " + value : name + ": <absent>";
253   }
254}