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

com.helger.commons.http.HttpHeaderMap Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2014-2024 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * 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.helger.commons.http;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.annotation.ReturnsMutableObject;
import com.helger.commons.codec.RFC2616Codec;
import com.helger.commons.collection.ArrayHelper;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.CommonsLinkedHashMap;
import com.helger.commons.collection.impl.ICommonsIterable;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.collection.impl.ICommonsOrderedMap;
import com.helger.commons.collection.impl.ICommonsOrderedSet;
import com.helger.commons.datetime.PDTFactory;
import com.helger.commons.datetime.PDTWebDateHelper;
import com.helger.commons.equals.EqualsHelper;
import com.helger.commons.hashcode.HashCodeGenerator;
import com.helger.commons.lang.ICloneable;
import com.helger.commons.lang.IHasSize;
import com.helger.commons.state.EChange;
import com.helger.commons.state.IClearable;
import com.helger.commons.string.StringHelper;
import com.helger.commons.string.ToStringGenerator;

/**
 * Abstracts HTTP header interface for external usage.
* Note: since version 9.1.8 (issue #11) the internal scheme changed and the * original case is stored. Older versions always stored the lower case header * names. The implications are that the casing of the first header is sustained. * So if the first API call uses name "Foo" and the second is "foo" they both * refer to the same header name and "Foo" will be the name that is retrieved. * * @author Philip Helger * @since 9.0.0 */ @NotThreadSafe public class HttpHeaderMap implements IHasSize, ICommonsIterable >>, ICloneable , IClearable { /** The separator between key and value */ public static final String SEPARATOR_KEY_VALUE = ": "; /** Default quote if necessary: false (since v10) */ public static final boolean DEFAULT_QUOTE_IF_NECESSARY = false; private static final Logger LOGGER = LoggerFactory.getLogger (HttpHeaderMap.class); private final ICommonsOrderedMap > m_aHeaders = new CommonsLinkedHashMap <> (); /** * Default constructor. */ public HttpHeaderMap () {} /** * Copy constructor. * * @param aOther * Map to copy from. May not be null. */ public HttpHeaderMap (@Nonnull final HttpHeaderMap aOther) { ValueEnforcer.notNull (aOther, "Other"); m_aHeaders.putAll (aOther.m_aHeaders); } /** * Avoid having header values spanning multiple lines. This has been * deprecated by RFC 7230 and Jetty 9.3 refuses to parse these requests with * HTTP 400 by default.
* Since v9.3.6 this method also takes care of quoting header values * correctly. If the value does not correspond to a token according to RFC * 2616 chapter 2.2, the value is enclosed in double quotes. * * @param sValue * The source header value. May be null. * @return The unified header value without \r, \n and \t. Never * null. */ @Nonnull public static String getUnifiedValue (@Nullable final String sValue) { return getUnifiedValue (sValue, DEFAULT_QUOTE_IF_NECESSARY); } /** * Avoid having header values spanning multiple lines. This has been * deprecated by RFC 7230 and Jetty 9.3 refuses to parse these requests with * HTTP 400 by default. * * @param sValue * The source header value. May be null. * @param bQuoteIfNecessary * true if automatic quoting according to RFC 2616, * chapter 2.2 should be used if necessary. * @return The unified header value without \r, \n and \t. Never * null. * @since 9.3.6 */ @Nonnull public static String getUnifiedValue (@Nullable final String sValue, final boolean bQuoteIfNecessary) { final char [] aOneLiner; int nLineLength = 0; if (StringHelper.hasText (sValue)) { // First replace special characters with space // Make sure to merge all consecutive chars to a single space aOneLiner = new char [sValue.length ()]; boolean bLastWasSpace = false; for (final char c : sValue.toCharArray ()) { if (Character.isWhitespace (c)) { // Only merge whitespaces outside of values if (c == '\r' || c == '\n' || c == '\t') { if (!bLastWasSpace) aOneLiner[nLineLength++] = ' '; } else aOneLiner[nLineLength++] = c; bLastWasSpace = true; } else { aOneLiner[nLineLength++] = c; bLastWasSpace = false; } } } else { // Nothing to replace anyway aOneLiner = ArrayHelper.EMPTY_CHAR_ARRAY; } final char [] aQuoted; if (bQuoteIfNecessary) { if (RFC2616Codec.isToken (aOneLiner) || RFC2616Codec.isMaybeEncoded (aOneLiner)) { // No need to quote aQuoted = aOneLiner; } else { // Quote is necessary aQuoted = new RFC2616Codec ().getEncoded (aOneLiner, 0, nLineLength); nLineLength = aQuoted.length; } } else aQuoted = aOneLiner; // to string final String ret = new String (aQuoted, 0, nLineLength); if (LOGGER.isDebugEnabled ()) { if (!EqualsHelper.equals (sValue, ret)) LOGGER.debug ("getUnifiedValue('" + sValue + "'," + bQuoteIfNecessary + ") resulted in '" + ret + "'"); } return ret; } /** * Remove all contained headers. * * @return {@link EChange}. */ @Nonnull public EChange removeAll () { return m_aHeaders.removeAll (); } @Nullable @ReturnsMutableObject private Map.Entry > _getHeaderEntryCaseInsensitive (@Nullable final String sName) { if (StringHelper.hasText (sName)) for (final Map.Entry > aEntry : m_aHeaders.entrySet ()) if (aEntry.getKey ().equalsIgnoreCase (sName)) return aEntry; return null; } @Nullable @ReturnsMutableObject private ICommonsList _getHeaderListCaseInsensitive (@Nullable final String sName) { final Map.Entry > aEntry = _getHeaderEntryCaseInsensitive (sName); return aEntry == null ? null : aEntry.getValue (); } @Nonnull @ReturnsMutableObject private ICommonsList _getOrCreateHeaderList (@Nonnull @Nonempty final String sName) { ICommonsList ret = _getHeaderListCaseInsensitive (sName); if (ret == null) { ret = new CommonsArrayList <> (2); m_aHeaders.put (sName, ret); } return ret; } @Nonnull private EChange _setHeader (@Nonnull @Nonempty final String sName, @Nonnull final String sValue) { ValueEnforcer.notEmpty (sName, "Name"); ValueEnforcer.notNull (sValue, "Value"); if (LOGGER.isTraceEnabled ()) LOGGER.trace ("Setting HTTP header: '" + sName + "' = '" + sValue + "'"); return _getOrCreateHeaderList (sName).set (sValue); } @Nonnull private EChange _addHeader (@Nonnull @Nonempty final String sName, @Nonnull final String sValue) { ValueEnforcer.notEmpty (sName, "Name"); ValueEnforcer.notNull (sValue, "Value"); if (LOGGER.isTraceEnabled ()) LOGGER.trace ("Adding HTTP header: '" + sName + "' = '" + sValue + "'"); return _getOrCreateHeaderList (sName).addObject (sValue); } /** * Set the passed header as is. * * @param sName * Header name. May neither be null nor empty. * @param sValue * The value to be set. May be null in which case nothing * happens. */ public void setHeader (@Nonnull @Nonempty final String sName, @Nullable final String sValue) { if (sValue != null) _setHeader (sName, sValue); } /** * Add the passed header as is. * * @param sName * Header name. May neither be null nor empty. * @param sValue * The value to be set. May be null in which case nothing * happens. */ public void addHeader (@Nonnull @Nonempty final String sName, @Nullable final String sValue) { if (sValue != null) _addHeader (sName, sValue); } @Nonnull public static String getDateTimeAsString (@Nonnull final ZonedDateTime aDT) { ValueEnforcer.notNull (aDT, "DateTime"); // This method internally converts the date to UTC return PDTWebDateHelper.getAsStringRFC822 (aDT); } @Nonnull public static String getDateTimeAsString (@Nonnull final LocalDateTime aLDT) { ValueEnforcer.notNull (aLDT, "DateTime"); // This method internally converts the date to UTC return PDTWebDateHelper.getAsStringRFC822 (aLDT); } /** * Set the passed header as a date header. * * @param sName * Header name. May neither be null nor empty. * @param nMillis * The milliseconds to set as a date. */ public void setDateHeader (@Nonnull @Nonempty final String sName, final long nMillis) { setDateHeader (sName, PDTFactory.createZonedDateTime (nMillis)); } /** * Set the passed header as a date header. * * @param sName * Header name. May neither be null nor empty. * @param aLD * The LocalDate to set as a date. The time is set to start of day. May * not be null. */ public void setDateHeader (@Nonnull @Nonempty final String sName, @Nonnull final LocalDate aLD) { setDateHeader (sName, PDTFactory.createZonedDateTime (aLD)); } /** * Set the passed header as a date header. * * @param sName * Header name. May neither be null nor empty. * @param aLDT * The LocalDateTime to set as a date. May not be null. */ public void setDateHeader (@Nonnull @Nonempty final String sName, @Nonnull final LocalDateTime aLDT) { setDateHeader (sName, PDTFactory.createZonedDateTime (aLDT)); } /** * Set the passed header as a date header. * * @param sName * Header name. May neither be null nor empty. * @param aDT * The DateTime to set as a date. May not be null. */ public void setDateHeader (@Nonnull @Nonempty final String sName, @Nonnull final ZonedDateTime aDT) { _setHeader (sName, getDateTimeAsString (aDT)); } /** * Add the passed header as a date header. * * @param sName * Header name. May neither be null nor empty. * @param nMillis * The milliseconds to set as a date. */ public void addDateHeader (@Nonnull @Nonempty final String sName, final long nMillis) { addDateHeader (sName, PDTFactory.createZonedDateTime (nMillis)); } /** * Add the passed header as a date header. * * @param sName * Header name. May neither be null nor empty. * @param aLD * The LocalDate to set as a date. The time is set to start of day. May * not be null. */ public void addDateHeader (@Nonnull @Nonempty final String sName, @Nonnull final LocalDate aLD) { addDateHeader (sName, PDTFactory.createZonedDateTime (aLD)); } /** * Add the passed header as a date header. * * @param sName * Header name. May neither be null nor empty. * @param aLDT * The LocalDateTime to set as a date. May not be null. */ public void addDateHeader (@Nonnull @Nonempty final String sName, @Nonnull final LocalDateTime aLDT) { addDateHeader (sName, PDTFactory.createZonedDateTime (aLDT)); } /** * Add the passed header as a date header. * * @param sName * Header name. May neither be null nor empty. * @param aDT * The DateTime to set as a date. May not be null. */ public void addDateHeader (@Nonnull @Nonempty final String sName, @Nonnull final ZonedDateTime aDT) { _addHeader (sName, getDateTimeAsString (aDT)); } /** * Set the passed header as a number. * * @param sName * Header name. May neither be null nor empty. * @param nValue * The value to be set. May not be null. */ public void setIntHeader (@Nonnull @Nonempty final String sName, final int nValue) { _setHeader (sName, Integer.toString (nValue)); } /** * Add the passed header as a number. * * @param sName * Header name. May neither be null nor empty. * @param nValue * The value to be set. May not be null. */ public void addIntHeader (@Nonnull @Nonempty final String sName, final int nValue) { _addHeader (sName, Integer.toString (nValue)); } /** * Set the passed header as a number. * * @param sName * Header name. May neither be null nor empty. * @param nValue * The value to be set. May not be null. */ public void setLongHeader (@Nonnull @Nonempty final String sName, final long nValue) { _setHeader (sName, Long.toString (nValue)); } /** * Add the passed header as a number. * * @param sName * Header name. May neither be null nor empty. * @param nValue * The value to be set. May not be null. */ public void addLongHeader (@Nonnull @Nonempty final String sName, final long nValue) { _addHeader (sName, Long.toString (nValue)); } /** * Set all headers from the passed map. Existing headers with the same name * are overwritten. Existing headers are not changed! * * @param aOther * The header map to add. May not be null. */ public void setAllHeaders (@Nonnull final HttpHeaderMap aOther) { ValueEnforcer.notNull (aOther, "Other"); // Don't use putAll for case sensitivity for (final Map.Entry > aEntry : aOther.m_aHeaders.entrySet ()) { final String sKey = aEntry.getKey (); removeHeaders (sKey); for (final String sValue : aEntry.getValue ()) addHeader (sKey, sValue); } } /** * Add all headers from the passed map. Existing headers with the same name * are extended. * * @param aOther * The header map to add. May not be null. */ public void addAllHeaders (@Nonnull final HttpHeaderMap aOther) { ValueEnforcer.notNull (aOther, "Other"); for (final Map.Entry > aEntry : aOther.m_aHeaders.entrySet ()) { final String sKey = aEntry.getKey (); for (final String sValue : aEntry.getValue ()) addHeader (sKey, sValue); } } @Nonnull @ReturnsMutableCopy public ICommonsOrderedMap > getAllHeaders () { return m_aHeaders.getClone (); } /** * @return A copy of all contained header names. Never null. */ @Nonnull @ReturnsMutableCopy public ICommonsOrderedSet getAllHeaderNames () { return m_aHeaders.copyOfKeySet (); } /** * Get all header values of a certain header name. * * @param sName * The name to be searched. * @return The list with all matching values. Never null but * maybe empty. */ @Nonnull @ReturnsMutableCopy public ICommonsList getAllHeaderValues (@Nullable final String sName) { if (StringHelper.hasText (sName)) { final ICommonsList aValues = _getHeaderListCaseInsensitive (sName); if (aValues != null) return aValues.getClone (); } return new CommonsArrayList <> (); } /** * Get the first header value of a certain header name. The matching of the * name happens case insensitive. * * @param sName * The name to be searched. May be null. * @return The first matching value or null. */ @Nullable public String getFirstHeaderValue (@Nullable final String sName) { if (StringHelper.hasText (sName)) { final ICommonsList aValues = _getHeaderListCaseInsensitive (sName); if (aValues != null) return aValues.getFirstOrNull (); } return null; } /** * Get the header value as a combination of all contained values. The matching * of the name happens case insensitive. * * @param sName * The header name to retrieve. May be null. * @param sDelimiter * The delimiter to be used. May not be null. * @return null if no such header is contained. */ @Nullable public String getHeaderCombined (@Nullable final String sName, @Nonnull final String sDelimiter) { if (StringHelper.hasText (sName)) { final ICommonsList aValues = _getHeaderListCaseInsensitive (sName); if (aValues != null) return StringHelper.getImploded (sDelimiter, aValues); } return null; } public boolean containsHeaders (@Nullable final String sName) { if (StringHelper.hasNoText (sName)) return false; return _getHeaderListCaseInsensitive (sName) != null; } /** * Remove all header values where the name matches the provided filter. * * @param aNameFilter * The name filter to be applied. May not be null. * @return {@link EChange} */ @Nonnull public EChange removeHeadersIf (@Nonnull final Predicate aNameFilter) { return m_aHeaders.removeIfKey (aNameFilter); } /** * Remove all header values with the provided name * * @param sName * The name to be removed. May be null. * @return {@link EChange} */ @Nonnull public EChange removeHeaders (@Nullable final String sName) { if (StringHelper.hasNoText (sName)) return EChange.UNCHANGED; final Map.Entry > aEntry = _getHeaderEntryCaseInsensitive (sName); if (aEntry != null) return m_aHeaders.removeObject (aEntry.getKey ()); return EChange.UNCHANGED; } @Nonnull public EChange removeHeader (@Nullable final String sName, @Nullable final String sValue) { final Map.Entry > aEntry = _getHeaderEntryCaseInsensitive (sName); final boolean bRemoved = aEntry != null && aEntry.getValue ().remove (sValue); if (bRemoved && aEntry.getValue ().isEmpty ()) { // If the last value was removed, remove the whole header m_aHeaders.remove (aEntry.getKey ()); } return EChange.valueOf (bRemoved); } @Nonnull public Iterator >> iterator () { return m_aHeaders.entrySet ().iterator (); } @Nonnegative public int size () { return m_aHeaders.size (); } public boolean isEmpty () { return m_aHeaders.isEmpty (); } /** * Invoke the provided consumer for every name/value pair. * * @param aConsumer * Consumer with key and unified value to be invoked. May not be * null. * @param bUnifyValue * true to unify the values, false if not * @see #getUnifiedValue(String,boolean) * @since 9.3.6 */ public void forEachSingleHeader (@Nonnull final BiConsumer aConsumer, final boolean bUnifyValue) { forEachSingleHeader (aConsumer, bUnifyValue, DEFAULT_QUOTE_IF_NECESSARY); } /** * Invoke the provided consumer for every name/value pair. * * @param aConsumer * Consumer with key and unified value to be invoked. May not be * null. * @param bUnifyValue * true to unify the values, false if not * @param bQuoteIfNecessary * true to automatically quote values if it is necessary, * false to not do it. This is only used, if "unify * values" is true. * @see #getUnifiedValue(String, boolean) * @since 9.3.7 */ public void forEachSingleHeader (@Nonnull final BiConsumer aConsumer, final boolean bUnifyValue, final boolean bQuoteIfNecessary) { for (final Map.Entry > aEntry : m_aHeaders.entrySet ()) { final String sKey = aEntry.getKey (); for (final String sValue : aEntry.getValue ()) { final String sUnifiedValue = bUnifyValue ? getUnifiedValue (sValue, bQuoteIfNecessary) : sValue; aConsumer.accept (sKey, sUnifiedValue); } } } /** * Invoke the provided consumer for every header line. * * @param aConsumer * Consumer with the assembled line to be invoked. May not be * null. * @param bUnifyValue * true to unify the values, false if not * @see #getUnifiedValue(String,boolean) * @since 9.3.6 */ public void forEachHeaderLine (@Nonnull final Consumer aConsumer, final boolean bUnifyValue) { forEachHeaderLine (aConsumer, bUnifyValue, DEFAULT_QUOTE_IF_NECESSARY); } /** * Invoke the provided consumer for every header line. * * @param aConsumer * Consumer with the assembled line to be invoked. May not be * null. * @param bUnifyValue * true to unify the values, false if not. * @param bQuoteIfNecessary * true to automatically quote values if it is necessary, * false to not do it. This is only used, if "unify * values" is true. * @see #getUnifiedValue(String,boolean) * @since 9.3.7 */ public void forEachHeaderLine (@Nonnull final Consumer aConsumer, final boolean bUnifyValue, final boolean bQuoteIfNecessary) { for (final Map.Entry > aEntry : m_aHeaders.entrySet ()) { final String sKey = aEntry.getKey (); for (final String sValue : aEntry.getValue ()) { final String sHeaderLine = sKey + SEPARATOR_KEY_VALUE + (bUnifyValue ? getUnifiedValue (sValue, bQuoteIfNecessary) : sValue); aConsumer.accept (sHeaderLine); } } } /** * Get all header lines as a list of strings. * * @param bUnifyValue * true to unify the values, false if not * @return Never null but maybe an empty list. */ @Nonnull @ReturnsMutableCopy public ICommonsList getAllHeaderLines (final boolean bUnifyValue) { return getAllHeaderLines (bUnifyValue, DEFAULT_QUOTE_IF_NECESSARY); } /** * Get all header lines as a list of strings. * * @param bUnifyValue * true to unify the values, false if not * @param bQuoteIfNecessary * true to automatically quote values if it is necessary, * false to not do it. This is only used, if "unify * values" is true. * @return Never null but maybe an empty list. * @since 9.3.7 */ @Nonnull @ReturnsMutableCopy public ICommonsList getAllHeaderLines (final boolean bUnifyValue, final boolean bQuoteIfNecessary) { final ICommonsList ret = new CommonsArrayList <> (); forEachHeaderLine (ret::add, bUnifyValue, bQuoteIfNecessary); return ret; } public void setContentLength (final long nLength) { _setHeader (CHttpHeader.CONTENT_LENGTH, Long.toString (nLength)); } public void setContentType (@Nonnull final String sContentType) { _setHeader (CHttpHeader.CONTENT_TYPE, sContentType); } @Nonnull @ReturnsMutableCopy public HttpHeaderMap getClone () { return new HttpHeaderMap (this); } /** * @return The HTTP header map in a different representation. Never * null but maybe empty. * @since 10.0 */ @Nonnull public Map > getAsMapStringToListString () { final Map > ret = new HashMap <> (); // Don't unify here forEachSingleHeader ( (k, v) -> ret.computeIfAbsent (k, k2 -> new ArrayList <> ()).add (v), false, false); return ret; } @Override public boolean equals (final Object o) { if (o == this) return true; if (o == null || !getClass ().equals (o.getClass ())) return false; final HttpHeaderMap rhs = (HttpHeaderMap) o; return m_aHeaders.equals (rhs.m_aHeaders); } @Override public int hashCode () { return new HashCodeGenerator (this).append (m_aHeaders).getHashCode (); } @Override public String toString () { return new ToStringGenerator (this).append ("Headers", m_aHeaders).getToString (); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy