All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.okta.commons.http.HttpHeaders Maven / Gradle / Ivy

There is a newer version: 2.0.1
Show newest version
/*
 * Copyright 2002-2017 the original author or authors.
 * Modifications Copyright 2018 Okta, Inc.
 *
 * Licensed 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 com.okta.commons.http;

import com.okta.commons.lang.Assert;
import com.okta.commons.lang.Strings;

import java.net.URI;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @since 0.5.0
 */
public class HttpHeaders implements MultiValueMap {

    private static final String ACCEPT = "Accept";

    private static final String ACCEPT_CHARSET = "Accept-Charset";

    private static final String ALLOW = "Allow";

    private static final String CACHE_CONTROL = "Cache-Control";

    private static final String CONTENT_DISPOSITION = "Content-Disposition";

    private static final String CONTENT_LENGTH = "Content-Length";

    private static final String CONTENT_TYPE = "Content-Type";

    private static final String DATE = "Date";

    private static final String ETAG = "ETag";

    private static final String EXPIRES = "Expires";

    private static final String IF_MODIFIED_SINCE = "If-Modified-Since";

    private static final String IF_NONE_MATCH = "If-None-Match";

    private static final String LAST_MODIFIED = "Last-Modified";

    private static final String LOCATION = "Location";

    private static final String PRAGMA = "Pragma";

    public static final String LINK = "Link";

    public static final String OKTA_REQUEST_ID = "Okta-Request-Id";

    public static final String OKTA_AGENT = "X-Okta-Agent";

    public static final String OKTA_CLIENT_REQUEST_ID = "X-Okta-Client-Request-Id";

    public static final String OKTA_USER_AGENT = "X-Okta-User-Agent-Extended";

    public static final String USER_AGENT = "User-Agent";

    private static final String[] DATE_FORMATS = new String[]{
            "EEE, dd MMM yyyy HH:mm:ss zzz",
            "EEE, dd-MMM-yy HH:mm:ss zzz",
            "EEE MMM dd HH:mm:ss yyyy"
    };

    private final static TimeZone GMT = TimeZone.getTimeZone("GMT");

    private final Map> headers;


    /**
     * Private constructor that can create read-only {@code HttpHeader} instances.
     */
    private HttpHeaders(Map> headers, boolean readOnly) {
        Assert.notNull(headers, "'headers' must not be null");
        if (readOnly) {
            Map> map =
                    new LinkedCaseInsensitiveMap<>(headers.size(), Locale.ENGLISH);
            for (Map.Entry> entry : headers.entrySet()) {
                List values = Collections.unmodifiableList(entry.getValue());
                map.put(entry.getKey(), values);
            }
            this.headers = Collections.unmodifiableMap(map);
        } else {
            this.headers = headers;
        }
    }

    /**
     * Constructs a new, empty instance of the {@code HttpHeaders} object.
     */
    public HttpHeaders() {
        this(new LinkedCaseInsensitiveMap>(8, Locale.ENGLISH), false);
    }

    /**
     * Returns {@code HttpHeaders} object that can only be read, not written to.
     * @param headers source headers
     * @return readonly copy of the source {@code headers}
     */
    public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
        return new HttpHeaders(headers, true);
    }

    /**
     * Set the list of acceptable {@linkplain MediaType media types}, as specified by the {@code Accept} header.
     *
     * @param acceptableMediaTypes the acceptable media types
     */
    public void setAccept(List acceptableMediaTypes) {
        set(ACCEPT, MediaType.toString(acceptableMediaTypes));
    }

    /**
     * Return the list of acceptable {@linkplain MediaType media types}, as specified by the {@code Accept} header.
     * 

Returns an empty list when the acceptable media types are unspecified. * * @return the acceptable media types */ public List getAccept() { String value = getFirst(ACCEPT); return value != null ? MediaType.parseMediaTypes(value) : Collections.emptyList(); } /** * Set the list of acceptable {@linkplain Charset charsets}, as specified by the {@code Accept-Charset} header. * * @param acceptableCharsets the acceptable charsets */ public void setAcceptCharset(List acceptableCharsets) { StringBuilder builder = new StringBuilder(); for (Iterator iterator = acceptableCharsets.iterator(); iterator.hasNext(); ) { Charset charset = iterator.next(); builder.append(charset.name().toLowerCase(Locale.ENGLISH)); if (iterator.hasNext()) { builder.append(", "); } } set(ACCEPT_CHARSET, builder.toString()); } /** * Return the list of acceptable {@linkplain Charset charsets}, as specified by the {@code Accept-Charset} * header. * * @return the acceptable charsets */ public List getAcceptCharset() { List result = new ArrayList(); String value = getFirst(ACCEPT_CHARSET); if (value != null) { String[] tokens = value.split(",\\s*"); for (String token : tokens) { int paramIdx = token.indexOf(';'); String charsetName; if (paramIdx == -1) { charsetName = token; } else { charsetName = token.substring(0, paramIdx); } if (!charsetName.equals("*")) { result.add(Charset.forName(charsetName)); } } } return result; } /** * Set the set of allowed {@link HttpMethod HTTP methods}, as specified by the {@code Allow} header. * * @param allowedMethods the allowed methods */ public void setAllow(Set allowedMethods) { set(ALLOW, Strings.collectionToCommaDelimitedString(allowedMethods)); } /** * Return the set of allowed {@link HttpMethod HTTP methods}, as specified by the {@code Allow} header. *

Returns an empty set when the allowed methods are unspecified. * * @return the allowed methods */ public Set getAllow() { String value = getFirst(ALLOW); if (value != null) { List allowedMethod = new ArrayList(5); String[] tokens = value.split(",\\s*"); for (String token : tokens) { allowedMethod.add(HttpMethod.valueOf(token)); } return EnumSet.copyOf(allowedMethod); } else { return EnumSet.noneOf(HttpMethod.class); } } /** * Sets the (new) value of the {@code Cache-Control} header. * * @param cacheControl the value of the header */ public void setCacheControl(String cacheControl) { set(CACHE_CONTROL, cacheControl); } /** * Returns the value of the {@code Cache-Control} header. * * @return the value of the header */ public String getCacheControl() { return getFirst(CACHE_CONTROL); } /** * Sets the (new) value of the {@code Content-Disposition} header for {@code form-data}. * * @param name the control name * @param filename the filename, may be {@code null} */ public void setContentDispositionFormData(String name, String filename) { Assert.notNull(name, "'name' must not be null"); StringBuilder builder = new StringBuilder("form-data; name=\""); builder.append(name).append('\"'); if (filename != null) { builder.append("; filename=\""); builder.append(filename).append('\"'); } set(CONTENT_DISPOSITION, builder.toString()); } /** * Set the length of the body in bytes, as specified by the {@code Content-Length} header. If * {@code contentLength} is negative, the {@code Content-Length} header will be removed (not set). * * @param contentLength the content length */ public void setContentLength(long contentLength) { if (contentLength < 0) { remove(CONTENT_LENGTH); } else { set(CONTENT_LENGTH, Long.toString(contentLength)); } } /** * Return the length of the body in bytes, as specified by the {@code Content-Length} header. *

Returns -1 when the content-length is unknown. * * @return the content length */ public long getContentLength() { String value = getFirst(CONTENT_LENGTH); return value != null ? Long.parseLong(value) : -1; } /** * Set the {@linkplain MediaType media type} of the body, as specified by the {@code Content-Type} header. * * @param mediaType the media type */ public void setContentType(MediaType mediaType) { //mediaType will be null when a 204 status is returned. if (mediaType != null) { Assert.isTrue(!mediaType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'"); Assert.isTrue(!mediaType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'"); set(CONTENT_TYPE, mediaType.toString()); } } /** * Return the {@linkplain MediaType media type} of the body, as specified by the {@code Content-Type} header. *

Returns {@code null} when the content-type is unknown. * * @return the content type */ public MediaType getContentType() { String value = getFirst(CONTENT_TYPE); return value != null ? MediaType.parseMediaType(value) : null; } /** * Sets the date and time at which the message was created, as specified by the {@code Date} header. *

The date should be specified as the number of milliseconds since January 1, 1970 GMT. * * @param date the date */ public void setDate(long date) { setDate(DATE, date); } /** * Returns the date and time at which the message was created, as specified by the {@code Date} header. *

The date is returned as the number of milliseconds since January 1, 1970 GMT. Returns -1 when the date is unknown. * * @return the creation date/time * @throws IllegalArgumentException if the value can't be converted to a date */ public long getDate() { return getFirstDate(DATE); } /** * Sets the (new) entity tag of the body, as specified by the {@code ETag} header. * * @param eTag the new entity tag */ public void setETag(String eTag) { if (eTag != null) { Assert.isTrue(eTag.startsWith("\"") || eTag.startsWith("W/"), "Invalid eTag, does not start with W/ or \""); Assert.isTrue(eTag.endsWith("\""), "Invalid eTag, does not end with \""); } set(ETAG, eTag); } /** * Returns the entity tag of the body, as specified by the {@code ETag} header. * * @return the entity tag */ public String getETag() { return getFirst(ETAG); } /** * Sets the date and time at which the message is no longer valid, as specified by the {@code Expires} header. *

The date should be specified as the number of milliseconds since January 1, 1970 GMT. * * @param expires the new expires header value */ public void setExpires(long expires) { setDate(EXPIRES, expires); } /** * Returns the date and time at which the message is no longer valid, as specified by the {@code Expires} header. *

The date is returned as the number of milliseconds since January 1, 1970 GMT. Returns -1 when the date is unknown. * * @return the expires value */ public long getExpires() { return getFirstDate(EXPIRES); } /** * Sets the (new) value of the {@code If-Modified-Since} header. *

The date should be specified as the number of milliseconds since January 1, 1970 GMT. * * @param ifModifiedSince the new value of the header */ public void setIfModifiedSince(long ifModifiedSince) { setDate(IF_MODIFIED_SINCE, ifModifiedSince); } /** * Returns the value of the {@code IfModifiedSince} header. *

The date is returned as the number of milliseconds since January 1, 1970 GMT. Returns -1 when the date is unknown. * * @return the header value */ public long getIfNotModifiedSince() { return getFirstDate(IF_MODIFIED_SINCE); } /** * Sets the (new) value of the {@code If-None-Match} header. * * @param ifNoneMatch the new value of the header */ public void setIfNoneMatch(String ifNoneMatch) { set(IF_NONE_MATCH, ifNoneMatch); } /** * Sets the (new) values of the {@code If-None-Match} header. * * @param ifNoneMatchList the new value of the header */ public void setIfNoneMatch(List ifNoneMatchList) { StringBuilder builder = new StringBuilder(); for (Iterator iterator = ifNoneMatchList.iterator(); iterator.hasNext(); ) { String ifNoneMatch = iterator.next(); builder.append(ifNoneMatch); if (iterator.hasNext()) { builder.append(", "); } } set(IF_NONE_MATCH, builder.toString()); } /** * Returns the value of the {@code If-None-Match} header. * * @return the header value */ public List getIfNoneMatch() { List result = new ArrayList(); String value = getFirst(IF_NONE_MATCH); if (value != null) { String[] tokens = value.split(",\\s*"); Collections.addAll(result, tokens); } return result; } /** * Sets the time the resource was last changed, as specified by the {@code Last-Modified} header. *

The date should be specified as the number of milliseconds since January 1, 1970 GMT. * * @param lastModified the last modified date */ public void setLastModified(long lastModified) { setDate(LAST_MODIFIED, lastModified); } /** * Returns the time the resource was last changed, as specified by the {@code Last-Modified} header. *

The date is returned as the number of milliseconds since January 1, 1970 GMT. Returns -1 when the date is unknown. * * @return the last modified date */ public long getLastModified() { return getFirstDate(LAST_MODIFIED); } /** * Set the (new) location of a resource, as specified by the {@code Location} header. * * @param location the location */ public void setLocation(URI location) { set(LOCATION, location.toASCIIString()); } /** * Return the (new) location of a resource, as specified by the {@code Location} header. *

Returns {@code null} when the location is unknown. * * @return the location */ public URI getLocation() { String value = getFirst(LOCATION); return value != null ? URI.create(value) : null; } /** * Sets the (new) value of the {@code Pragma} header. * * @param pragma the value of the header */ public void setPragma(String pragma) { set(PRAGMA, pragma); } /** * Returns the value of the {@code Pragma} header. * * @return the value of the header */ public String getPragma() { return getFirst(PRAGMA); } /** * Returns the value of the {@code Okta-Request-Id} header. * * @return the value of the header */ public String getOktaRequestId() { return getFirst(OKTA_REQUEST_ID); } public List getLinkHeaders() { return get(LINK); } public Map getLinkMap() { List links = getLinkHeaders(); if (!com.okta.commons.lang.Collections.isEmpty(links)) { List headerValues = headers.get(LINK); return headerValues.stream() .map(HttpHeaders::parseLinkHeader) .collect(Collectors.toMap(Link::getRelationType, Link::getHref)); } return Collections.emptyMap(); } private static Link parseLinkHeader(String rawHeader) { Pattern pattern = Pattern.compile("\\<(.*)\\>;.*rel=\"(.*)\""); Matcher matcher = pattern.matcher(rawHeader); if (matcher.matches()) { return new DefaultLink(matcher.group(2), matcher.group(1)); } return null; } // Utility methods private long getFirstDate(String headerName) { String headerValue = getFirst(headerName); if (headerValue == null) { return -1; } for (String dateFormat : DATE_FORMATS) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); simpleDateFormat.setTimeZone(GMT); try { return simpleDateFormat.parse(headerValue).getTime(); } catch (ParseException e) { // NOPMD // ignore } } throw new IllegalArgumentException("Cannot parse date value \"" + headerValue + "\" for \"" + headerName + "\" header"); } private void setDate(String headerName, long date) { SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US); dateFormat.setTimeZone(GMT); set(headerName, dateFormat.format(new Date(date))); } // Single string methods /** * Return the first header value for the given header name, if any. * * @param headerName the header name * @return the first header value; or {@code null} */ public String getFirst(String headerName) { List headerValues = headers.get(headerName); return headerValues != null ? headerValues.get(0) : null; } /** * Add the given, single header value under the given name. * * @param headerName the header name * @param headerValue the header value * @throws UnsupportedOperationException if adding headers is not supported * @see #put(String, List) * @see #set(String, String) */ public void add(String headerName, String headerValue) { List headerValues = headers.get(headerName); if (headerValues == null) { headerValues = new LinkedList(); this.headers.put(headerName, headerValues); } headerValues.add(headerValue); } /** * Set the given, single header value under the given name. * * @param headerName the header name * @param headerValue the header value * @throws UnsupportedOperationException if adding headers is not supported * @see #put(String, List) * @see #add(String, String) */ public void set(String headerName, String headerValue) { List headerValues = new LinkedList(); headerValues.add(headerValue); headers.put(headerName, headerValues); } public void setAll(Map values) { for (Map.Entry entry : values.entrySet()) { set(entry.getKey(), entry.getValue()); } } public Map toSingleValueMap() { LinkedHashMap singleValueMap = new LinkedHashMap(this.headers.size()); for (Map.Entry> entry : headers.entrySet()) { singleValueMap.put(entry.getKey(), entry.getValue().get(0)); } return singleValueMap; } public Map> getXHeaders() { return this.headers.entrySet().stream() .filter(e -> e.getKey().toLowerCase(Locale.ENGLISH).startsWith("x-")) .collect( LinkedCaseInsensitiveMap::new, (map, entry) -> map.put(entry.getKey(), entry.getValue()), Map::putAll); } // Map implementation public int size() { return this.headers.size(); } public boolean isEmpty() { return this.headers.isEmpty(); } public boolean containsKey(Object key) { return this.headers.containsKey(key); } public boolean containsValue(Object value) { return this.headers.containsValue(value); } public List get(Object key) { return this.headers.get(key); } public List put(String key, List value) { return this.headers.put(key, value); } public List remove(Object key) { return this.headers.remove(key); } public void putAll(Map> m) { this.headers.putAll(m); } public void clear() { this.headers.clear(); } public Set keySet() { return this.headers.keySet(); } public Collection> values() { return this.headers.values(); } public Set>> entrySet() { return this.headers.entrySet(); } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof HttpHeaders)) { return false; } HttpHeaders otherHeaders = (HttpHeaders) other; return this.headers.equals(otherHeaders.headers); } @Override public int hashCode() { return this.headers.hashCode(); } @Override public String toString() { return this.headers.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy