|
| 1 | +package io.goodforgod.http.common; |
| 2 | + |
| 3 | +import java.util.*; |
| 4 | +import java.util.stream.Collectors; |
| 5 | +import org.jetbrains.annotations.NotNull; |
| 6 | +import org.jetbrains.annotations.Nullable; |
| 7 | + |
| 8 | +/** |
| 9 | + * Constants for common HTTP headers. |
| 10 | + * <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html">W3</a> |
| 11 | + * |
| 12 | + * @author Anton Kurako (GoodforGod) |
| 13 | + * @since 13.03.2022 |
| 14 | + */ |
| 15 | +public final class HttpHeaders { |
| 16 | + |
| 17 | + public static final String ACCEPT = "Accept"; |
| 18 | + public static final String ACCEPT_CH = "Accept-CH"; |
| 19 | + public static final String ACCEPT_CH_LIFETIME = "Accept-CH-Lifetime"; |
| 20 | + public static final String ACCEPT_CHARSET = "Accept-Charset"; |
| 21 | + public static final String ACCEPT_ENCODING = "Accept-Encoding"; |
| 22 | + public static final String ACCEPT_LANGUAGE = "Accept-Language"; |
| 23 | + public static final String ACCEPT_RANGES = "Accept-Ranges"; |
| 24 | + public static final String ACCEPT_PATCH = "Accept-Patch"; |
| 25 | + public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; |
| 26 | + public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; |
| 27 | + public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; |
| 28 | + public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; |
| 29 | + public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; |
| 30 | + public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; |
| 31 | + public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; |
| 32 | + public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; |
| 33 | + public static final String AGE = "Age"; |
| 34 | + public static final String ALLOW = "Allow"; |
| 35 | + public static final String AUTHORIZATION = "Authorization"; |
| 36 | + public static final String AUTHORIZATION_INFO = "Authorization-Info"; |
| 37 | + public static final String CACHE_CONTROL = "Cache-Control"; |
| 38 | + public static final String CONNECTION = "Connection"; |
| 39 | + public static final String CONTENT_BASE = "Content-Base"; |
| 40 | + public static final String CONTENT_DISPOSITION = "Content-Disposition"; |
| 41 | + public static final String CONTENT_DPR = "Content-DPR"; |
| 42 | + public static final String CONTENT_ENCODING = "Content-Encoding"; |
| 43 | + public static final String CONTENT_LANGUAGE = "Content-Language"; |
| 44 | + public static final String CONTENT_LENGTH = "Content-Length"; |
| 45 | + public static final String CONTENT_LOCATION = "Content-Location"; |
| 46 | + public static final String CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding"; |
| 47 | + public static final String CONTENT_MD5 = "Content-MD5"; |
| 48 | + public static final String CONTENT_RANGE = "Content-Range"; |
| 49 | + public static final String CONTENT_TYPE = "Content-Type"; |
| 50 | + public static final String COOKIE = "Cookie"; |
| 51 | + public static final String CROSS_ORIGIN_RESOURCE_POLICY = "Cross-Origin-Resource-Policy"; |
| 52 | + public static final String DATE = "Date"; |
| 53 | + public static final String DEVICE_MEMORY = "Device-Memory"; |
| 54 | + public static final String DOWNLINK = "Downlink"; |
| 55 | + public static final String DPR = "DPR"; |
| 56 | + public static final String ECT = "ECT"; |
| 57 | + public static final String ETAG = "ETag"; |
| 58 | + public static final String EXPECT = "Expect"; |
| 59 | + public static final String EXPIRES = "Expires"; |
| 60 | + public static final String FEATURE_POLICY = "Feature-Policy"; |
| 61 | + public static final String FORWARDED = "Forwarded"; |
| 62 | + public static final String FROM = "From"; |
| 63 | + public static final String HOST = "Host"; |
| 64 | + public static final String IF_MATCH = "If-Match"; |
| 65 | + public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; |
| 66 | + public static final String IF_NONE_MATCH = "If-None-Match"; |
| 67 | + public static final String IF_RANGE = "If-Range"; |
| 68 | + public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; |
| 69 | + public static final String LAST_MODIFIED = "Last-Modified"; |
| 70 | + public static final String LINK = "Link"; |
| 71 | + public static final String LOCATION = "Location"; |
| 72 | + public static final String MAX_FORWARDS = "Max-Forwards"; |
| 73 | + public static final String ORIGIN = "Origin"; |
| 74 | + public static final String PRAGMA = "Pragma"; |
| 75 | + public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; |
| 76 | + public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; |
| 77 | + public static final String RANGE = "Range"; |
| 78 | + public static final String REFERER = "Referer"; |
| 79 | + public static final String REFERRER_POLICY = "Referrer-Policy"; |
| 80 | + public static final String RETRY_AFTER = "Retry-After"; |
| 81 | + public static final String RTT = "RTT"; |
| 82 | + public static final String SAVE_DATA = "Save-Data"; |
| 83 | + public static final String SEC_WEBSOCKET_KEY1 = "Sec-WebSocket-Key1"; |
| 84 | + public static final String SEC_WEBSOCKET_KEY2 = "Sec-WebSocket-Key2"; |
| 85 | + public static final String SEC_WEBSOCKET_LOCATION = "Sec-WebSocket-Location"; |
| 86 | + public static final String SEC_WEBSOCKET_ORIGIN = "Sec-WebSocket-Origin"; |
| 87 | + public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; |
| 88 | + public static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; |
| 89 | + public static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; |
| 90 | + public static final String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; |
| 91 | + public static final String SERVER = "Server"; |
| 92 | + public static final String SET_COOKIE = "Set-Cookie"; |
| 93 | + public static final String SET_COOKIE2 = "Set-Cookie2"; |
| 94 | + public static final String SOURCE_MAP = "SourceMap"; |
| 95 | + public static final String TE = "TE"; |
| 96 | + public static final String TRAILER = "Trailer"; |
| 97 | + public static final String TRANSFER_ENCODING = "Transfer-Encoding"; |
| 98 | + public static final String UPGRADE = "Upgrade"; |
| 99 | + public static final String USER_AGENT = "User-Agent"; |
| 100 | + public static final String VARY = "Vary"; |
| 101 | + public static final String VIA = "Via"; |
| 102 | + public static final String VIEWPORT_WIDTH = "Viewport-Width"; |
| 103 | + public static final String WARNING = "Warning"; |
| 104 | + public static final String WEBSOCKET_LOCATION = "WebSocket-Location"; |
| 105 | + public static final String WEBSOCKET_ORIGIN = "WebSocket-Origin"; |
| 106 | + public static final String WEBSOCKET_PROTOCOL = "WebSocket-Protocol"; |
| 107 | + public static final String WIDTH = "Width"; |
| 108 | + public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; |
| 109 | + public static final String X_AUTH_TOKEN = "X-Auth-Token"; |
| 110 | + |
| 111 | + private static final HttpHeaders EMPTY = new HttpHeaders(Collections.emptyMap()); |
| 112 | + |
| 113 | + private final Map<String, List<String>> multiHeaderMap; |
| 114 | + |
| 115 | + private HttpHeaders(@NotNull Map<String, List<String>> multiHeaderMap) { |
| 116 | + this.multiHeaderMap = multiHeaderMap; |
| 117 | + } |
| 118 | + |
| 119 | + @NotNull |
| 120 | + public static HttpHeaders ofMultiMap(@Nullable Map<String, List<String>> multiHeaderMap) { |
| 121 | + if (multiHeaderMap == null || multiHeaderMap.isEmpty()) { |
| 122 | + return EMPTY; |
| 123 | + } |
| 124 | + |
| 125 | + final Map<String, List<String>> headers = Map.copyOf(multiHeaderMap.entrySet().stream() |
| 126 | + .filter(e -> e.getKey() != null && !e.getKey().isEmpty() |
| 127 | + && e.getValue() != null && !e.getValue().isEmpty()) |
| 128 | + .collect(Collectors.toMap(Map.Entry::getKey, e -> List.copyOf(e.getValue())))); |
| 129 | + |
| 130 | + return new HttpHeaders(headers); |
| 131 | + } |
| 132 | + |
| 133 | + @NotNull |
| 134 | + public static HttpHeaders ofMap(@Nullable Map<String, String> headerMap) { |
| 135 | + if (headerMap == null || headerMap.isEmpty()) { |
| 136 | + return EMPTY; |
| 137 | + } |
| 138 | + |
| 139 | + final Map<String, List<String>> headers = Map.copyOf(headerMap.entrySet().stream() |
| 140 | + .filter(e -> e.getKey() != null && !e.getKey().isEmpty() |
| 141 | + && e.getValue() != null) |
| 142 | + .collect(Collectors.toMap(Map.Entry::getKey, e -> List.of(e.getValue())))); |
| 143 | + |
| 144 | + return new HttpHeaders(headers); |
| 145 | + } |
| 146 | + |
| 147 | + @NotNull |
| 148 | + public static HttpHeaders of(@Nullable String... headerAndValue) { |
| 149 | + if (headerAndValue == null || headerAndValue.length == 0) { |
| 150 | + return EMPTY; |
| 151 | + } |
| 152 | + |
| 153 | + final boolean isEven = headerAndValue.length % 2 == 0; |
| 154 | + if (!isEven) { |
| 155 | + throw new IllegalArgumentException("Header and values amount must be even, but was: " + headerAndValue.length); |
| 156 | + } |
| 157 | + |
| 158 | + final Map<String, List<String>> multiHeaderMap = new HashMap<>(headerAndValue.length + 5); |
| 159 | + for (int i = 0; i < headerAndValue.length; i += 2) { |
| 160 | + final String key = headerAndValue[i]; |
| 161 | + final String value = headerAndValue[i + 1]; |
| 162 | + if (key != null && value != null) { |
| 163 | + multiHeaderMap.put(key, List.of(value)); |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + final Map<String, List<String>> headers = Map.copyOf(multiHeaderMap); |
| 168 | + return new HttpHeaders(headers); |
| 169 | + } |
| 170 | + |
| 171 | + @NotNull |
| 172 | + public static HttpHeaders empty() { |
| 173 | + return EMPTY; |
| 174 | + } |
| 175 | + |
| 176 | + /** |
| 177 | + * Get the first value of the given header. |
| 178 | + * |
| 179 | + * @param headerName The header name |
| 180 | + * @return The first value or null if it is present |
| 181 | + */ |
| 182 | + @NotNull |
| 183 | + public Optional<String> findFirst(@NotNull CharSequence headerName) { |
| 184 | + final List<String> values = multiHeaderMap.get(headerName.toString()); |
| 185 | + if (values == null) { |
| 186 | + return Optional.empty(); |
| 187 | + } |
| 188 | + |
| 189 | + return Optional.of(values.get(0)); |
| 190 | + } |
| 191 | + |
| 192 | + /** |
| 193 | + * Find all values of the given header. |
| 194 | + * |
| 195 | + * @param headerName The header name |
| 196 | + * @return All values or empty list if not present |
| 197 | + */ |
| 198 | + @NotNull |
| 199 | + public List<String> findAll(@NotNull CharSequence headerName) { |
| 200 | + return multiHeaderMap.getOrDefault(headerName.toString(), Collections.emptyList()); |
| 201 | + } |
| 202 | + |
| 203 | + /** |
| 204 | + * @return all headers and values as multi map |
| 205 | + */ |
| 206 | + @NotNull |
| 207 | + public Map<String, List<String>> getMultiMap() { |
| 208 | + return multiHeaderMap; |
| 209 | + } |
| 210 | + |
| 211 | + /** |
| 212 | + * @return all headers and only first corresponding value as map |
| 213 | + */ |
| 214 | + @NotNull |
| 215 | + public Map<String, String> getMap() { |
| 216 | + return multiHeaderMap.entrySet().stream() |
| 217 | + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().iterator().next())); |
| 218 | + } |
| 219 | + |
| 220 | + /** |
| 221 | + * The request or response content type. |
| 222 | + * |
| 223 | + * @return The content type |
| 224 | + */ |
| 225 | + @NotNull |
| 226 | + public Optional<Long> contentLength() { |
| 227 | + return findFirst(CONTENT_LENGTH) |
| 228 | + .map(Long::parseLong); |
| 229 | + } |
| 230 | + |
| 231 | + /** |
| 232 | + * The request or response content type. |
| 233 | + * |
| 234 | + * @return The content type |
| 235 | + */ |
| 236 | + @NotNull |
| 237 | + public Optional<MediaType> contentType() { |
| 238 | + return findFirst(CONTENT_TYPE).map(MediaType::of); |
| 239 | + } |
| 240 | + |
| 241 | + /** |
| 242 | + * @return The {@link #ORIGIN} header |
| 243 | + */ |
| 244 | + @NotNull |
| 245 | + public Optional<String> origin() { |
| 246 | + return findFirst(ORIGIN); |
| 247 | + } |
| 248 | + |
| 249 | + /** |
| 250 | + * @return The {@link #AUTHORIZATION} header |
| 251 | + */ |
| 252 | + @NotNull |
| 253 | + public Optional<String> authorization() { |
| 254 | + return findFirst(AUTHORIZATION); |
| 255 | + } |
| 256 | + |
| 257 | + /** |
| 258 | + * @return Whether the {@link HttpHeaders#CONNECTION} header is set to Keep-Alive |
| 259 | + */ |
| 260 | + public boolean isKeepAlive() { |
| 261 | + return findFirst(CONNECTION) |
| 262 | + .map(v -> v.equalsIgnoreCase("keep-alive")) |
| 263 | + .orElse(false); |
| 264 | + } |
| 265 | + |
| 266 | + /** |
| 267 | + * A list of accepted {@link MediaType} instances. |
| 268 | + * |
| 269 | + * @return A list of zero or many {@link MediaType} instances |
| 270 | + */ |
| 271 | + @NotNull |
| 272 | + public List<MediaType> accept() { |
| 273 | + final List<String> values = findAll(ACCEPT); |
| 274 | + if (values.isEmpty()) { |
| 275 | + return Collections.emptyList(); |
| 276 | + } |
| 277 | + |
| 278 | + final List<MediaType> mediaTypes = new ArrayList<>(6); |
| 279 | + for (String value : values) { |
| 280 | + for (String token : value.split(",")) { |
| 281 | + if (!token.isEmpty()) { |
| 282 | + try { |
| 283 | + mediaTypes.add(MediaType.of(token)); |
| 284 | + } catch (IllegalArgumentException e) { |
| 285 | + // ignore |
| 286 | + } |
| 287 | + } |
| 288 | + } |
| 289 | + } |
| 290 | + |
| 291 | + return mediaTypes; |
| 292 | + } |
| 293 | +} |
0 commit comments